Skip to content
Snippets Groups Projects
Forked from IUT info - R4.02 / ctp-2024
Up to date with the upstream repository.
Thomas Clavier's avatar
Thomas Clavier authored
de79f3e7
History
Name Last commit Last update
.gitlab-ci.yml
README.md
pom.xml

Portefeuille d'investissement

Vous allez coder un système de gestion de portefeuille de valeurs mobilières, "stocks portfolio" en anglais.

Dans le domaine de l'investissement, on parle de valeurs mobilières (qui se vendent facilement) que l'on range dans un portefeuille. Un portefeuille est par exemple composés :

  • de devises (euros, dollars américains, yen, etc.)
  • d'actions (Renault, LVMH, Bouygues, etc.)
  • de matière première (charbon, pétrol, zinc, café, avoine, blé, etc.)
  • de valeurs moins banales comme des cryptos monnaies
  • et de pleins d'autres produits plus ou moins complexes composés des valeurs ci-dessus.

Une fois le portefeuille initialisé avec des devises, il est possible d'acheter d'autres valeurs. Si j'initialise mon portefeuille avec 100€, je vais pouvoir acheter des actions. En sens inverse, il est possible de vendre des valeurs mobilières dans une devise donnée. Je peux par exemple vendre des dollars américains pour avoir des euros. En ayant connaissance des taux de changes pour les devises et des taux de vente des autres valeurs mobilières, il est possible d'évaluer la valeur dans une devise donnée de l'ensemble du portefeuille.

Imaginons par exemple le portefeuille suivants :

  • 120 Euros
  • 120 USD
  • 1 lingo d'or
  • 0.1 Bitcoin
  • 1 baril de brut Brent

En considérant les taux de changes et taux d'achats suivants : 1 EUR = 1.089111 USD 1 lingo = 63962.85 € 1 EUR = 0.000016154758 BTC 1 Baril de Brent = 78.01 €

Alors le portefeuille est estimé à : 120 + 1.089111 * 120 + 63962.85 + 1 / 0.000016154758 * 0.1 + 78.01 = 70481.68 €

Durant ce contrôle TP, vous allez construire en TDD une gestion de portefeuille ne pouvant contenir que des monnaies avec un appel d'API fixer.io pour pouvoir faire l'évaluation dans une devise donnée.

Voici l'interface à respecter

Les fonctionnalités de base

  • Les monnaies sont représentées par leur code ISO 4217 par exemple Currency.EUR
  • Une valeur s'initialise avec un montant et une monnaie. new Stock (5.0, Currency.USD)
  • Il est possible de créer un portefeuille en l'initialisant avec quelques valeurs. Portfolio portfolio = new Portfolio(new Stock (5.2, Currency.USD), new Stock(11.3, Currency.EUR));
  • l'appel à la méthode valueIn(Currency) permet d'avoir une estimation de la valeur du portefeuille dans une monnaie donnée. portfolio.valueIn(Currency.EUR)

Appel fixer.io

Le projet ayant déjà les bonnes dépendances, en supposant le record suivant :

public record FixerIORatesResponse(Long timestamp, String base, LocalDate date, Map<String, Double> rates) {
}

Le code suivant, permet de faire l'appel au service REST :

public FixerIORatesResponse getRates(String token) {
  try {
    HttpRequest request = HttpRequest.newBuilder()
            .uri(new URI(String.format("http://data.fixer.io/api/latest?access_key=%s", token)))
            .GET()
            .build();
    HttpResponse<String> response = HttpClient.newBuilder()
            .build()
            .send(request, BodyHandlers.ofString());
    return mapToObject(response);
  } catch (URISyntaxException | IOException | InterruptedException e) {
    throw new RuntimeException(e);
  }
}

private FixerIORatesResponse mapToObject(HttpResponse<String> response) throws JsonProcessingException {
  ObjectMapper mapper = new ObjectMapper();
  mapper.registerModule(new JavaTimeModule());
  mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
  return mapper.readValue(response.body(), FixerIORatesResponse.class);
}

