import%20marimo%0A%0A__generated_with%20%3D%20%220.14.17%22%0Aapp%20%3D%20marimo.App()%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20%22%22%22%0A%20%20%20%20%23%20Yahoo%20pipeline%0A%0A%20%20%20%20Dans%20ce%20notebook%20je%20montre%20un%20exemple%20d'inter-op%C3%A9rabilit%C3%A9%20entre%20polars%2C%20dataframely%2C%20et%20des%20outils%20%22classiques%22%20tels%20que%20pandas%20et%20yfinance%0A%20%20%20%20%22%22%22%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20r%22%22%22%0A%20%20%20%20%23%23%20Imports%0A%0A%20%20%20%20Tout%20d'abord%20on%20importe%20les%20diff%C3%A9rents%20modules%20n%C3%A9cessaires.%20%0A%0A%20%20%20%20Polars%20et%20yfinance%20parlent%20d'eux%20m%C3%AAmes%2C%20et%20enum.auto%20permet%20tout%20simplement%20d'%C3%A9viter%20la%20redondance%20lors%20de%20la%20d%C3%A9finition%20d'%C3%A9num%C3%A9rations.%0A%0A%20%20%20%20En%20revanche%20pathlib%2C%20dataframely%20et%20framelib%20m%C3%A9ritent%20que%20l'on%20s'attarde%20dessus.%0A%20%20%20%20%22%22%22%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20from%20enum%20import%20auto%0A%20%20%20%20from%20typing%20import%20Any%0A%0A%20%20%20%20import%20polars%20as%20pl%0A%20%20%20%20import%20yfinance%20as%20yf%20%20%23%20type%3A%20ignore%5Bimport%5D%0A%20%20%20%20return%20Any%2C%20auto%2C%20pl%2C%20yf%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20r%22%22%22%0A%20%20%20%20%23%23%20Pathlib%0A%0A%20%20%20%20pathlib%20remplace%20os%20pour%20la%20gestion%20des%20chemins%20en%20python%20et%20est%20tr%C3%A8s%20tr%C3%A8s%20pratique.%0A%20%20%20%20le%20code%20ci-dessous%20permet%20de%20dire%20que%20je%20veux%20un%20folder%20%22yf%22%20au%20meme%20niveau%20que%20mon%20fichier%20python.%0A%20%20%20%20%22%22%22%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20from%20pathlib%20import%20Path%0A%0A%20%20%20%20FOLDER%20%3D%20Path(__file__).resolve().parent.joinpath(%22yf%22)%0A%20%20%20%20FOLDER%0A%20%20%20%20return%20(FOLDER%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20r%22%22%22%0A%20%20%20%20%23%23%23%20Dataframely%0A%20%20%20%20Cette%20library%20permet%20de%20d%C3%A9finir%20de%20fa%C3%A7on%20structur%C3%A9e%20les%20donn%C3%A9es%20que%20l'on%20manipule.%20On%20peut%20ainsi%20d%C3%A9finir%20des%20sch%C3%A9mas%20de%20donn%C3%A9es%2C%20leurs%20colonnes%2C%20et%20des%20collections%20regroupant%20ces%20sch%C3%A9mas.%0A%20%20%20%20Liens%20utiles%20-%3E%0A%0A%20%20%20%20https%3A%2F%2Fdataframely.readthedocs.io%2Fen%2Flatest%2F%0A%0A%20%20%20%20https%3A%2F%2Fgithub.com%2FQuantco%2Fdataframely%0A%20%20%20%20%22%22%22%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20dataframely%20as%20dy%0A%0A%0A%20%20%20%20class%20MySchema(dy.Schema)%3A%0A%20%20%20%20%20%20%20%20name%20%3D%20dy.String(nullable%3DFalse)%0A%20%20%20%20%20%20%20%20age%20%3D%20dy.UInt8(nullable%3DFalse)%0A%20%20%20%20%20%20%20%20height%20%3D%20dy.Float32(nullable%3DTrue)%0A%0A%0A%20%20%20%20MySchema.polars_schema()%0A%20%20%20%20return%20MySchema%2C%20dy%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20r%22%22%22%0A%20%20%20%20%23%23%23%20Framelib%0A%20%20%20%20Framelib%20est%20ma%20library%20d'extensions%20d'utilitaires%20divers%20pour%20Polars.%0A%0A%20%20%20%20https%3A%2F%2Fgithub.com%2FOutSquareCapital%2Fframelib%0A%0A%20%20%20%20%23%23%23%23%20framelib.PolarsEnum%0A%0A%20%20%20%20Une%20classe%20simple%20qui%20h%C3%A9rite%20de%20enum.Strenum%2C%20et%20ajoute%20simplement%202%20m%C3%A9thodes%20(to_pl%20et%20to_list)%2C%20afin%20de%20faciliter%20les%20interactions%20polars%2Fdataframely%2Fenums%20pythons.%0A%0A%20%20%20%20%23%23%23%23%20framelib.schemas%0A%0A%20%20%20%20Ce%20module%20contient%20une%20class%20d'interface%20Schema%2C%20qui%20h%C3%A9rite%20de%20%60dy.Schema%60%2C%20ainsi%20que%20des%20impl%C3%A9mentations%20telles%20que%20CSV%2C%20Parquet%2C%20ou%20NDJSON.%0A%0A%20%20%20%20Ceci%20permet%20de%20d%C3%A9finir%20des%20structures%20de%20donn%C3%A9es%20qui%20seront%20sauvegard%C3%A9s%20dans%20un%20dossier%20sp%C3%A9cifique%20via%20le%20dunder%20attribute%20%60__directory__%60.%0A%0A%20%20%20%20De%20plus%2C%20ces%20classes%20fournissent%20des%20attributs%20%60read%60%20et%20%60scan%60.%0A%0A%20%20%20%20Ces%20derniers%20sont%20en%20fait%20des%20partials%20funcs%20(via%20functools.partial)%20pr%C3%A9-remplies%20par%20le%20chemin%20cr%C3%A9%C3%A9%20automatiquement%20lors%20de%20la%20d%C3%A9claration%20de%20la%20classe%2C%20ce%20dernier%20%C3%A9tant%20accessible%20via%20la%20methode%20%60Schema.path()%60%0A%0A%20%20%20%20Le%20path%20g%C3%A9n%C3%A9r%C3%A9%20par%20cette%20m%C3%A9thode%20se%20compose%20de%20la%20mani%C3%A8re%20suivante%20-%3E%0A%0A%20%20%20%20-%20l'attribut%20__directory__%20-%3E%20folder%0A%20%20%20%20-%20le%20nom%20de%20la%20class%20-%3E%20fichier%20final%0A%20%20%20%20-%20sa%20sous-class%20(Parquet%2C%20CSV...)%20-%3E%20son%20extension%0A%0A%20%20%20%20Les%20param%C3%A8tres%20pour%20la%20lecture%20et%20l'%C3%A9criture%20des%20donn%C3%A9es%20sont%20ainsi%20centralis%C3%A9s%20dans%20les%20sch%C3%A9mas%20les%20concernant.%0A%0A%20%20%20%20Ci-dessous%20un%20exemple%20d'h%C3%A9ritage%20par%20composition%20pour%20d%C3%A9montrer%20la%20flexibilit%C3%A9%20du%20framework.%0A%20%20%20%20%22%22%22%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(FOLDER%2C%20MySchema%2C%20dy)%3A%0A%20%20%20%20from%20framelib%20import%20PolarsEnum%2C%20schemas%0A%0A%0A%20%20%20%20class%20MyFile(schemas.Parquet%2C%20MySchema)%3A%0A%20%20%20%20%20%20%20%20__directory__%20%3D%20FOLDER%0A%20%20%20%20%20%20%20%20id%20%3D%20dy.UInt32(nullable%3DFalse%2C%20primary_key%3DTrue)%0A%0A%0A%20%20%20%20MyFile.path().touch()%20%20%23%20cr%C3%A9%C3%A9e%20un%20fichier%20vide%20placeholder%0A%20%20%20%20MyFile.show_tree()%0A%20%20%20%20return%20PolarsEnum%2C%20schemas%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20r%22%22%22%0A%20%20%20%20%23%23%20D%C3%A9claration%20de%20la%20configuration%0A%0A%20%20%20%20Pour%20commencer%2C%20on%20d%C3%A9clare%20nos%20constantes%20de%20configuration.%0A%0A%20%20%20%20Marimo%20permet%20de%20facilement%20cr%C3%A9er%20de%20l'UI%20interactive%2C%20ce%20qui%20permet%20de%20simplifier%20les%20UI%20prototypes%20de%20mani%C3%A8re%20significative.%0A%20%20%20%20%22%22%22%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20r%22%22%22%0A%20%20%20%20%23%23%23%20Date%20de%20d%C3%A9but%0A%20%20%20%20Afin%20de%20configurer%20la%20plage%20de%20dates%20pour%20la%20r%C3%A9cup%C3%A9ration%20des%20donn%C3%A9es%2C%20nous%20utilisons%20un%20calendrier%20interactif.%0A%20%20%20%20%22%22%22%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20START_DATE%20%3D%20mo.ui.date(start%3D%222025-01-01%22%2C%20label%3D%22Start%20Date%22)%0A%20%20%20%20START_DATE%0A%20%20%20%20return%20(START_DATE%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20r%22%22%22%0A%20%20%20%20%23%23%23%20Actifs%20et%20cat%C3%A9gories%0A%0A%20%20%20%20On%20d%C3%A9finit%20ici%20des%20enums%20de%20cat%C3%A9gorie%2C%20un%20NamedTuple%20pour%20les%20actifs%20choisis%2C%20ainsi%20qu'un%20registery%20qui%20centralise%20la%20configuration.%20%0A%0A%20%20%20%20La%20liste%20d'actifs%20pourrait%20ainsi%20%C3%AAtre%20stock%C3%A9%20dans%20un%20NDJSON%20par%20exemple%2C%20et%2C%20en%20%C3%A9tendant%20la%20classe%20Registery%2C%20potentiellement%20%C3%AAtre%20utilis%C3%A9e%20dans%20l'impl%C3%A9mentation%20d'une%20logique%20front-end%20pour%20une%20configuration%20dynamique.%0A%0A%20%20%20%20Joindre%20les%20clusters%2Fproducts%20au%20data%20final%20est%20faisable%20par%20la%20suite%20via%20pl.DataFrame.join%2C%20pour%20manipuler%20les%20donn%C3%A9es%20selon%20ces%20crit%C3%A8res.%0A%20%20%20%20%22%22%22%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(PolarsEnum%2C%20START_DATE%2C%20auto%2C%20pl)%3A%0A%20%20%20%20from%20dataclasses%20import%20dataclass%0A%20%20%20%20from%20typing%20import%20NamedTuple%0A%0A%0A%20%20%20%20class%20Product(PolarsEnum)%3A%0A%20%20%20%20%20%20%20%20STOCK%20%3D%20auto()%0A%20%20%20%20%20%20%20%20ETF%20%3D%20auto()%0A%0A%0A%20%20%20%20class%20Cluster(PolarsEnum)%3A%0A%20%20%20%20%20%20%20%20EQUITIES%20%3D%20auto()%0A%20%20%20%20%20%20%20%20COMMODITIES%20%3D%20auto()%0A%20%20%20%20%20%20%20%20BONDS%20%3D%20auto()%0A%0A%0A%20%20%20%20class%20Asset(NamedTuple)%3A%0A%20%20%20%20%20%20%20%20ticker%3A%20str%0A%20%20%20%20%20%20%20%20product%3A%20Product%0A%20%20%20%20%20%20%20%20cluster%3A%20Cluster%0A%0A%0A%20%20%20%20%40dataclass(slots%3DTrue)%0A%20%20%20%20class%20AssetRegistery%3A%0A%20%20%20%20%20%20%20%20data%3A%20list%5BAsset%5D%0A%20%20%20%20%20%20%20%20start_date%3A%20str%0A%0A%20%20%20%20%20%20%20%20%40property%0A%20%20%20%20%20%20%20%20def%20tickers(self)%20-%3E%20list%5Bstr%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%5Basset.ticker%20for%20asset%20in%20self.data%5D%0A%0A%20%20%20%20%20%20%20%20def%20to_df(self)%20-%3E%20pl.DataFrame%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20pl.DataFrame(self.data)%20%20%23%20output%20qui%20suit%2C%20a%20titre%20d'exemple%0A%0A%0A%20%20%20%20ALL_ASSETS%20%3D%20AssetRegistery(%0A%20%20%20%20%20%20%20%20data%3D%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20Asset(%22AAPL%22%2C%20Product.STOCK%2C%20Cluster.EQUITIES)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20Asset(%22MSFT%22%2C%20Product.STOCK%2C%20Cluster.EQUITIES)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20Asset(%22GOOGL%22%2C%20Product.STOCK%2C%20Cluster.EQUITIES)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20Asset(%22AMZN%22%2C%20Product.STOCK%2C%20Cluster.EQUITIES)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20Asset(%22TSLA%22%2C%20Product.STOCK%2C%20Cluster.EQUITIES)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20Asset(%22SPY%22%2C%20Product.ETF%2C%20Cluster.EQUITIES)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20Asset(%22GLD%22%2C%20Product.ETF%2C%20Cluster.COMMODITIES)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20Asset(%22DBC%22%2C%20Product.ETF%2C%20Cluster.COMMODITIES)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20Asset(%22TLT%22%2C%20Product.ETF%2C%20Cluster.BONDS)%2C%0A%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20start_date%3DSTART_DATE.value%2C%0A%20%20%20%20)%0A%20%20%20%20return%20ALL_ASSETS%2C%20AssetRegistery%0A%0A%0A%40app.cell%0Adef%20_(ALL_ASSETS)%3A%0A%20%20%20%20ALL_ASSETS.to_df()%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20r%22%22%22%0A%20%20%20%20%23%23%23%20Appart%C3%A9%20sur%20l'utilisation%20du%20Namedtuple%20et%20dataclass%0A%0A%20%20%20%20TLDR%3A%20plus%20concis%2C%20plus%20s%C3%A9curis%C3%A9%2C%20plus%20rapide%20et%20plus%20memory%20efficient%20que%20les%20classes%20standard.%0A%0A%20%20%20%20Plut%C3%B4t%20que%20de%20d%C3%A9finir%20des%20classes%20Python%20%22simples%22%2C%20on%20utilise%20ici%20le%20d%C3%A9corateur%20dataclasses.dataclass%2C%20et%20la%20classe%20de%20base%20typing.NamedTuple.%20Pourquoi%3F%0A%0A%20%20%20%20Ces%20deux%20alternatives%20%C3%A9vitent%20tout%20d'abord%20la%20redondance%20d'%C3%A9crire%20un%20__init__%2C%20les%20arguments%2C%20et%20les%20self.attribut%20associ%C3%A9s%2C%20qui%20am%C3%A8nent%20souvent%20a%20ce%20genre%20de%20situation%20-%3E%0A%20%20%20%20%60%60%60python%0A%20%20%20%20class%20Point%0A%20%20%20%20%20%20%20%20__slots__%20%3D%20(%22x%22%2C%20%22y%22)%0A%20%20%20%20%20%20%20%20def%20__init__(self%2C%20x%3A%20float%2C%20y%3A%20float)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20self.x%3A%20float%20%3D%20x%0A%20%20%20%20%20%20%20%20%20%20%20%20self.y%3A%20float%20%3D%20y%0A%20%20%20%20%60%60%60%0A%0A%20%20%20%20O%C3%B9%20lorsque%20l'init%20ne%20contient%20pas%20de%20logique%20particuli%C3%A8re%2C%20on%20se%20retrouve%20a%20r%C3%A9%C3%A9crire%203%20(4%20avec%20slots!)%20fois%20chaque%20variable.%0A%0A%20%20%20%20Mais%20ces%20alternatives%20divergent%20ensuite%20en%20plusieurs%20points.%0A%0A%20%20%20%20NamedTuple%20est%20en%20r%C3%A9alit%C3%A9%20un%20tuple%20(surprenant%20je%20sais)%2C%20donc%20immutable%20et%20bien%20plus%20memory%20efficient%20qu'une%20class%20de%20base.%0A%0A%20%20%20%20Une%20dataclass%20en%20revanche%20a%20des%20attributs%20mutables%20(sauf%20si%20l'on%20pr%C3%A9cise%20frozen%3DTrue%20au%20d%C3%A9corateur%2C%20mais%20cela%20%C3%A0%20un%20co%C3%BBt%20de%20performance)%2C%20mais%20n'est%20pas%20un%20tuple.%20%0A%0A%20%20%20%20Elle%20reste%20une%20classe%20%22standard%22%2C%20mais%20augment%C3%A9e%2C%20de%20par%20l'ajout%20de%20nombreuse%20m%C3%A9thodes%20pr%C3%A9-initialis%C3%A9es%20(__repr__%2C%20__eq__%2C%20etc)%2C%20est%20tr%C3%A8s%20facilement%20plus%20memory%20efficient%20via%20le%20param%C3%A8tre%20%60slots%3DTrue%60%20(d%C3%A9finir%20les%20slots%20dans%20une%20classe%20%22standard%22%20est%20%C3%A9galement%20faisable%2C%20mais%20cela%20reste%20plus%20verbeux)%2C%20et%20est%20de%20facto%20ma%20mani%C3%A8re%20usuelle%20de%20d%C3%A9finir%20des%20classes%20qui%20n'ont%20pas%20de%20constructeur%2Fh%C3%A9ritage%20complexe.%0A%0A%20%20%20%20%60slots%3DTrue%60%20a%20%C3%A9galement%20le%20tr%C3%A8s%20grand%20avantage%20d'interdire%20toute%20g%C3%A9n%C3%A9ration%20d'attribut%20dynamique%20APRES%20l'instanciation%20d'un%20objet.%20%0A%0A%20%20%20%20Donc%2C%20pour%20reprendre%20l'exemple%20de%20la%20classe%20Point%20d%C3%A9finie%20plus%20t%C3%B4t%2C%20si%20on%20l'avait%20d%C3%A9finie%20en%20tant%20que%20dataclass(slots%3DTrue)%2C%20et%20que%20l'on%20aurait%20ensuite%20essay%C3%A9%20de%20faire%20%60self.z%20%3D%203%60%20plus%20tard%2C%20ceci%20aurait%20r%C3%A9sult%C3%A9%20en%20une%20runtime%20error%20claire%2C%20%C3%A9vitant%20ainsi%20certains%20bugs%20pernicieux%20d%C3%BBs%20a%20des%20fautes%20de%20frappes.%20%0A%0A%20%20%20%20En%20effet%2C%20la%20classe%20avec%20slots%20ne%20maintient%20plus%20un%20dictionnaire%20interne%20mutable%20une%20fois%20instanci%C3%A9%2C%20ce%20qui%20a%20par%20ailleurs%20comme%20bonus%20final%20d'am%C3%A9liorer%20les%20performances%20d'environ%20%2B20%25.%0A%0A%20%20%20%20Plus%20de%20d%C3%A9tails%20ici%20-%3E%0A%0A%20%20%20%20https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DCvQ7e6yUtnw%26t%3D804s%26ab_channel%3DArjanCodes%0A%20%20%20%20%22%22%22%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20r%22%22%22%0A%20%20%20%20%23%23%20R%C3%A9cup%C3%A9ration%20du%20data%0A%20%20%20%20Une%20fois%20la%20configuration%20instanci%C3%A9e%2C%20on%20peut%20r%C3%A9cup%C3%A9rer%20les%20donn%C3%A9es%20de%20l'API%20Yahoo%20Finance.%20%0A%0A%20%20%20%20On%20d%C3%A9finit%20la%20fonction%20%60fetch%60%20pour%20t%C3%A9l%C3%A9charger%20les%20donn%C3%A9es%20des%20tickers%20sp%C3%A9cifi%C3%A9s%20dans%20la%20config%2C%20puis%20on%20les%20convertit%20en%20un%20dataframe%20Polars.%0A%20%20%20%20%22%22%22%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Any%2C%20pl%2C%20yf)%3A%0A%20%20%20%20def%20fetch_from_yf(assets%3A%20list%5Bstr%5D%2C%20start%3A%20str)%20-%3E%20pl.DataFrame%3A%0A%20%20%20%20%20%20%20%20try%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20data%3A%20Any%20%3D%20yf.download(%20%20%23%20type%3A%20ignore%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20tickers%3Dassets%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20interval%3D%221d%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20start%3Dstart%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20auto_adjust%3DTrue%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20progress%3DFalse%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20except%20Exception%20as%20e%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20ValueError(f%22Error%20fetching%20data%3A%20%7Be%7D%22)%20from%20e%0A%20%20%20%20%20%20%20%20if%20data%20is%20None%20or%20data.empty%3A%20%20%23%20type%3A%20ignore%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20ValueError(%22No%20data%20returned.%22)%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20pl.from_pandas(data%3Ddata%2C%20include_index%3DTrue)%20%20%23%20type%3A%20ignore%5Breturn-value%5D%0A%20%20%20%20return%20(fetch_from_yf%2C)%0A%0A%0A%40app.cell%0Adef%20_(ALL_ASSETS%2C%20fetch_from_yf)%3A%0A%20%20%20%20df_yf%20%3D%20fetch_from_yf(ALL_ASSETS.tickers%2C%20%222025-07-01%22)%0A%20%20%20%20df_yf%0A%20%20%20%20return%20(df_yf%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20r%22%22%22%0A%20%20%20%20%23%23%23%20D%C3%A9claration%20des%20sch%C3%A9mas%0A%0A%20%20%20%20Ci%20dessous%2C%20les%20sch%C3%A9mas%20de%20la%20pipeline%20sont%20d%C3%A9clar%C3%A9s.%0A%0A%20%20%20%20Plusieurs%20%C3%A9l%C3%A9ments%20int%C3%A9ressants%20notables%3A%0A%0A%20%20%20%20-%20Les%20schemas%20h%C3%A9ritent%20entre%20eux%20afin%20de%20facilement%20d%C3%A9finir%20des%20structures%20interm%C3%A9diaires%20communes%0A%0A%20%20%20%20-%20Chaque%20schema%20d%C3%A9fini%20le%20type%20de%20sa%20colonne.%20Les%20PolarsEnum%20sont%20tr%C3%A8s%20pratiques%20ici.%0A%0A%20%20%20%20-%20La%20collection%20Pipeline%20permet%20de%20d%C3%A9clarer%20la%20structure%20logique%20attendue.%0A%0A%20%20%20%20Tout%20ceci%20permet%20une%20self-documentation%20et%20une%20validation%20des%20donn%C3%A9es%20%C3%A0%20chaque%20%C3%A9tape%20du%20pipeline.%0A%20%20%20%20%22%22%22%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(FOLDER%2C%20dy%2C%20schemas)%3A%0A%20%20%20%20class%20RawDF(dy.Schema)%3A%0A%20%20%20%20%20%20%20%20date%20%3D%20dy.Datetime(nullable%3DTrue)%0A%20%20%20%20%20%20%20%20index%20%3D%20dy.String(nullable%3DTrue)%0A%20%20%20%20%20%20%20%20data%20%3D%20dy.Float64(nullable%3DTrue)%0A%0A%0A%20%20%20%20class%20CommonCols(dy.Schema)%3A%0A%20%20%20%20%20%20%20%20ticker%20%3D%20dy.Categorical(nullable%3DFalse)%0A%20%20%20%20%20%20%20%20date%20%3D%20dy.Date(nullable%3DFalse)%0A%0A%0A%20%20%20%20class%20CleanedDF(CommonCols)%3A%0A%20%20%20%20%20%20%20%20data%20%3D%20dy.Float64(nullable%3DTrue)%0A%20%20%20%20%20%20%20%20category%20%3D%20dy.String(nullable%3DTrue)%0A%0A%0A%20%20%20%20class%20FinalDF(CommonCols%2C%20schemas.Parquet)%3A%0A%20%20%20%20%20%20%20%20__directory__%20%3D%20FOLDER%0A%20%20%20%20%20%20%20%20open%20%3D%20dy.Float32(nullable%3DTrue)%0A%20%20%20%20%20%20%20%20high%20%3D%20dy.Float32(nullable%3DTrue)%0A%20%20%20%20%20%20%20%20low%20%3D%20dy.Float32(nullable%3DTrue)%0A%20%20%20%20%20%20%20%20close%20%3D%20dy.Float32(nullable%3DTrue)%0A%20%20%20%20%20%20%20%20volume%20%3D%20dy.UInt32(nullable%3DTrue)%0A%0A%0A%20%20%20%20class%20Pipeline(dy.Collection)%3A%0A%20%20%20%20%20%20%20%20raw_df%3A%20dy.LazyFrame%5BRawDF%5D%0A%20%20%20%20%20%20%20%20cleaned_df%3A%20dy.LazyFrame%5BCleanedDF%5D%0A%20%20%20%20%20%20%20%20final_df%3A%20dy.LazyFrame%5BFinalDF%5D%0A%20%20%20%20return%20CleanedDF%2C%20FinalDF%2C%20RawDF%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20r%22%22%22%0A%20%20%20%20%23%23%20Premi%C3%A8re%20transformation%0A%0A%20%20%20%20Passer%20d'un%20dataframe%20pandas%20a%20polars%20est%20trivial.%0A%0A%20%20%20%20-%20On%20renomme%20d'abord%20la%20colonne%20de%20date%20en%20lowercase.%0A%20%20%20%20-%20On%20unpivot%20le%20dataframe%20(wide%20format%20to%20long%20format)%2C%20afin%20d'aplatir%20le%20multi-index%20de%20pandas.%0A%20%20%20%20-%20On%20valide%20en%20appelant%20la%20fonction%20validate%20de%20dataframely.%20Ceci%20appelle%20%60collect()%60%20du%20dataframe%20polars%20(ex%C3%A9cute%20le%20query%20plan)%2C%20puis%20s%C3%A9lectionne%20et%20convertit%20les%20colonnes%20de%20notre%20sch%C3%A9ma.%0A%0A%20%20%20%20La%20colonne%20Category%20contient%20ainsi%20les%20labels%20OHLC%20%26%20volume%20associ%C3%A9s%20au%20ticker%2C%20data%20contenant%20les%20valeurs%20associ%C3%A9es.%0A%0A%20%20%20%20Concr%C3%A8tement%20-%3E%0A%0A%20%20%20%20-%20la%20colonne%20date%20contient%20l'index%20vertical%20pandas%20(index)%0A%20%20%20%20-%20la%20colonne%20index%20contient%20le%20multi-index%20horizontal%20pandas%20(variable)%0A%20%20%20%20-%20la%20colonne%20data%20contient%20les%20donn%C3%A9es%20%22concr%C3%A8tes%22%20du%20dataframe%20(value)%0A%20%20%20%20%22%22%22%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(RawDF%2C%20dy%2C%20pl)%3A%0A%20%20%20%20def%20to_raw_df(df%3A%20pl.DataFrame)%20-%3E%20dy.DataFrame%5BRawDF%5D%3A%0A%20%20%20%20%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20df.lazy()%0A%20%20%20%20%20%20%20%20%20%20%20%20.rename(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22Date%22%3A%20RawDF.date.name%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20%20%20%20%20.unpivot(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20index%3DRawDF.date.name%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20variable_name%3DRawDF.index.name%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20value_name%3DRawDF.data.name%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20%20%20%20%20.pipe(RawDF.validate%2C%20cast%3DTrue)%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20return%20(to_raw_df%2C)%0A%0A%0A%40app.cell%0Adef%20_(df_yf%2C%20to_raw_df)%3A%0A%20%20%20%20df_raw%20%3D%20df_yf.pipe(to_raw_df)%0A%20%20%20%20df_raw%0A%20%20%20%20return%20(df_raw%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20r%22%22%22%0A%20%20%20%20%23%23%20Deuxi%C3%A8me%20transformation%0A%0A%20%20%20%20Une%20fois%20le%20data%20pivot%C3%A9%20r%C3%A9cup%C3%A9r%C3%A9%2C%20il%20faut%20maintenant%20manipuler%20les%20valeurs%20afin%20de%20s%C3%A9parer%20tickers%20et%20categories%2C%20%C3%A9tant%20donn%C3%A9%20que%20l'on%20r%C3%A9cup%C3%A8re%20de%20base%20un%20df%20pandas%20multi-index.%20%0A%0A%20%20%20%20Pour%20ce%20faire%2C%20on%20utilise%20des%20expressions%20polars%20afin%20de%20manipuler%20les%20list%20et%20str%20dans%20la%20colonne%20de%20categories%2C%20les%20s%C3%A9parer%2C%20et%20les%20renommer.%0A%0A%20%20%20%20Le%20fait%20de%20split%20le%20nom%20transforme%20la%20valeur%20de%20la%20colonne%20d'un%20str%20unique%20a%20une%20liste%20de%20str.%20Les%20namespaces%20%22str%22%20et%20%22list%22%20de%20polars.Expr%20sont%20souvent%20utilis%C3%A9s%20de%20mani%C3%A8re%20back-and-forth%20lors%20de%20ce%20genre%20de%20manipulation%0A%20%20%20%20%22%22%22%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(CleanedDF%2C%20RawDF%2C%20dy%2C%20pl)%3A%0A%20%20%20%20def%20to_clean_df(df%3A%20pl.LazyFrame)%20-%3E%20dy.DataFrame%5BCleanedDF%5D%3A%0A%20%20%20%20%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20df.with_columns(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20RawDF.index.col.pipe(clean_category_name).alias(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20CleanedDF.category.name%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20RawDF.index.col.pipe(clean_ticker_name).alias(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3DCleanedDF.ticker.name%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20).drop_nulls(subset%3DCleanedDF.data.name)%0A%20%20%20%20%20%20%20%20).pipe(CleanedDF.validate%2C%20cast%3DTrue)%0A%0A%0A%20%20%20%20def%20clean_category_name(expr%3A%20pl.Expr)%20-%3E%20pl.Expr%3A%0A%20%20%20%20%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20expr.str.split(by%3D%22%2C%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20.list.first()%0A%20%20%20%20%20%20%20%20%20%20%20%20.str.replace_all(pattern%3D%22'%22%2C%20value%3D%22%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20.str.strip_prefix(prefix%3D%22(%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20.str.strip_chars()%0A%20%20%20%20%20%20%20%20)%0A%0A%0A%20%20%20%20def%20clean_ticker_name(expr%3A%20pl.Expr)%20-%3E%20pl.Expr%3A%0A%20%20%20%20%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20expr.str.split(by%3D%22%2C%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20.list.last()%0A%20%20%20%20%20%20%20%20%20%20%20%20.str.replace_all(pattern%3D%22'%22%2C%20value%3D%22%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20.str.strip_suffix(suffix%3D%22)%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20.str.strip_chars()%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20return%20(to_clean_df%2C)%0A%0A%0A%40app.cell%0Adef%20_(df_raw%2C%20to_clean_df)%3A%0A%20%20%20%20df_raw.pipe(to_clean_df).head(20)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20r%22%22%22%0A%20%20%20%20%23%23%20Finalisation%0A%0A%20%20%20%20Il%20nous%20faut%20maintenant%20s%C3%A9parer%20les%20diff%C3%A9rents%20types%20de%20donn%C3%A9es%20(volume%2C%20open%2C%20high%2C%20etc...)%20en%20plusieurs%20colonnes.%0A%20%20%20%20Pour%20ce%20faire%20-%3E%0A%0A%20%20%20%20-%20On%20pivote%20le%20data%20(wide%20to%20long)%20une%20derni%C3%A8re%20fois%20pour%20obtenir%20un%20dataframe%20final%20avec%20les%20colonnes%20OHLC%20et%20volume.%20Date%20et%20ticker%20%C3%A9tant%20d%C3%A9ja%20%22ok%22%2C%20on%20les%20passe%20en%20index%2C%20category%20en%20%22on%22%20%C3%A9tant%20donn%C3%A9%20que%20c'est%20la%20colonne%20qu'on%20veut%20%22aplatir%22%2C%20et%20la%20colonne%20%22data%22%20correspond%20a%20values%20car%20c'est%20les%20valeurs%20associ%C3%A9es%20a%20chaque%20category%0A%20%20%20%20-%20On%20renomme%20les%20colonnes%20via%20une%20lambda%20en%20lowercase%0A%20%20%20%20-%20On%20trie%20les%20donn%C3%A9es%20par%20date%2C%20puis%20par%20ticker%0A%20%20%20%20-%20On%20valide%20et%20mat%C3%A9rialise%20le%20data%0A%0A%0A%20%20%20%20Ceci%20nous%20permet%20%C3%A9galement%20de%20d%C3%A9finir%20notre%20main%20function%20qui%20encapsule%20toute%20la%20pipeline%20d%C3%A9finie%20jusqu'%C3%A0%20pr%C3%A9sent.%20%0A%20%20%20%20Cette%20derni%C3%A8re%20contient%20%C3%A9galement%20un%20bool%C3%A9en%20de%20param%C3%A8trage%20pour%20sauvegarder%20(ou%20non)%20le%20r%C3%A9sultat%20final%0A%20%20%20%20%22%22%22%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20AssetRegistery%2C%0A%20%20%20%20CleanedDF%2C%0A%20%20%20%20FinalDF%2C%0A%20%20%20%20dy%2C%0A%20%20%20%20fetch_from_yf%2C%0A%20%20%20%20pl%2C%0A%20%20%20%20to_clean_df%2C%0A%20%20%20%20to_raw_df%2C%0A)%3A%0A%20%20%20%20def%20convert_to_polars(df%3A%20pl.DataFrame)%20-%3E%20dy.DataFrame%5BFinalDF%5D%3A%0A%20%20%20%20%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20df.pivot(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20index%3D%5BCleanedDF.date.name%2C%20CleanedDF.ticker.name%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20on%3DCleanedDF.category.name%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20values%3DCleanedDF.data.name%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20%20%20%20%20.lazy()%0A%20%20%20%20%20%20%20%20%20%20%20%20.rename(lambda%20x%3A%20x.lower())%0A%20%20%20%20%20%20%20%20%20%20%20%20.sort(FinalDF.date.col%2C%20FinalDF.ticker.col)%0A%20%20%20%20%20%20%20%20%20%20%20%20.pipe(FinalDF.validate%2C%20cast%3DTrue)%0A%20%20%20%20%20%20%20%20)%0A%0A%0A%20%20%20%20def%20main(config%3A%20AssetRegistery%2C%20save%3A%20bool)%20-%3E%20dy.DataFrame%5BFinalDF%5D%3A%0A%20%20%20%20%20%20%20%20df%20%3D%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20fetch_from_yf(config.tickers%2C%20config.start_date)%0A%20%20%20%20%20%20%20%20%20%20%20%20.pipe(to_raw_df)%0A%20%20%20%20%20%20%20%20%20%20%20%20.pipe(to_clean_df)%0A%20%20%20%20%20%20%20%20%20%20%20%20.pipe(convert_to_polars)%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20if%20save%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20df.write_parquet(FinalDF.path(make_dir%3DTrue))%0A%0A%20%20%20%20%20%20%20%20return%20df%0A%20%20%20%20return%20(main%2C)%0A%0A%0A%40app.cell%0Adef%20_(ALL_ASSETS%2C%20main)%3A%0A%20%20%20%20final_df%20%3D%20main(config%3DALL_ASSETS%2C%20save%3DFalse)%0A%20%20%20%20final_df%0A%20%20%20%20return%20(final_df%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20r%22%22%22%0A%20%20%20%20%23%23%20Plot%20bonus%0A%0A%20%20%20%20On%20peut%20facilement%20utiliser%20ploty.express%20afin%20de%20visualiser%20les%20donn%C3%A9es%20extraites.%0A%20%20%20%20%22%22%22%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(FinalDF%2C%20dy%2C%20final_df%2C%20pl)%3A%0A%20%20%20%20import%20plotly.express%20as%20px%0A%0A%0A%20%20%20%20def%20normalize_log(expr%3A%20pl.Expr)%20-%3E%20pl.Expr%3A%0A%20%20%20%20%20%20%20%20return%20expr.log().sub(expr.log().first()).add(1)%0A%0A%0A%20%20%20%20class%20StatsDf(FinalDF)%3A%0A%20%20%20%20%20%20%20%20log_close%20%3D%20dy.Float32(nullable%3DTrue)%0A%20%20%20%20%20%20%20%20pct_change%20%3D%20dy.Float32(nullable%3DTrue)%0A%0A%0A%20%20%20%20def%20get_stats(df%3A%20pl.DataFrame)%20-%3E%20pl.DataFrame%3A%0A%20%20%20%20%20%20%20%20return%20df.with_columns(%0A%20%20%20%20%20%20%20%20%20%20%20%20StatsDf.close.col.pipe(normalize_log)%0A%20%20%20%20%20%20%20%20%20%20%20%20.over(StatsDf.ticker.col)%0A%20%20%20%20%20%20%20%20%20%20%20%20.alias(StatsDf.log_close.name)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20StatsDf.close.col.pct_change()%0A%20%20%20%20%20%20%20%20%20%20%20%20.over(StatsDf.ticker.col)%0A%20%20%20%20%20%20%20%20%20%20%20%20.alias(StatsDf.pct_change.name)%2C%0A%20%20%20%20%20%20%20%20)%0A%0A%0A%20%20%20%20df_stats%20%3D%20get_stats(final_df)%0A%20%20%20%20return%20StatsDf%2C%20df_stats%2C%20px%0A%0A%0A%40app.cell%0Adef%20_(StatsDf%2C%20df_stats%2C%20px)%3A%0A%20%20%20%20px.line(%0A%20%20%20%20%20%20%20%20df_stats%2C%0A%20%20%20%20%20%20%20%20x%3DStatsDf.date.name%2C%0A%20%20%20%20%20%20%20%20y%3DStatsDf.log_close.name%2C%0A%20%20%20%20%20%20%20%20color%3DStatsDf.ticker.name%2C%0A%20%20%20%20%20%20%20%20title%3D%22Log%20Close%20Price%20by%20Ticker%22%2C%0A%20%20%20%20%20%20%20%20template%3D%22plotly_dark%22%2C%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(StatsDf%2C%20df_stats%2C%20px)%3A%0A%20%20%20%20px.box(%0A%20%20%20%20%20%20%20%20df_stats%2C%0A%20%20%20%20%20%20%20%20y%3DStatsDf.pct_change.name%2C%0A%20%20%20%20%20%20%20%20x%3DStatsDf.ticker.name%2C%0A%20%20%20%20%20%20%20%20color%3DStatsDf.ticker.name%2C%0A%20%20%20%20%20%20%20%20title%3D%22Percentage%20Change%20Distribution%20by%20Ticker%22%2C%0A%20%20%20%20%20%20%20%20template%3D%22plotly_dark%22%2C%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20marimo%20as%20mo%0A%20%20%20%20return%20(mo%2C)%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20app.run()%0A
9a47908caf243a117cb6029be299bb732c639912ad9d0d45c98b1a62e72230ba