152 lines
6.7 KiB
Markdown
152 lines
6.7 KiB
Markdown
|
---
|
||
|
title: "Gérer son épargne en action sous Firefly"
|
||
|
date: 2023-12-19T20:10:42+02:00
|
||
|
draft: true
|
||
|
---
|
||
|
|
||
|
## Problématique
|
||
|
|
||
|
## Gestion du portefeuille
|
||
|
|
||
|
### Valeur d'une action
|
||
|
|
||
|
Comme énoncé précédemment, la valeur d'un portefeuille d'actions dépend de la composition de ce portefeuille
|
||
|
(donc de quelles actions et en quelle quantité), et de la valeur unitaire de chacune de ces actions.
|
||
|
|
||
|
Cette valeur unitaire est définie par les marchés financiers et varie continuellement. On considère
|
||
|
que la valeur d'une action est le prix du dernier ordre d'achat passé pour cette action sur les marchés
|
||
|
financiers.
|
||
|
|
||
|
{{< callout type="example" >}}
|
||
|
Considérons la valeur "MSCI World" (un ETF trackant les valeurs des plus grosses entreprises dans
|
||
|
le monde entier). Au 10 décembre, les marchés financiers s'ouvrent avec la valeur de 27.13€ par part
|
||
|
pour cette valeur. à 9h05, une personne passe un ordre d'achat de 15 actions au prix de 27.17€. Dès que
|
||
|
cet ordre est exécuté, on considère que l'action "MSCI World" a la valeur de 27.17€ par action.
|
||
|
{{< /callout >}}
|
||
|
|
||
|
Dans mon cas, je n'ai absolument pas besoin d'une mise à jour rapide des valeurs, une mise à jour
|
||
|
quotidienne est largement suffisante pour mes besoins, et une mise à jour hebdomadaire pourrait même
|
||
|
convenir à vrai dire.
|
||
|
|
||
|
J'ai choisi donc de considérer pour chaque action, sa valeur au moment de la cloture des marchés (17h).
|
||
|
Mais comment calculer cette valeur ?
|
||
|
|
||
|
### Obtenir la valeur d'une action
|
||
|
|
||
|
#### À la recherche d'une API
|
||
|
|
||
|
Pour le reste de cet article, je considèrerai l'action "MSCI Monde" en titre d'exemple. Avec un navigateur,
|
||
|
on arrive facilement à obtenir les informations que l'on veut :
|
||
|
- La bourse européenne (Euronext) nous donne accès à son [cours](https://live.euronext.com/fr/product/etfs/fr0011869353-xpar/lyxor-msci-wor-pea/ewld)
|
||
|
- On obtient facilement les mêmes informations sur [Google Finance](https://www.google.com/finance/quote/EWLD:EPA?hl=fr) ou [Yahoo Finance](https://fr.finance.yahoo.com/quote/EWLD.PA/profile/?guccounter=1)
|
||
|
|
||
|
Cependant, il s'agit d'information destinées à un visionnage humain, aucune API n'est publiquement
|
||
|
disponible pour obtenir ces informations de façon automatisée.
|
||
|
|
||
|
En faisant des recherches sur le sujet, je me heurte rapidement à deux problèmes :
|
||
|
- soit je trouve des services gratuits, mais limités, soit en quantité d'appels (5 appels par jour par exemple)
|
||
|
soit en fonctionnalités (pas d'ETF)
|
||
|
- soit je trouve des services... chers. De l'ordre d'au moins plusieurs dizaines d'euros par mois, ce
|
||
|
qui est complètement en dehors du cadre d'un projet de ce type.
|
||
|
|
||
|
De plus, la majorité des API disponibles sont basées sur les marchés américains. Étant dans des marchés
|
||
|
européens, il est encore plus difficile d'obtenir les informations voulues.
|
||
|
|
||
|
#### Scraping d'Euronext
|
||
|
|
||
|
Finalement, la solution la plus simple revient à faire du scraping d'un site pertinent (cela expose
|
||
|
aux protections anti-bot, mais je n'en ai pas vu sur le site d'Euronext que j'ai décidé d'utiliser,
|
||
|
et le volume de requêtes est en pratique très faible).
|
||
|
|
||
|
Mon point de départ est la fiche d'une action: prenons notre habituel [MSCI world](https://live.euronext.com/fr/product/etfs/fr0011869353-xpar/lyxor-msci-wor-pea/ewld).
|
||
|
|
||
|
Toutes les valeurs intéressantes se trouvent sur un bandeau de la fiche de la valeur. Ici il s'agit du
|
||
|
*dernier cours traité* de 27.188€.
|
||
|
|
||
|
![Bandeau des valeurs de l'action MSCI world sur le site Euronext](euronext.png)
|
||
|
|
||
|
En observant le contenu de la page web, on obtient d'ailleurs ceci :
|
||
|
|
||
|
```html
|
||
|
<div class="col text-ui-grey-0 font-weight-bold data-header-cash ">
|
||
|
<div class="lastprice_min_height pb-1 ">
|
||
|
<span class="data-50 " id="header-instrument-currency">€</span>
|
||
|
<span class="data-60" id="header-instrument-price">27,188</span>
|
||
|
</div>
|
||
|
</div>
|
||
|
```
|
||
|
|
||
|
Il suffirait donc de faire un petit script de scraping accédant à l'élément possédant l'id `#header-instrument-price`
|
||
|
pour obtenir directement la valeur voulue. Mais on peut faire encore mieux en observant les requêtes
|
||
|
AJAX effectués depuis le navigateur.
|
||
|
|
||
|
Une requête en particulier est intéressante, puisqu'elle interroge la route `https://live.euronext.com/intraday_chart/getChartData/FR0011869353-XPAR/intraday`
|
||
|
et renvoie un tableau JSON ayant la forme suivante (le tableau est tronqué, mais contient plus de 1000 éléments):
|
||
|
|
||
|
```json
|
||
|
[
|
||
|
{"time":"2023-12-12 17:29","price":26.879999999999999,"volume":11},
|
||
|
{"time":"2023-12-13 09:04","price":26.974,"volume":1888},
|
||
|
...
|
||
|
{"time":"2023-12-19 17:35","price":27.187999999999999,"volume":32}
|
||
|
]
|
||
|
```
|
||
|
|
||
|
Chaque ligne du tableau contient un ordre d'achat passé au cours de la journée. La valeur intéressante
|
||
|
correspond à la dernière ligne de ce tableau : il s'agit du dernier ordre passé, et correspond donc
|
||
|
exactement à la valeur affichée sur la fiche de la valeur, et est donc la valeur que l'on souhaite
|
||
|
obtenir.
|
||
|
|
||
|
À partir de là, la fonction Python suivante est suffisante pour obtenir la valeur d'une action donnée
|
||
|
|
||
|
```python
|
||
|
EURONEXT_BASE_URL = "https://live.euronext.com/intraday_chart/getChartData"
|
||
|
|
||
|
|
||
|
class StoredAction(NamedTuple):
|
||
|
name: str
|
||
|
value: float
|
||
|
date: str
|
||
|
|
||
|
|
||
|
class Action(NamedTuple):
|
||
|
name: str
|
||
|
code: str
|
||
|
|
||
|
|
||
|
def get_last_value_for_action(action: Action) -> StoredAction:
|
||
|
url = f"{EURONEXT_BASE_URL}/{action.code}/intraday"
|
||
|
resp = requests.get(url)
|
||
|
resp_json = resp.json()
|
||
|
if len(resp_json) < 1:
|
||
|
raise ValueError("Empty list of values")
|
||
|
last_value = resp_json[-1]
|
||
|
try:
|
||
|
return StoredAction(
|
||
|
name=action.name,
|
||
|
value=last_value["price"],
|
||
|
date=last_value["time"],
|
||
|
)
|
||
|
except KeyError as exc:
|
||
|
raise ValueError("Invalid format for response") from exc
|
||
|
```
|
||
|
|
||
|
Il suffit de trouver le **code** associé à l'action voulue. Je le trouve à partir de l'URL de la fiche
|
||
|
produit de l'action cherchée. Dans notre exemple, il s'agit de `https://live.euronext.com/fr/product/etfs/fr0011869353-xpar/lyxor-msci-wor-pea/ewld`.
|
||
|
Le code est donc `fr0011869353-xpar`.
|
||
|
|
||
|
{{< callout type="warning" >}}
|
||
|
De façon surprenante, la deuxième partie du code est sensible à la casse et doit être en majuscules :
|
||
|
- `fr0011869353-xpar` ne renverra aucun résultat
|
||
|
- `fr0011869353-XPAR` renverra le résultat attendu
|
||
|
- `FR0011869353-XPAR` renverra aussi le résultat attendu
|
||
|
- `FR0011869353-xpar` ne renverra aucun résultat
|
||
|
{{< /callout >}}
|
||
|
|
||
|
À partir de ces éléments, il m'est donc possible de calculer la valeur de mon portefeuille à partir
|
||
|
de son contenu. Il me reste deux questions à résoudre :
|
||
|
- comment stocker le contenu de ce portefeuille, et le mettre à jour à chaque fois que j'achète une
|
||
|
action ? (ou vend)
|
||
|
- comment mettre à jour la valeur totale de mon portefeuille telle qu'elle apparaît sur FireflyIII ?
|
||
|
|
||
|
## Lien avec Firefly
|