Si vous souhaitez tester la dé-sérialisation du JSON en object, voici un exemple de JSON retourné par l'API fixer.io :

{
  "success": true,
  "timestamp": 1710500223,
  "base": "EUR",
  "date": "2024-03-15",
  "rates": {
    "AED": 3.99958,
    "AFN": 77.949187,
    "ALL": 103.783967,
    "AMD": 441.269602,
    "ANG": 1.974449,
    "AOA": 906.829628,
    "ARS": 925.995224,
    "AUD": 1.658411,
    "AWG": 1.9604,
    "AZN": 1.849155,
    "BAM": 1.958328,
    "BBD": 2.212106,
    "BDT": 120.235203,
    "BGN": 1.956141,
    "BHD": 0.410485,
    "BIF": 3132.843953,
    "BMD": 1.089111,
    "BND": 1.458883,
    "BOB": 7.570912,
    "BRL": 5.437388,
    "BSD": 1.095614,
    "BTC": 1.6154758e-5,
    "BTN": 90.710291,
    "BWP": 14.855175,
    "BYN": 3.584827,
    "BYR": 21346.572685,
    "BZD": 2.208401,
    "CAD": 1.473774,
    "CDF": 3014.659135,
    "CHF": 0.96234,
    "CLF": 0.037068,
    "CLP": 1022.89635,
    "CNY": 7.837348,
    "COP": 4259.741253,
    "CRC": 559.522245,
    "CUC": 1.089111,
    "CUP": 28.861438,
    "CVE": 110.409547,
    "CZK": 25.18155,
    "DJF": 195.091829,
    "DKK": 7.457254,
    "DOP": 64.743573,
    "DZD": 146.628116,
    "EGP": 52.056888,
    "ERN": 16.336663,
    "ETB": 62.207799,
    "EUR": 1,
    "FJD": 2.440425,
    "FKP": 0.854348,
    "GBP": 0.853955,
    "GEL": 2.886367,
    "GGP": 0.854348,
    "GHS": 14.133503,
    "GIP": 0.854348,
    "GMD": 74.005355,
    "GNF": 9416.068037,
    "GTQ": 8.554042,
    "GYD": 229.210079,
    "HKD": 8.519298,
    "HNL": 26.977104,
    "HRK": 7.495669,
    "HTG": 145.247489,
    "HUF": 393.592135,
    "IDR": 17014.470907,
    "ILS": 3.982089,
    "IMP": 0.854348,
    "INR": 90.243619,
    "IQD": 1435.15253,
    "IRR": 45780.774647,
    "ISK": 148.500387,
    "JEP": 0.854348,
    "JMD": 169.628961,
    "JOD": 0.771961,
    "JPY": 162.027263,
    "KES": 146.481764,
    "KGS": 97.486006,
    "KHR": 4429.71799,
    "KMF": 492.468662,
    "KPW": 980.172421,
    "KRW": 1448.767764,
    "KWD": 0.334771,
    "KYD": 0.912978,
    "KZT": 490.693399,
    "LAK": 22789.644423,
    "LBP": 97475.420954,
    "LKR": 334.762119,
    "LRD": 209.81713,
    "LSL": 20.44264,
    "LTL": 3.215861,
    "LVL": 0.658792,
    "LYD": 5.23314,
    "MAD": 10.98378,
    "MDL": 19.308924,
    "MGA": 4930.364236,
    "MKD": 61.615331,
    "MMK": 2300.669762,
    "MNT": 3699.014472,
    "MOP": 8.826994,
    "MRU": 43.454884,
    "MUR": 50.138198,
    "MVR": 16.768377,
    "MWK": 1832.973728,
    "MXN": 18.19828,
    "MYR": 5.123724,
    "MZN": 69.159703,
    "NAD": 20.431407,
    "NGN": 1731.958033,
    "NIO": 40.08628,
    "NOK": 11.519106,
    "NPR": 145.13768,
    "NZD": 1.786381,
    "OMR": 0.419225,
    "PAB": 1.095614,
    "PEN": 4.027499,
    "PGK": 4.184324,
    "PHP": 60.477777,
    "PKR": 303.862145,
    "PLN": 4.293714,
    "PYG": 8003.330898,
    "QAR": 3.964905,
    "RON": 4.970676,
    "RSD": 117.219918,
    "RUB": 99.811576,
    "RWF": 1402.774776,
    "SAR": 4.084528,
    "SBD": 9.14492,
    "SCR": 14.631474,
    "SDG": 456.883419,
    "SEK": 11.253635,
    "SGD": 1.456364,
    "SHP": 1.38671,
    "SLE": 24.773385,
    "SLL": 24773.385106,
    "SOS": 622.423388,
    "SRD": 38.409127,
    "STD": 22542.395715,
    "SVC": 9.586374,
    "SYP": 14160.267075,
    "SZL": 20.381309,
    "THB": 39.016286,
    "TJS": 11.996385,
    "TMT": 3.822779,
    "TND": 3.37243,
    "TOP": 2.574329,
    "TRY": 35.09089,
    "TTD": 7.438602,
    "TWD": 34.427889,
    "TZS": 2777.232831,
    "UAH": 42.382508,
    "UGX": 4256.572655,
    "USD": 1.089111,
    "UYU": 42.509212,
    "UZS": 13767.771781,
    "VEF": 3944540.464996,
    "VES": 39.460335,
    "VND": 26922.820243,
    "VUV": 130.161613,
    "WST": 2.982949,
    "XAF": 656.80482,
    "XAG": 0.043238,
    "XAU": 0.000502,
    "XCD": 2.943376,
    "XDR": 0.820259,
    "XOF": 656.80482,
    "XPF": 119.331742,
    "YER": 272.65899,
    "ZAR": 20.344101,
    "ZMK": 9803.309845,
    "ZMW": 27.27984,
    "ZWL": 350.69325
  }
}

