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 descurrencyUsedToBuy
la monnaieCurrencyToBuy
, 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 decurrencyUsedToBuy
. Les frais de transaction s'appliquent sur le montant enCurrencyToBuy
dans une opération à suivre. - la méthode
void sell(Currency currencyToSell, double amount, Currency currencyTarget)
permet de vendre descurrencyToSell
encurrencyTarget
, 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 decurrencyToSell
- la méthode
void withdraw(double amount, Currency currency)
permet de retirer du portefeuilleamount
decurrency
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'ajouteramount
decurrency
dans le portefeuille. C'est la seule transaction gratuite. - la méthode
String[] history(LocaDate start, LocalDate end)
permet de lister sous forme de tableau deString
toutes les transactions entrestart
etend
. Une ligne de transaction est de la formeDate 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 entrelowerRateLimit
etupperRateLimit
. 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"