Ajoutons des fonctionnalités

  • la méthode void buy(Currency CurrencyToBuy, double amount, Currency currencyUsedToBuy) permet d'acheter avec des currencyUsedToBuy la monnaie CurrencyToBuy, l'achat se fait selon le taux de change au moment de l'achat et la banque prend 0,2% du montant en frais de transaction. Attention, il n'est pas possible de faire l'achat s'il n'y a pas suffisamment de currencyUsedToBuy. Les frais de transaction s'appliquent sur le montant en CurrencyToBuy dans une opération à suivre.
  • la méthode void sell(Currency currencyToSell, double amount, Currency currencyTarget) permet de vendre des currencyToSell en currencyTarget, la vente se fait selon le taux de change au moment de la vente et la banque prend 0,2% du montant en frais de transaction. Attention, il n'est pas possible de faire l'achat s'il n'y a pas suffisamment de currencyToSell
  • la méthode void withdraw(double amount, Currency currency) permet de retirer du portefeuille amount de currency pour les mettre sur un compte en banque. La banque ne supporte pour l'instant que les comptes en euro. La banque prend 2% du montant en frais de transaction.
  • la méthode void deposit(double amount, Currency currency) permet d'ajouter amount de currencydans le portefeuille. C'est la seule transaction gratuite.
  • la méthode String[] history(LocaDate start, LocalDate end) permet de lister sous forme de tableau de String toutes les transactions entre start et end. Une ligne de transaction est de la forme Date au format ISO 8601 | Type d'opération | Montant | Devise source format ISO | Devise cible format ISO | Taux de change (source vers cible).
  • la méthode void order(Currency CurrencyToBuy, double amount, Currency currencyUsedToBuy, double lowerRateLimit, double upperRateLimit) permet de lancer un ordre d'achat qui va scruter les taux de changes toutes les minutes pour n'acheter que si le taux est compris entre lowerRateLimit et upperRateLimit. La méthode s'arrête dès que la transaction a pu être réalisée.

Calcul de la performance

  • la méthode double performance() retourne le montant gagné ou perdu depuis la création du portefeuille. La performance est calculé en euro. Toutes les devises restants dans le portefeille sont évalué au taux en vigueur au moment du calcul.

Par exemple :

2024-03-16T10:18:32+01:00 | deposit | 100 | EUR | EUR | 1 EUR = 1 EUR
2024-03-16T10:19:52+01:00 | buy | 120 | EUR | USD | 1 EUR = 1.2 USD
2024-03-16T10:19:52+01:00 | fees | - 2.4 | USD | USD | 1 USD = 1 USD
2024-03-17T10:20:12+01:00 | buy | 105.84 | USD | EUR | 1 USD = .9 EUR
2024-03-17T10:20:12+01:00 | fees | - 2.12 | EUR | EUR | 1 EUR = 1 EUR

Si on détaille le solde opération par opération :

  • 100 EUR
  • (100 EUR * 1.2 = 120 USD) * 0.98 = 117.60 USD
  • (117.60 USD * 0.9 = 105.84 EUR) * 0.98 = 103.72 EUR

Ce qui fait une performance de : 103.72 - 100 = 3.72 €

Attention :

  • les dépôts supplémentaires ne sont pas considérés comme des bénéfices.
  • il est possible de démarrer le portefeuille avec d'autres devises que l'euro, de même qu'il est possible de déposer d'autres devises que l'euro. Dans ce cas la valorisation en euro de départ est celle de la devise convertie au moment de la création ou du dépot.

Par exemple :

2024-03-16T10:18:32+01:00 | deposit | 100 | EUR | EUR | 1 EUR = 1 EUR
2024-03-16T10:18:32+01:00 | deposit | 120 | EUR | USD | 1 EUR = 1.2 EUR
2024-03-16T10:19:52+01:00 | buy | 110 | EUR | USD | 1 EUR = 1.1 USD
2024-03-16T10:19:52+01:00 | fees | - 2.2 | USD | USD | 1 USD = 1 USD
2024-03-17T10:20:12+01:00 | buy | 90 | USD | EUR | 1 USD = .9 EUR
2024-03-17T10:20:12+01:00 | fees | - 1.8 | EUR | EUR | 1 EUR = 1 EUR
2024-03-17T10:21:44+01:00 | deposit | 100 | EUR | EUR | 1 EUR = 1 EUR

Si on détaille le solde portefeuille opération par opération :

Calcul Portefeuille Commentaire
100 EUR 100 EUR Initialisation
120 USD / 1.2 = 100 EUR 100 EUR, 120 USD Pour pouvoir faire la soustraction à la fin
(100 EUR * 1.1 = 110 USD) * 0.98 = 107.80 USD 0 EUR, 227.8 USD
(100 USD * 0.9 = 90 EUR) * 0.98 = 88.2 EUR 88.2 EUR, 127.8 USD
100 EUR 188.2 EUR, 127.8 USD Dépot
127.8 USD * 0.88 = 112.46 EUR 188.2 EUR, 127.8 USD Évaluation du portefeuille en EUR pour calculer la performance

Ce qui fait une performance de (188.2 + 112.46) - (100 + 100 + 100) = 0.66 €

Critères d'évaluation

  • Vos tests doivent être une documentation vivante du projet
    • soit sous forme de Gherkin,
    • soit en JUnit 5 et suffisamment expressif
  • vos tests doivent respecter tous les critères de qualité des bons tests vue en cours
  • Le code doit être écrit en TDD (les commits faisant fois)
  • Pour toutes les interactions hors domaine métier vous veillerez à utiliser une "clean architecture"
  • La couverture de test technique doit être de 100% dans le domain
  • Votre code doit respecter l'API décrite ci-dessous
  • Votre dépot git doit être "propre"