From c1f041769dae16b287cc7823c6024ef698c8abcf Mon Sep 17 00:00:00 2001 From: jsboige Date: Wed, 7 Oct 2026 02:04:16 +0200 Subject: [PATCH 1/4] feat(qc,#18962): Chronos re-entraine -- portage 06/18/02 + mesure hors echantillon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See #18962 (livraison partielle : volet Chronos ; le volet FinBERT depend de #18903). Le re-entrainement de Chronos n'existait pas dans le depot (GAP de BOOK_MAPPING.md, ligne 18/02). Il est porte et mesure : - main_finetuned.py : port de 06/18/02 (HandsOnAITradingBook @ e025f21). Une seule difference fonctionnelle avec le livre, verifiee par diff du corps des methodes : set_seed(1) devient set_seed(self._seed), avec un defaut de 1 -- la valeur que le livre fige. Non executable en CI (GPU), dit dans l'en-tete du fichier. - finetune/run_finetune_chronos.py : harnais base vs re-entraine, hors echantillon (2022-01-01 -> 2026-01-01), 19 origines, appariement strict (meme graine de tirage pour les deux bras a chaque origine). - finetune/chronos_training.py : module d'entrainement de chronos, vendorise depuis le tag v2.3.2 (Apache-2.0, en-tete de provenance). Motif : le livre importe chronos.scripts.training.train, absent du wheel PyPI et du tag -- sans cette copie le port ne tourne nulle part. - finetune/measures/ : les resultats des quatre graines et l'agregat (49 Ko). measures/ et non results/, que l'arbre QuantConnect ignore. Verdict de precision : NO BEATS. Le re-entraine est plus precis sur les quatre graines (MAE moyenne 18,43 -> 17,40), mais aucune n'atteint le seuil de 5 % (p de 0,093 a 0,409). L'effet strategie est instable : le signe du Sharpe change selon la graine (0,639 -> 0,694 en moyenne, pire baisse -29,1 % -> -33,5 %). Aucun gain n'est revendique. L'univers (AAPL, MSFT, NVDA, AMZN, GOOGL) est un sous-ensemble de Mag7 : le protocole du depot interdit d'y revendiquer un BEATS, et aucun n'est revendique. Corrections attenantes : les deux README renvoyaient a research.ipynb, fichier absent de main (git ls-tree) ; le renvoi mort est retire. BOOK_MAPPING.md : ligne 18/02 du chapitre 06 et table « Exemples sans reproduction ». Co-Authored-By: Claude Sonnet 5.5 --- .../QuantConnect/BOOK_MAPPING.md | 4 +- .../ML-Chronos-Foundation/README.en.md | 100 +- .../projects/ML-Chronos-Foundation/README.md | 100 +- .../finetune/chronos_training.py | 709 +++++++++++ .../finetune/measures/seed-1.json | 273 ++++ .../finetune/measures/seed-2.json | 273 ++++ .../finetune/measures/seed-3.json | 273 ++++ .../finetune/measures/seed-42.json | 273 ++++ .../finetune/measures/summary.json | 1119 +++++++++++++++++ .../finetune/run_finetune_chronos.py | 352 ++++++ .../ML-Chronos-Foundation/main_finetuned.py | 372 ++++++ 11 files changed, 3843 insertions(+), 5 deletions(-) create mode 100644 MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/chronos_training.py create mode 100644 MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/measures/seed-1.json create mode 100644 MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/measures/seed-2.json create mode 100644 MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/measures/seed-3.json create mode 100644 MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/measures/seed-42.json create mode 100644 MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/measures/summary.json create mode 100644 MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/run_finetune_chronos.py create mode 100644 MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/main_finetuned.py diff --git a/MyIA.AI.Notebooks/QuantConnect/BOOK_MAPPING.md b/MyIA.AI.Notebooks/QuantConnect/BOOK_MAPPING.md index 0ac524bb4e..ba430835e2 100644 --- a/MyIA.AI.Notebooks/QuantConnect/BOOK_MAPPING.md +++ b/MyIA.AI.Notebooks/QuantConnect/BOOK_MAPPING.md @@ -113,7 +113,7 @@ Algorithmes LEAN complets sur données de marché. Les exemples 04, 08, 18 et 19 | 16 | LLM Summarization of Tiingo News Articles | [ML-LLM-Summarization](projects/ML-LLM-Summarization/), [QC-Py-26-LLM-Trading-Signals](Python/QC-Py-26-LLM-Trading-Signals.ipynb) | PARTIAL | Needs-improvement (tranche 12) | sans clé d'API en paramètre du projet, `ML-LLM-Summarization` classe les articles par mots-clés, sans LLM, et l'actif est SPY, pas TSLA ; le notebook enseigne l'analyse de sentiment par LLM (réponses simulées dans ses sorties), sans résumé d'articles | | 17 | Head Shoulders Pattern Matching with CNN | [ML-HeadShoulders-CNN](projects/ML-HeadShoulders-CNN/) | COVERED | Vivant | | | 18/01 | Amazon Chronos Model — Base Model | [ML-Chronos-Foundation](projects/ML-Chronos-Foundation/), [Chronos-Foundation-Forecasting](projects/Chronos-Foundation-Forecasting/) | COVERED | Needs-improvement (les deux, tranche 14) | | -| 18/02 | Amazon Chronos Model — Fine-Tuned Model | — | GAP | — | aucun ré-entraînement de Chronos dans le dépôt | +| 18/02 | Amazon Chronos Model — Fine-Tuned Model | [ML-Chronos-Foundation](projects/ML-Chronos-Foundation/) (`main_finetuned.py`, `finetune/`) | COVERED | Non déployé (le portage s'exécute hors QC) | ré-entraînement porté et mesuré hors échantillon sur quatre graines ; voir le verdict de la tranche 3 | | 19/01 | FinBERT Model — Base Model | [ML-FinBERT-Sentiment](projects/ML-FinBERT-Sentiment/), [QC-Py-Cloud-01-FinBERT-Sentiment](Python/QC-Py-Cloud-01-FinBERT-Sentiment.ipynb) | COVERED | Needs-improvement (tranche 12) | le portage ne produit pas de transaction sur QC Cloud : [#18903](https://github.com/jsboige/CoursIA/issues/18903) | | 19/02 | FinBERT Model — Fine-Tuned Model | — | GAP | — | aucun ré-entraînement de FinBERT dans le dépôt | @@ -303,7 +303,7 @@ Ces projets n'ont pas d'exemple correspondant dans le livre, mais illustrent des | 04/05, 04/18, 05/02, 05/15 | [#18957](https://github.com/jsboige/CoursIA/issues/18957) : écart interquartile, élimination récursive des variables, régression polynomiale, OPTICS (scripts courts sur données synthétiques, à porter dans QC-Py-18 à 20) | | 06/02 | [#18958](https://github.com/jsboige/CoursIA/issues/18958) : régimes par prétraitement de facteurs | | 06/08/01, 06/08/03 | [#18960](https://github.com/jsboige/CoursIA/issues/18960) : stop fixe de référence et couverture par put, dans [Stoploss-Volatility-ML](projects/Stoploss-Volatility-ML/) | -| 06/18/02, 06/19/02 | [#18962](https://github.com/jsboige/CoursIA/issues/18962) : ré-entraînement de Chronos et de FinBERT (calcul GPU) | +| 06/19/02 | [#18962](https://github.com/jsboige/CoursIA/issues/18962) : ré-entraînement de FinBERT (calcul GPU), bloqué par [#18903](https://github.com/jsboige/CoursIA/issues/18903). Le ré-entraînement de Chronos (06/18/02) est porté dans [ML-Chronos-Foundation](projects/ML-Chronos-Foundation/) | | 07/01 | [#18902](https://github.com/jsboige/CoursIA/issues/18902) | | 08/01 | exclusion : le livre appelle l'API payante PredictNow.ai ; le dépôt garde une optimisation sans service externe | | 08/02 | [#18901](https://github.com/jsboige/CoursIA/issues/18901) (méta-étiquetage sans l'API PredictNow.ai) | diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.en.md b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.en.md index 827b20ff13..e080a6419e 100644 --- a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.en.md +++ b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.en.md @@ -22,11 +22,107 @@ Amazon Chronos T5 foundation model for time series forecasting. Biweekly rebalan | Model | Chronos T5 (Amazon) | | Rebalance | Biweekly | +## Fine-Tuned Variant (Example 18/02) + +`main_finetuned.py` is a faithful port of the book (`06 Applied Machine Learning/18 +Amazon Chronos Model/02 Fine-Tuned Model/main.py`, `HandsOnAITradingBook` repository at +commit `e025f21`): retraining runs **inside the algorithm**, at the first quarterly +rebalance, then `ChronosPipeline.predict` supplies the forecast curves and SciPy the +max-Sharpe weights. It is **not CI-runnable** (GPU, `chronos` and `gluonts` required) and +is not deployed to QC Cloud. + +Retraining is therefore also carried **outside** the algorithm, by a harness (`finetune/`) +that makes it measurable on a local GPU: + +- `finetune/run_finetune_chronos.py` — retrains per seed, then compares the base and the + retrained model out of sample; +- `finetune/chronos_training.py` — the `chronos` training module, **vendored** from tag + `v2.3.2` (Apache-2.0). The book imports `chronos.scripts.training.train`, a module + **absent from the PyPI wheel** (the file lives at the upstream repository root): the + vendored copy carries its provenance header, and `main_finetuned.py` falls back to it + when the upstream import fails. + +### Protocol + +- **Universe**: `AAPL, MSFT, NVDA, AMZN, GOOGL` — a fixed basket of five mega-caps. +- **Training**: 2016-01-01 → 2021-12-31. **Out of sample**: 2022-01-01 → 2026-01-01. +- Book recipe: `context_length` 126 days, `prediction_length` 63 days, `learning_rate` + 1e-5, `adamw_torch_fused`, batch 32, accumulation 2, `tf32` (Ampere), 20 forecast + samples per origin. +- **Error**: MAE, MASE (step-1 naive over the actual window) and WQL (quantiles + 0.1 / 0.25 / 0.5 / 0.75 / 0.9), averaged over the five series, per quarterly origin + (19 origins). +- **Significance**: Diebold-Mariano test paired by origin on the MAE loss. Quarterly + origins do not overlap: no Newey-West correction. The p-value is the normal + approximation one, liberal at 19 origins — the Harvey, Leybourne and Newbold correction + would only widen it. +- **Strategy effect**: the book's SLSQP weights (long-only, sum 1, maximising the Sharpe + of the forecast curves), quarterly rebalance, **5 basis points** of cost on turnover. + Both arms play **the same** origin, with the same draw seed. +- **Seeds**: 1, 2, 3 and 42. + +### Deviations from the book, assumed + +| Deviation | Reason | +|---|---| +| `max_steps` = 300 (the book: 3) | 3 steps retrain nothing; 3 is a cloud runtime budget, not a method choice | +| `torch_compile` = `False` | the book compiles for Linux/Ampere; `torch.compile` is not portable on this workstation | +| One retraining per seed over 2016-2021, evaluated out of sample | the book retrains every quarter; the harness isolates the retraining effect on a fixed window, which makes the two arms comparable | +| Fixed five mega-cap universe | the book selects the five most liquid tickers by dollar volume, unavailable outside QC. **This universe is Mag7**: no gain claim is made here (see the verdict) | +| `yfinance` data (adjusted closes) | the book reads LEAN engine history; the harness runs outside QC | + +### Out-of-sample result + +| Seed | MAE base | MAE retrained | MASE base | MASE retrained | DM (statistic) | DM (p) | Sharpe base | Sharpe retrained | +|---|---|---|---|---|---|---|---|---| +| 1 | 18.40 | 17.61 | 6.24 | 5.99 | 0.83 | 0.409 | 0.746 | 0.603 | +| 2 | 17.78 | 16.87 | 6.03 | 5.69 | 1.43 | 0.153 | 0.664 | 0.751 | +| 3 | 19.21 | 17.82 | 6.50 | 6.02 | 1.68 | 0.093 | 0.588 | 0.746 | +| 42 | 18.31 | 17.30 | 6.27 | 5.83 | 1.14 | 0.256 | 0.559 | 0.675 | +| **mean** | **18.43** | **17.40** | **6.26** | **5.88** | — | — | **0.639** | **0.694** | + +Mean WQL: 0.0745 → 0.0732. Mean CAGR: 17.5% → 18.0%. Mean max drawdown: +−29.1% → −33.5%. The test's `mean_diff` field is signed `base − retrained`: a positive +value means the **base** model has the larger error. + +**Precision — `NO BEATS`.** The retrained model is more accurate on **all four seeds** +(mean MAE 18.43 → 17.40, about −5.5%), but **no** seed reaches the 5% threshold (p from +0.093 to 0.409). The effect is therefore stable in sign and too small to be told apart +from noise at this sample size (19 origins × 5 series). + +**Strategy — unstable effect, no gain claimed.** Mean Sharpe rises (0.639 → 0.694) and so +does CAGR, but the sign **changes with the seed** (one seed in four sees the retrained +model fall back) and the worst drawdown deepens on average. + +**What these numbers are not.** They do not compare to the 0.277 Sharpe published for +example 18/01: that one comes from a LEAN backtest over 2015-2026, whereas the +measurement above comes from a harness outside QC, over another window and another +universe. + +**The disagreement between the two error measurements is worth stating.** The training +loss does fall over the steps (mean 4.83 over the 300 steps of the last seed, 4.66 at the +last step), without carrying over out of sample. That is the expected behaviour of a fit +to the training window, for a model of this size (`chronos-t5-tiny`) and 300 steps. + +### Reproducing the measurement + +```bash +# local GPU; the measurement does not need QC Cloud +python finetune/run_finetune_chronos.py --stage all --seeds 1 2 3 42 \ + --max-steps 300 --run-dir +``` + +The harness writes one file per seed into `/results/`, plus a +`summary.json` aggregating the seeds. The committed copies live under +`finetune/measures/`: the QuantConnect tree ignores `results/`, and `measures/` is the +repository convention for measurements kept in git. + ## Files - main.py - Strategy (v1.0, Chronos forecasting) -- research.ipynb - Foundation model evaluation +- main_finetuned.py - Fine-tuned variant (example 18/02, not CI-runnable) +- finetune/ - Out-of-sample measurement harness (local, outside QC Cloud) + ## References - Hands-On AI Trading, Section 06, Example 18 - diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.md b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.md index eb44cd486c..0ee541c31c 100644 --- a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.md +++ b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.md @@ -22,10 +22,108 @@ Modèle de fondation Chronos T5 d'Amazon pour le forecasting de séries temporel | Modèle | Chronos T5 (Amazon) | | Rebalance | Biweekly | +## Variante ré-entraînée (exemple 18/02) + +`main_finetuned.py` est le port fidèle du livre (section `06 Applied Machine Learning/18 +Amazon Chronos Model/02 Fine-Tuned Model/main.py`, dépôt `HandsOnAITradingBook` au commit +`e025f21`) : le ré-entraînement tourne **dans l'algorithme**, au premier rebalancement +trimestriel, puis `ChronosPipeline.predict` fournit les courbes de prévision et SciPy les +poids de Sharpe maximal. Il n'est **pas exécutable en CI** (GPU, `chronos` et `gluonts` +requis) et n'est pas déployé sur QC Cloud. + +Le ré-entraînement est donc aussi porté **hors** de l'algorithme, par un harnais +(`finetune/`) qui le rend mesurable sur un GPU local : + +- `finetune/run_finetune_chronos.py` — ré-entraîne par graine, puis compare le modèle de + base et le modèle ré-entraîné hors échantillon ; +- `finetune/chronos_training.py` — le module d'entraînement de `chronos`, **vendorisé** + depuis le tag `v2.3.2` (Apache-2.0). Le livre importe + `chronos.scripts.training.train`, module **absent du wheel PyPI** (le fichier vit à la + racine du dépôt amont) : la copie vendorisée porte son en-tête de provenance, et + `main_finetuned.py` retombe dessus quand l'import amont échoue. + +### Protocole + +- **Univers** : `AAPL, MSFT, NVDA, AMZN, GOOGL` — panier fixe de cinq + méga-capitalisations. +- **Entraînement** : 2016-01-01 → 2021-12-31. **Hors échantillon** : 2022-01-01 → 2026-01-01. +- Recette du livre : `context_length` 126 jours, `prediction_length` 63 jours, + `learning_rate` 1e-5, `adamw_torch_fused`, lot 32, accumulation 2, `tf32` (Ampere), 20 + échantillons de prévision par origine. +- **Erreur** : MAE, MASE (naïf de pas 1 sur la fenêtre réelle) et WQL (quantiles + 0,1 / 0,25 / 0,5 / 0,75 / 0,9), moyenne des cinq séries, par origine trimestrielle + (19 origines). +- **Significativité** : test de Diebold-Mariano apparié par origine sur la perte MAE. Les + origines trimestrielles ne se chevauchent pas : aucune correction de Newey-West. La + p-value est celle de l'approximation normale, libérale à 19 origines — la correction de + Harvey, Leybourne et Newbold ne ferait que l'élargir. +- **Effet de stratégie** : poids SLSQP du livre (long-only, somme 1, maximisation du + Sharpe des courbes prévues), rebalancement trimestriel, **5 points de base** de frais sur + le turnover. Les deux bras jouent **la même** origine, avec la même graine de tirage. +- **Graines** : 1, 2, 3 et 42. + +### Écarts au livre, assumés + +| Écart | Motif | +|---|---| +| `max_steps` = 300 (le livre : 3) | 3 pas ne ré-entraînent rien ; 3 est un budget de temps d'exécution cloud, pas un choix de méthode | +| `torch_compile` = `False` | le livre compile pour Linux/Ampere ; `torch.compile` n'est pas portable sur ce poste | +| Un ré-entraînement par graine sur 2016-2021, évalué hors échantillon | le livre ré-entraîne à chaque trimestre ; le harnais isole l'effet du ré-entraînement sur une fenêtre fixe, ce qui rend les deux bras comparables | +| Univers fixe de cinq méga-capitalisations | le livre sélectionne les cinq titres les plus liquides au dollar-volume, indisponible hors QC. **Cet univers est Mag7** : aucune revendication de gain n'est portée ici (voir le verdict) | +| Données `yfinance` (clôtures ajustées) | le livre lit l'historique du moteur LEAN ; le harnais tourne hors QC | + +### Résultat hors échantillon + +| Graine | MAE base | MAE ré-entraîné | MASE base | MASE ré-entraîné | DM (statistique) | DM (p) | Sharpe base | Sharpe ré-entraîné | +|---|---|---|---|---|---|---|---|---| +| 1 | 18,40 | 17,61 | 6,24 | 5,99 | 0,83 | 0,409 | 0,746 | 0,603 | +| 2 | 17,78 | 16,87 | 6,03 | 5,69 | 1,43 | 0,153 | 0,664 | 0,751 | +| 3 | 19,21 | 17,82 | 6,50 | 6,02 | 1,68 | 0,093 | 0,588 | 0,746 | +| 42 | 18,31 | 17,30 | 6,27 | 5,83 | 1,14 | 0,256 | 0,559 | 0,675 | +| **moyenne** | **18,43** | **17,40** | **6,26** | **5,88** | — | — | **0,639** | **0,694** | + +WQL moyen : 0,0745 → 0,0732. CAGR moyen : 17,5 % → 18,0 %. Pire baisse moyenne : +−29,1 % → −33,5 %. Le champ `mean_diff` du test est signé `base − ré-entraîné` : une +valeur positive veut dire que le **modèle de base** a la plus grande erreur. + +**Précision — `NO BEATS`.** Le modèle ré-entraîné est plus précis sur **les quatre +graines** (MAE moyenne 18,43 → 17,40, soit environ −5,5 %), mais **aucune** graine +n'atteint le seuil de 5 % (p de 0,093 à 0,409). L'effet est donc stable dans son signe et +trop petit pour être distingué du bruit à cette taille d'échantillon (19 origines × 5 +séries). + +**Stratégie — effet instable, aucun gain revendiqué.** Le Sharpe moyen monte +(0,639 → 0,694) et le CAGR aussi, mais le signe **change selon la graine** (une graine sur +quatre voit le ré-entraîné reculer) et la pire baisse se creuse en moyenne. + +**Ce que ces chiffres ne sont pas.** Ils ne se comparent pas au Sharpe 0,277 publié pour +l'exemple 18/01 : celui-ci vient d'un backtest LEAN sur 2015-2026, quand la mesure +ci-dessus vient d'un harnais hors QC, sur une autre fenêtre et un autre univers. + +**Le désaccord entre les deux mesures d'erreur mérite d'être dit.** La perte +d'entraînement décroît bien au fil des pas (moyenne 4,83 sur les 300 pas de la dernière +graine, 4,66 au dernier pas), sans que cela se transporte hors échantillon. C'est le +comportement attendu d'un ajustement sur la fenêtre d'entraînement, pour un modèle de +cette taille (`chronos-t5-tiny`) et 300 pas. + +### Reproduire la mesure + +```bash +# GPU local ; la mesure n'a pas besoin de QC Cloud +python finetune/run_finetune_chronos.py --stage all --seeds 1 2 3 42 \ + --max-steps 300 --run-dir +``` + +Le harnais écrit un fichier par graine dans `/results/`, plus un +`summary.json` agrégeant les graines. Les copies committées vivent sous +`finetune/measures/` : l'arbre QuantConnect ignore `results/`, et `measures/` est la +convention du dépôt pour les mesures conservées. + ## Fichiers - main.py - Stratégie (v1.0, forecasting Chronos) -- research.ipynb - Évaluation du modèle de fondation +- main_finetuned.py - Variante ré-entraînée (exemple 18/02, non exécutable en CI) +- finetune/ - Harnais de mesure hors échantillon (local, hors QC Cloud) ## Références diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/chronos_training.py b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/chronos_training.py new file mode 100644 index 0000000000..8234f6be3c --- /dev/null +++ b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/chronos_training.py @@ -0,0 +1,709 @@ +# Vendorise depuis amazon-science/chronos-forecasting, tag v2.3.2 (Apache-2.0), +# fichier scripts/training/train.py -- le wheel PyPI n'embarque PAS ce +# module (il vit a la racine du depot), et le port 18/02 du livre +# (HandsOnAITradingBook e025f21) importe chronos.scripts.training.train. +# Verbatim, aucune modification fonctionnelle. Licence: Apache-2.0, +# copyright Amazon.com, Inc. or its affiliates. +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import ast +import logging +import os +import re +import sys +import json +import itertools +import random +from copy import deepcopy +from pathlib import Path +from functools import partial +from typing import List, Iterator, Optional, Dict + +import typer +from typer_config import use_yaml_config +import numpy as np +import torch +import torch.distributed as dist +from torch.utils.data import IterableDataset, get_worker_info +import transformers +from packaging import version +from transformers import ( + AutoModelForSeq2SeqLM, + AutoModelForCausalLM, + AutoConfig, + T5Config, + Trainer, + TrainingArguments, +) +import accelerate +import gluonts +from gluonts.dataset.common import FileDataset +from gluonts.itertools import Cyclic, Map, Filter +from gluonts.transform import ( + FilterTransformation, + TestSplitSampler, + ValidationSplitSampler, + InstanceSplitter, + ExpectedNumInstanceSampler, + MissingValueImputation, + LeavesMissingValues, + LastValueImputation, +) + +from chronos import ChronosConfig, ChronosTokenizer + +_TRANSFORMERS_V5 = version.parse(transformers.__version__) >= version.parse("5.0.0") + +app = typer.Typer(pretty_exceptions_enable=False) + + +def is_main_process() -> bool: + """ + Check if we're on the main process. + """ + if not dist.is_torchelastic_launched(): + return True + return int(os.environ["RANK"]) == 0 + + +def log_on_main(msg: str, logger: logging.Logger, log_level: int = logging.INFO): + """ + Log the given message using the given logger, if we're on the main process. + """ + if is_main_process(): + logger.log(log_level, msg) + + +def get_training_job_info() -> Dict: + """ + Returns info about this training job. + """ + job_info = {} + + # CUDA info + job_info["cuda_available"] = torch.cuda.is_available() + if torch.cuda.is_available(): + job_info["device_count"] = torch.cuda.device_count() + + job_info["device_names"] = { + idx: torch.cuda.get_device_name(idx) + for idx in range(torch.cuda.device_count()) + } + job_info["mem_info"] = { + idx: torch.cuda.mem_get_info(device=idx) + for idx in range(torch.cuda.device_count()) + } + + # DDP info + job_info["torchelastic_launched"] = dist.is_torchelastic_launched() + + if dist.is_torchelastic_launched(): + job_info["world_size"] = dist.get_world_size() + + # Versions + job_info["python_version"] = sys.version.replace("\n", " ") + job_info["torch_version"] = torch.__version__ + job_info["numpy_version"] = np.__version__ + job_info["gluonts_version"] = gluonts.__version__ + job_info["transformers_version"] = transformers.__version__ + job_info["accelerate_version"] = accelerate.__version__ + + return job_info + + +def save_training_info(ckpt_path: Path, training_config: Dict): + """ + Save info about this training job in a json file for documentation. + """ + assert ckpt_path.is_dir() + with open(ckpt_path / "training_info.json", "w") as fp: + json.dump( + {"training_config": training_config, "job_info": get_training_job_info()}, + fp, + indent=4, + ) + + +def get_next_path( + base_fname: str, + base_dir: Path, + file_type: str = "yaml", + separator: str = "-", +): + """ + Gets the next available path in a directory. For example, if `base_fname="results"` + and `base_dir` has files ["results-0.yaml", "results-1.yaml"], this function returns + "results-2.yaml". + """ + if file_type == "": + # Directory + items = filter( + lambda x: x.is_dir() and re.match(f"^{base_fname}{separator}\\d+$", x.stem), + base_dir.glob("*"), + ) + else: + # File + items = filter( + lambda x: re.match(f"^{base_fname}{separator}\\d+$", x.stem), + base_dir.glob(f"*.{file_type}"), + ) + run_nums = list( + map(lambda x: int(x.stem.replace(base_fname + separator, "")), items) + ) + [-1] + + next_num = max(run_nums) + 1 + fname = f"{base_fname}{separator}{next_num}" + ( + f".{file_type}" if file_type != "" else "" + ) + + return base_dir / fname + + +def load_model( + model_id="google/t5-efficient-tiny", + model_type="seq2seq", + vocab_size=4096, + random_init=False, + tie_embeddings=False, + pad_token_id=0, + eos_token_id=1, +): + """ + Load the specified HuggingFace model, adjusting the vocabulary + size, special token IDs, and initialization options. + + This allows to set a model up for training on a new vocabulary + of tokens. + """ + assert model_type in ["seq2seq", "causal"] + AutoModelClass = ( + AutoModelForSeq2SeqLM if model_type == "seq2seq" else AutoModelForCausalLM + ) + if random_init: + log_on_main("Using random initialization", logger) + config = AutoConfig.from_pretrained(model_id) + if isinstance(config, T5Config): + # The default initializer_factor (1.0) in transformers is too large + config.initializer_factor = 0.05 + config.tie_word_embeddings = tie_embeddings + model = AutoModelClass.from_config(config) + else: + log_on_main(f"Using pretrained initialization from {model_id}", logger) + model = AutoModelClass.from_pretrained(model_id) + + model.resize_token_embeddings(vocab_size) + + model.config.pad_token_id = model.generation_config.pad_token_id = pad_token_id + model.config.eos_token_id = model.generation_config.eos_token_id = eos_token_id + + return model + + +def has_enough_observations( + entry: dict, min_length: int = 0, max_missing_prop: float = 1.0 +) -> bool: + """ + Check if the given entry has enough observations in the ``"target"`` attribute. + + Parameters + ---------- + entry + The data entry (dictionary) to be tested. + min_length + The minimum length the ``"target"`` attribute must have. + max_missing_prop + The maximum proportion of missing data allowed in the ``"target"`` + attribute. + """ + if ( + len(entry["target"]) >= min_length + and np.isnan(entry["target"]).mean() <= max_missing_prop + ): + return True + return False + + +class PseudoShuffledIterableDataset(IterableDataset): + """ + Shuffle entries from an iterable by temporarily accumulating them + in an intermediate buffer. + + Parameters + ---------- + base_dataset + The original iterable object, representing the dataset. + shuffle_buffer_length + Size of the buffer use to shuffle entries from the base dataset. + """ + + def __init__(self, base_dataset, shuffle_buffer_length: int = 100) -> None: + super().__init__() + self.base_dataset = base_dataset + self.shuffle_buffer_length = shuffle_buffer_length + self.generator = torch.Generator() + + def __iter__(self): + shuffle_buffer = [] + + for element in self.base_dataset: + shuffle_buffer.append(element) + if len(shuffle_buffer) >= self.shuffle_buffer_length: + idx = torch.randint( + len(shuffle_buffer), size=(), generator=self.generator + ) + yield shuffle_buffer.pop(idx) + + while shuffle_buffer: + idx = torch.randint(len(shuffle_buffer), size=(), generator=self.generator) + yield shuffle_buffer.pop(idx) + + +class ShuffleMixin: + """ + Mix-in class that datasets can inherit from to get + shuffling functionality. + """ + + def shuffle(self, shuffle_buffer_length: int = 100): + return PseudoShuffledIterableDataset(self, shuffle_buffer_length) + + +class ChronosDataset(IterableDataset, ShuffleMixin): + """ + Dataset wrapper, using a ``ChronosTokenizer`` to turn data from a time series + into a HuggingFace-compatible set of ``input_ids``, ``attention_mask`` and + ``labels``. + + Entries from the original datasets are assumed to have a ``"start"`` attribute + (of type ``pd.Period``), and a ``"target"`` attribute (of type ``np.ndarray``). + + Parameters + ---------- + datasets + Datasets containing the original time series data. + probabilities + In training mode, data will be sampled from each of the original datasets + with these probabilities. + tokenizer + Tokenizer to be used to turn sequences of real numbers into token IDs. + context_length + Samples context will be limited to this length. + prediction_length + Samples labels will be limited to this length. + drop_prob + In training mode, observations from a sample will be turned into ``np.nan``, + i.e. turned into missing values, with this probability. + min_past + Data samples will be considered only if there's at least ``min_past``-many + historical observations. + mode + One of ``"training"``, ``"validation"``, or ``"test"``. + np_dtype + Numpy float data type. + """ + + def __init__( + self, + datasets: list, + probabilities: List[float], + tokenizer: ChronosTokenizer, + context_length: int = 512, + prediction_length: int = 64, + drop_prob: float = 0.2, + min_past: Optional[int] = None, + model_type: str = "seq2seq", + imputation_method: Optional[MissingValueImputation] = None, + mode: str = "training", + np_dtype=np.float32, + ) -> None: + super().__init__() + + assert len(probabilities) == len(datasets) + assert mode in ("training", "validation", "test") + assert model_type in ("seq2seq", "causal") + + self.datasets = datasets + self.probabilities = probabilities + self.tokenizer = tokenizer + self.context_length = context_length + self.prediction_length = prediction_length + self.drop_prob = drop_prob if model_type == "seq2seq" else 0.0 + self.min_past = min_past or prediction_length + self.model_type = model_type + self.imputation_method = imputation_method or LeavesMissingValues() + self.mode = mode + self.np_dtype = np_dtype + + def preprocess_entry(self, entry: dict, mode: str) -> dict: + entry = {f: entry[f] for f in ["start", "target"]} + entry["target"] = np.asarray(entry["target"], dtype=self.np_dtype) + assert entry["target"].ndim == 1, f"got {entry['target'].ndim=}, expected 1" + + if self.model_type == "causal": + # Causal models do not play nice with missing values, so it is + # recommended to use an imputation method, e.g., LastValueImputation + entry["target"] = self.imputation_method(entry["target"]) + + if mode == "training" and self.drop_prob > 0: + target = entry["target"].copy() + drop_p = np.random.uniform(low=0.0, high=self.drop_prob) + mask = np.random.choice( + [True, False], size=len(target), p=[drop_p, 1 - drop_p] + ) + target[mask] = np.nan + entry["target"] = target + + return entry + + def _create_instance_splitter(self, mode: str): + assert mode in ["training", "test", "validation"] + + instance_sampler = { + "training": ExpectedNumInstanceSampler( + num_instances=1.0, + min_instances=1, + min_past=self.min_past, + min_future=self.prediction_length, + ), + "test": TestSplitSampler(), + "validation": ValidationSplitSampler(min_future=self.prediction_length), + }[mode] + + return InstanceSplitter( + target_field="target", + is_pad_field="is_pad", + start_field="start", + forecast_start_field="forecast_start", + instance_sampler=instance_sampler, + past_length=self.context_length, + future_length=self.prediction_length, + dummy_value=np.nan, + ) + + def create_training_data(self, data): + data = Cyclic(data) + split_transform = self._create_instance_splitter( + "training" + ) + FilterTransformation( + condition=lambda entry: (~np.isnan(entry["past_target"])).sum() > 0 + ) + data = split_transform.apply(data, is_train=True) + return data + + def create_test_data(self, data): + data = self._create_instance_splitter("test").apply(data, is_train=False) + return data + + def create_validation_data(self, data): + data = self._create_instance_splitter("validation").apply(data, is_train=False) + return data + + def to_hf_format(self, entry: dict) -> dict: + past_target = torch.tensor(entry["past_target"]).unsqueeze(0) + input_ids, attention_mask, scale = self.tokenizer.context_input_transform( + past_target + ) + future_target = torch.tensor(entry["future_target"]).unsqueeze(0) + labels, labels_mask = self.tokenizer.label_input_transform(future_target, scale) + labels[labels_mask == 0] = -100 + + if self.model_type == "causal": + # The InstanceSplitter pads time series on the left to be equal to the + # context_length. However, certain models (e.g., GPT2) with absolute + # position embeddings should not be trained with left padding. + # The following piece of code moves padding from left to right. + + assert input_ids.shape[-1] == entry["past_is_pad"].shape[0] + + # Find the index where padding starts + pad_start_idx = np.searchsorted(1 - entry["past_is_pad"], 1) + padded_input_ids, obs_input_ids = torch.tensor_split( + input_ids, [pad_start_idx], dim=-1 + ) + padded_attention_mask, obs_attention_mask = torch.tensor_split( + attention_mask, [pad_start_idx], dim=-1 + ) + + # Move padding to the right + input_ids = torch.cat( + [ + obs_input_ids, + labels, + padded_input_ids, + ], + axis=-1, + ) + attention_mask = torch.cat( + [ + obs_attention_mask, + labels_mask, + padded_attention_mask, + ], + axis=-1, + ) + + # labels for causal models are same as the input_ids. + # Internally transformers shifts the labels by one during training. + labels = input_ids.clone() + input_ids[~attention_mask] = self.tokenizer.config.pad_token_id + labels[~attention_mask] = -100 + + return { + "input_ids": input_ids.squeeze(0), + "attention_mask": attention_mask.squeeze(0), + "labels": labels.squeeze(0), + } + + def __iter__(self) -> Iterator: + preprocessed_datasets = [ + Map( + partial(self.preprocess_entry, mode=self.mode), + dataset, + ) + for dataset in self.datasets + ] + + if self.mode == "training": + iterables = [ + self.create_training_data(dataset) for dataset in preprocessed_datasets + ] + elif self.mode == "test": + iterables = [ + self.create_test_data(dataset) for dataset in preprocessed_datasets + ] + else: + iterables = [ + self.create_validation_data(dataset) + for dataset in preprocessed_datasets + ] + + worker_info = get_worker_info() + if worker_info is None: + probs = list(self.probabilities) + else: + worker_id = worker_info.id + num_workers = worker_info.num_workers + iterables = list(itertools.islice(iterables, worker_id, None, num_workers)) + probs = list( + itertools.islice(self.probabilities, worker_id, None, num_workers) + ) + + probs = [prob / sum(probs) for prob in probs] + + iterators = list(map(iter, iterables)) + if self.mode == "training": + while True: + idx = np.random.choice(range(len(iterators)), p=probs) + try: + yield self.to_hf_format(next(iterators[idx])) + except StopIteration: + probs[idx] = 0 + if sum(probs) == 0: + return + probs = [prob / sum(probs) for prob in probs] + else: + for entry in itertools.chain(*iterators): + yield self.to_hf_format(entry) + + +@app.command() +@use_yaml_config(param_name="config") +def main( + training_data_paths: str, + probability: Optional[str] = None, + context_length: int = 512, + prediction_length: int = 64, + min_past: int = 64, + max_steps: int = 200_000, + save_steps: int = 50_000, + log_steps: int = 500, + per_device_train_batch_size: int = 32, + learning_rate: float = 1e-3, + optim: str = "adamw_torch_fused", + shuffle_buffer_length: int = 100, + gradient_accumulation_steps: int = 2, + model_id: str = "google/t5-efficient-tiny", + model_type: str = "seq2seq", + random_init: bool = False, + tie_embeddings: bool = False, + output_dir: str = "./output/", + tf32: bool = True, + torch_compile: bool = True, + tokenizer_class: str = "MeanScaleUniformBins", + tokenizer_kwargs: str = "{'low_limit': -15.0, 'high_limit': 15.0}", + n_tokens: int = 4096, + n_special_tokens: int = 2, + pad_token_id: int = 0, + eos_token_id: int = 1, + use_eos_token: bool = True, + lr_scheduler_type: str = "linear", + warmup_ratio: float = 0.0, + dataloader_num_workers: int = 1, + max_missing_prop: float = 0.9, + num_samples: int = 20, + temperature: float = 1.0, + top_k: int = 50, + top_p: float = 1.0, + seed: Optional[int] = None, +): + if tf32 and not ( + torch.cuda.is_available() and torch.cuda.get_device_capability()[0] >= 8 + ): + # TF32 floating point format is available only on NVIDIA GPUs + # with compute capability 8 and above. See link for details. + # https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#compute-capability-8-x + log_on_main( + "TF32 format is only available on devices with compute capability >= 8. " + "Setting tf32 to False.", + logger, + ) + tf32 = False + + if seed is None: + seed = random.randint(0, 2**32) + + log_on_main(f"Using SEED: {seed}", logger) + transformers.set_seed(seed=seed) + + raw_training_config = deepcopy(locals()) + output_dir = Path(output_dir) + training_data_paths = ast.literal_eval(training_data_paths) + assert isinstance(training_data_paths, list) + + if isinstance(probability, str): + probability = ast.literal_eval(probability) + elif probability is None: + probability = [1.0 / len(training_data_paths)] * len(training_data_paths) + assert isinstance(probability, list) + + assert len(training_data_paths) == len(probability) + + if dataloader_num_workers > len(training_data_paths): + log_on_main( + f"Setting the number of data loader workers to {len(training_data_paths)}, " + f"instead of {dataloader_num_workers}.", + logger, + ) + dataloader_num_workers = len(training_data_paths) + + if isinstance(tokenizer_kwargs, str): + tokenizer_kwargs = ast.literal_eval(tokenizer_kwargs) + assert isinstance(tokenizer_kwargs, dict) + + assert model_type in ["seq2seq", "causal"] + + output_dir = get_next_path("run", base_dir=output_dir, file_type="") + + log_on_main(f"Logging dir: {output_dir}", logger) + log_on_main( + f"Loading and filtering {len(training_data_paths)} datasets " + f"for training: {training_data_paths}", + logger, + ) + + log_on_main( + f"Mixing probabilities: {probability}", + logger, + ) + + train_datasets = [ + Filter( + partial( + has_enough_observations, + min_length=min_past + prediction_length, + max_missing_prop=max_missing_prop, + ), + FileDataset(path=Path(data_path), freq="h"), + ) + for data_path in training_data_paths + ] + + log_on_main("Initializing model", logger) + + model = load_model( + model_id=model_id, + model_type=model_type, + vocab_size=n_tokens, + random_init=random_init, + tie_embeddings=tie_embeddings, + pad_token_id=pad_token_id, + eos_token_id=eos_token_id, + ) + + chronos_config = ChronosConfig( + tokenizer_class=tokenizer_class, + tokenizer_kwargs=tokenizer_kwargs, + n_tokens=n_tokens, + n_special_tokens=n_special_tokens, + pad_token_id=pad_token_id, + eos_token_id=eos_token_id, + use_eos_token=use_eos_token, + model_type=model_type, + context_length=context_length, + prediction_length=prediction_length, + num_samples=num_samples, + temperature=temperature, + top_k=top_k, + top_p=top_p, + ) + + # Add extra items to model config so that it's saved in the ckpt + model.config.chronos_config = chronos_config.__dict__ + + shuffled_train_dataset = ChronosDataset( + datasets=train_datasets, + probabilities=probability, + tokenizer=chronos_config.create_tokenizer(), + context_length=context_length, + prediction_length=prediction_length, + min_past=min_past, + model_type=model_type, + imputation_method=LastValueImputation() if model_type == "causal" else None, + mode="training", + ).shuffle(shuffle_buffer_length=shuffle_buffer_length) + + # Define training args + training_args = TrainingArguments( + output_dir=str(output_dir), + per_device_train_batch_size=per_device_train_batch_size, + learning_rate=learning_rate, + lr_scheduler_type=lr_scheduler_type, + **({"warmup_steps": round(warmup_ratio * max_steps)} if _TRANSFORMERS_V5 else {"warmup_ratio": warmup_ratio}), + optim=optim, + logging_strategy="steps", + logging_steps=log_steps, + save_strategy="steps", + save_steps=save_steps, + report_to=["tensorboard"], + max_steps=max_steps, + gradient_accumulation_steps=gradient_accumulation_steps, + dataloader_num_workers=dataloader_num_workers, + tf32=tf32, # remove this if not using Ampere GPUs (e.g., A100) + torch_compile=torch_compile, + ddp_find_unused_parameters=False, + remove_unused_columns=False, + ) + + # Create Trainer instance + trainer = Trainer( + model=model, + args=training_args, + train_dataset=shuffled_train_dataset, + ) + log_on_main("Training", logger) + + trainer.train() + + if is_main_process(): + model.save_pretrained(output_dir / "checkpoint-final") + save_training_info( + output_dir / "checkpoint-final", training_config=raw_training_config + ) + + +if __name__ == "__main__": + logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") + logger = logging.getLogger(__file__) + logger.setLevel(logging.INFO) + app() diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/measures/seed-1.json b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/measures/seed-1.json new file mode 100644 index 0000000000..61b4835e2d --- /dev/null +++ b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/measures/seed-1.json @@ -0,0 +1,273 @@ +{ + "per_origin": [ + { + "origin": "2022-01-03", + "base": { + "mae": 19.112240885174465, + "mase": 6.715310179770936, + "wql": 0.09924832974802081 + }, + "finetuned": { + "mae": 20.493394736638145, + "mase": 7.326273040439956, + "wql": 0.1082552414214325 + } + }, + { + "origin": "2022-04-01", + "base": { + "mae": 27.043341430785166, + "mase": 9.06461132645371, + "wql": 0.18836133795248183 + }, + "finetuned": { + "mae": 33.06470503125872, + "mase": 11.342710261402814, + "wql": 0.2637204596395267 + } + }, + { + "origin": "2022-07-01", + "base": { + "mae": 11.951092798747712, + "mase": 5.432116571175683, + "wql": 0.07046427566274908 + }, + "finetuned": { + "mae": 11.199436796279182, + "mase": 5.483097114827757, + "wql": 0.07158508346999515 + } + }, + { + "origin": "2022-10-03", + "base": { + "mae": 12.252073069981165, + "mase": 5.2359021067876315, + "wql": 0.08582680810405671 + }, + "finetuned": { + "mae": 12.781836337134951, + "mase": 5.537484910061301, + "wql": 0.09204240183338079 + } + }, + { + "origin": "2023-01-03", + "base": { + "mae": 10.09796972426157, + "mase": 6.406238714293873, + "wql": 0.0927650540302959 + }, + "finetuned": { + "mae": 7.825651020473903, + "mase": 5.269053476930168, + "wql": 0.06989706890940843 + } + }, + { + "origin": "2023-04-03", + "base": { + "mae": 14.975024613879976, + "mase": 8.079705452724259, + "wql": 0.0764614260218213 + }, + "finetuned": { + "mae": 10.218651938059972, + "mase": 5.884669916278058, + "wql": 0.06756602456747748 + } + }, + { + "origin": "2023-07-03", + "base": { + "mae": 10.35564916701544, + "mase": 5.202818064252516, + "wql": 0.04306302185560437 + }, + "finetuned": { + "mae": 10.58020054650685, + "mase": 4.992265651478332, + "wql": 0.03979578819364563 + } + }, + { + "origin": "2023-10-02", + "base": { + "mae": 12.099410320463635, + "mase": 5.887076163057914, + "wql": 0.04678358175478743 + }, + "finetuned": { + "mae": 8.894339183020211, + "mase": 4.2498191685318645, + "wql": 0.034558537238122286 + } + }, + { + "origin": "2024-01-02", + "base": { + "mae": 15.044763619559154, + "mase": 7.821216948087307, + "wql": 0.09788473543274959 + }, + "finetuned": { + "mae": 15.283677733890595, + "mase": 7.918241486041909, + "wql": 0.09391008133007414 + } + }, + { + "origin": "2024-04-01", + "base": { + "mae": 12.93625270298549, + "mase": 5.130334760564909, + "wql": 0.05380064966230843 + }, + "finetuned": { + "mae": 11.925704810732887, + "mase": 4.865963458537815, + "wql": 0.052605293775461824 + } + }, + { + "origin": "2024-07-01", + "base": { + "mae": 19.129212757897754, + "mase": 6.653539599583444, + "wql": 0.07124206257952674 + }, + "finetuned": { + "mae": 19.85181548103454, + "mase": 6.956537426617999, + "wql": 0.06953813768270228 + } + }, + { + "origin": "2024-10-01", + "base": { + "mae": 13.956217593238467, + "mase": 5.173618148239184, + "wql": 0.0546707949386418 + }, + "finetuned": { + "mae": 13.289636182028147, + "mase": 4.814990531501581, + "wql": 0.04666304004655715 + } + }, + { + "origin": "2025-01-02", + "base": { + "mae": 19.047051469106524, + "mase": 5.21132946204933, + "wql": 0.061964934565550425 + }, + "finetuned": { + "mae": 20.692156933981273, + "mase": 5.7089908081309, + "wql": 0.07149593014706293 + } + }, + { + "origin": "2025-04-01", + "base": { + "mae": 23.59232713003007, + "mase": 5.962642861827978, + "wql": 0.08171056568355033 + }, + "finetuned": { + "mae": 19.232701498364644, + "mase": 4.823220904333876, + "wql": 0.06594535220200567 + } + }, + { + "origin": "2025-07-01", + "base": { + "mae": 17.38633786156064, + "mase": 6.9582471426332955, + "wql": 0.06279104751569271 + }, + "finetuned": { + "mae": 13.667767624627976, + "mase": 5.377368618436665, + "wql": 0.0439684938499635 + } + }, + { + "origin": "2025-10-01", + "base": { + "mae": 16.622267998589408, + "mase": 4.295474368728061, + "wql": 0.045797900931628846 + }, + "finetuned": { + "mae": 22.225901624891492, + "mase": 5.863159238072177, + "wql": 0.05156435069471923 + } + }, + { + "origin": "2026-01-02", + "base": { + "mae": 24.067667933872762, + "mase": 5.480242706218102, + "wql": 0.06211389207195315 + }, + "finetuned": { + "mae": 29.348118421766497, + "mase": 6.65787568602988, + "wql": 0.07150832383322832 + } + }, + { + "origin": "2026-04-01", + "base": { + "mae": 40.83081970214844, + "mase": 8.662998868974928, + "wql": 0.10507715182427524 + }, + "finetuned": { + "mae": 29.669935293046258, + "mase": 6.304983383328478, + "wql": 0.07606022818210197 + } + }, + { + "origin": "2026-07-01", + "base": { + "mae": 29.062042526971727, + "mase": 5.204465898019073, + "wql": 0.06227272534086028 + }, + "finetuned": { + "mae": 24.406141347733758, + "mase": 4.511598129619542, + "wql": 0.052585148665675444 + } + } + ], + "dm": { + "mean_diff": 0.7847363560420826, + "dm_stat": 0.8262043599297081, + "dm_p": 0.40868817543074304, + "n": 19 + }, + "strategy": { + "base": { + "sharpe": 0.745783066608768, + "cagr": 0.21691535915102955, + "max_dd": -0.2696805452808133, + "final_equity": 2.540895297857738, + "n_rebalances": 19 + }, + "finetuned": { + "sharpe": 0.6027453160254682, + "cagr": 0.14300055636712705, + "max_dd": -0.3392080485926423, + "final_equity": 1.8867771086199885, + "n_rebalances": 19 + } + } +} \ No newline at end of file diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/measures/seed-2.json b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/measures/seed-2.json new file mode 100644 index 0000000000..ecf16554be --- /dev/null +++ b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/measures/seed-2.json @@ -0,0 +1,273 @@ +{ + "per_origin": [ + { + "origin": "2022-01-03", + "base": { + "mae": 19.240389554462734, + "mase": 7.023998112107421, + "wql": 0.09894969938376562 + }, + "finetuned": { + "mae": 16.591725964016387, + "mase": 6.289248166278516, + "wql": 0.09644956003610414 + } + }, + { + "origin": "2022-04-01", + "base": { + "mae": 25.575135515606593, + "mase": 8.880534424505063, + "wql": 0.17040201080994988 + }, + "finetuned": { + "mae": 29.848317606486972, + "mase": 10.572572541237077, + "wql": 0.231936547396064 + } + }, + { + "origin": "2022-07-01", + "base": { + "mae": 11.015293678404792, + "mase": 5.212918914509073, + "wql": 0.06413789780408859 + }, + "finetuned": { + "mae": 10.086685534885952, + "mase": 4.853558689481431, + "wql": 0.06194327627197342 + } + }, + { + "origin": "2022-10-03", + "base": { + "mae": 8.605987981765988, + "mase": 4.158369332402529, + "wql": 0.0688811345158282 + }, + "finetuned": { + "mae": 10.11735132913741, + "mase": 4.565337839263637, + "wql": 0.0801429799715745 + } + }, + { + "origin": "2023-01-03", + "base": { + "mae": 11.175294219122994, + "mase": 6.919649610065283, + "wql": 0.09761679194889952 + }, + "finetuned": { + "mae": 8.65040077633328, + "mase": 5.51351429126901, + "wql": 0.07957510821230626 + } + }, + { + "origin": "2023-04-03", + "base": { + "mae": 10.800493579440646, + "mase": 6.330310883782866, + "wql": 0.0705801312350444 + }, + "finetuned": { + "mae": 9.264314457726858, + "mase": 5.3625057321610825, + "wql": 0.055240575140493554 + } + }, + { + "origin": "2023-07-03", + "base": { + "mae": 9.005152747744607, + "mase": 4.683129228704184, + "wql": 0.04245093829148341 + }, + "finetuned": { + "mae": 10.235760049971324, + "mase": 4.963969165588479, + "wql": 0.042578702594097094 + } + }, + { + "origin": "2023-10-02", + "base": { + "mae": 12.404869442894347, + "mase": 5.808955280069314, + "wql": 0.04411252606167688 + }, + "finetuned": { + "mae": 9.91220709180075, + "mase": 4.821603713108225, + "wql": 0.03647561511059898 + } + }, + { + "origin": "2024-01-02", + "base": { + "mae": 14.103005545479911, + "mase": 7.166527587717094, + "wql": 0.09136786889803836 + }, + "finetuned": { + "mae": 14.108838435581756, + "mase": 7.288597527835418, + "wql": 0.08501738803960976 + } + }, + { + "origin": "2024-04-01", + "base": { + "mae": 13.712336077008928, + "mase": 5.4225618029052916, + "wql": 0.052747260464580205 + }, + "finetuned": { + "mae": 11.544982377309646, + "mase": 4.493152408950246, + "wql": 0.041862125090910376 + } + }, + { + "origin": "2024-07-01", + "base": { + "mae": 18.325657992892797, + "mase": 6.304854215111592, + "wql": 0.06092091443955248 + }, + "finetuned": { + "mae": 18.064010111490887, + "mase": 6.232632112519619, + "wql": 0.06292170120626835 + } + }, + { + "origin": "2024-10-01", + "base": { + "mae": 13.215916491311692, + "mase": 4.850769066787696, + "wql": 0.04839476189385877 + }, + "finetuned": { + "mae": 11.898759484669517, + "mase": 4.294095976248818, + "wql": 0.041729056694459 + } + }, + { + "origin": "2025-01-02", + "base": { + "mae": 18.965100775824652, + "mase": 5.2330212784984, + "wql": 0.05943303924749856 + }, + "finetuned": { + "mae": 21.13191378154452, + "mase": 5.655052841700413, + "wql": 0.06259368087244278 + } + }, + { + "origin": "2025-04-01", + "base": { + "mae": 22.464964512416294, + "mase": 5.657843453030179, + "wql": 0.06907014679313236 + }, + "finetuned": { + "mae": 19.56221715533544, + "mase": 4.988403407206558, + "wql": 0.06114565082607025 + } + }, + { + "origin": "2025-07-01", + "base": { + "mae": 17.6908197796534, + "mase": 6.995424381565494, + "wql": 0.05693571332754854 + }, + "finetuned": { + "mae": 14.046976483057415, + "mase": 5.438470064331862, + "wql": 0.046728126095048506 + } + }, + { + "origin": "2025-10-01", + "base": { + "mae": 18.63142365955171, + "mase": 4.852930094325646, + "wql": 0.0412270541021487 + }, + "finetuned": { + "mae": 19.37632344563802, + "mase": 5.060783954088473, + "wql": 0.04295808673659006 + } + }, + { + "origin": "2026-01-02", + "base": { + "mae": 26.47312278142051, + "mase": 5.9423433373274674, + "wql": 0.06250326078623801 + }, + "finetuned": { + "mae": 30.139845203218005, + "mase": 6.762423515387857, + "wql": 0.07883904923042531 + } + }, + { + "origin": "2026-04-01", + "base": { + "mae": 38.492870609343996, + "mase": 8.110046005695054, + "wql": 0.10363441518732333 + }, + "finetuned": { + "mae": 31.133926197839163, + "mase": 6.449502023136193, + "wql": 0.08079452222494701 + } + }, + { + "origin": "2026-07-01", + "base": { + "mae": 27.998204355391245, + "mase": 5.008481696565779, + "wql": 0.055339928012947195 + }, + "finetuned": { + "mae": 24.792178150964162, + "mase": 4.519070245645079, + "wql": 0.046250700025709475 + } + } + ], + "dm": { + "mean_diff": 0.9152266138279145, + "dm_stat": 1.4289291192568372, + "dm_p": 0.1530246069878926, + "n": 19 + }, + "strategy": { + "base": { + "sharpe": 0.6642300946206589, + "cagr": 0.18811708844821018, + "max_dd": -0.287988405479613, + "final_equity": 2.2676780739147313, + "n_rebalances": 19 + }, + "finetuned": { + "sharpe": 0.7510316224582633, + "cagr": 0.20356756166906131, + "max_dd": -0.33740640670720123, + "final_equity": 2.411208425076607, + "n_rebalances": 19 + } + } +} \ No newline at end of file diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/measures/seed-3.json b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/measures/seed-3.json new file mode 100644 index 0000000000..3617a70004 --- /dev/null +++ b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/measures/seed-3.json @@ -0,0 +1,273 @@ +{ + "per_origin": [ + { + "origin": "2022-01-03", + "base": { + "mae": 19.187569742354135, + "mase": 6.504784341888853, + "wql": 0.09323511243167339 + }, + "finetuned": { + "mae": 19.07165127103291, + "mase": 6.627630351716027, + "wql": 0.09366091735788966 + } + }, + { + "origin": "2022-04-01", + "base": { + "mae": 29.577983895559157, + "mase": 10.015682203402648, + "wql": 0.1893461299995895 + }, + "finetuned": { + "mae": 33.590193524436344, + "mase": 11.757195071821455, + "wql": 0.2756116723705998 + } + }, + { + "origin": "2022-07-01", + "base": { + "mae": 12.300530227782236, + "mase": 5.700427777527996, + "wql": 0.07221312828803769 + }, + "finetuned": { + "mae": 12.218761210971408, + "mase": 5.611972682801342, + "wql": 0.06760616703358126 + } + }, + { + "origin": "2022-10-03", + "base": { + "mae": 9.827110814291334, + "mase": 4.470741149321305, + "wql": 0.06734046906188715 + }, + "finetuned": { + "mae": 11.765081956651477, + "mase": 5.432541436523687, + "wql": 0.08149074682559745 + } + }, + { + "origin": "2023-01-03", + "base": { + "mae": 11.400473479619102, + "mase": 7.173366788391671, + "wql": 0.1087881423946215 + }, + "finetuned": { + "mae": 6.993351482209704, + "mase": 4.782363520801956, + "wql": 0.07702990568691749 + } + }, + { + "origin": "2023-04-03", + "base": { + "mae": 13.419580326383079, + "mase": 7.427877862619812, + "wql": 0.0767118636068171 + }, + "finetuned": { + "mae": 10.988303611392066, + "mase": 6.1206767621236455, + "wql": 0.05539038532906495 + } + }, + { + "origin": "2023-07-03", + "base": { + "mae": 13.063607509552487, + "mase": 6.115218781016517, + "wql": 0.04594152613476126 + }, + "finetuned": { + "mae": 13.641395278204055, + "mase": 6.276273920998695, + "wql": 0.04486114382685417 + } + }, + { + "origin": "2023-10-02", + "base": { + "mae": 12.551746271527005, + "mase": 5.92763812459795, + "wql": 0.04430073847226629 + }, + "finetuned": { + "mae": 8.30003106253488, + "mase": 4.048028592285309, + "wql": 0.033792901952940296 + } + }, + { + "origin": "2024-01-02", + "base": { + "mae": 15.444296991257442, + "mase": 7.863943554026179, + "wql": 0.09702248718097463 + }, + "finetuned": { + "mae": 15.273911636594743, + "mase": 7.830120937236996, + "wql": 0.09215270770148329 + } + }, + { + "origin": "2024-04-01", + "base": { + "mae": 13.718541463216145, + "mase": 5.321502922133572, + "wql": 0.0508694740402833 + }, + "finetuned": { + "mae": 11.580626835898746, + "mase": 4.649825981576372, + "wql": 0.04722500880905197 + } + }, + { + "origin": "2024-07-01", + "base": { + "mae": 19.448245675223212, + "mase": 6.532271859424823, + "wql": 0.06693209405123593 + }, + "finetuned": { + "mae": 19.999737573048424, + "mase": 6.654714522818549, + "wql": 0.06749723162136774 + } + }, + { + "origin": "2024-10-01", + "base": { + "mae": 15.39874257890005, + "mase": 5.6190274840720145, + "wql": 0.06038869184178612 + }, + "finetuned": { + "mae": 12.028678191654263, + "mase": 4.331683606977913, + "wql": 0.041171675418957804 + } + }, + { + "origin": "2025-01-02", + "base": { + "mae": 21.080347551618303, + "mase": 5.988196995621358, + "wql": 0.06479205834998664 + }, + "finetuned": { + "mae": 21.584850250728543, + "mase": 6.030662965966997, + "wql": 0.0681672676787988 + } + }, + { + "origin": "2025-04-01", + "base": { + "mae": 21.177659655374196, + "mase": 5.461816801269606, + "wql": 0.06827351779370362 + }, + "finetuned": { + "mae": 20.19732380216084, + "mase": 5.274280184778867, + "wql": 0.07119972389902389 + } + }, + { + "origin": "2025-07-01", + "base": { + "mae": 20.285511028955852, + "mase": 7.870041714298314, + "wql": 0.06338324437980324 + }, + "finetuned": { + "mae": 16.48996102469308, + "mase": 6.00734955914148, + "wql": 0.048910138443719026 + } + }, + { + "origin": "2025-10-01", + "base": { + "mae": 21.08738282218812, + "mase": 5.7570886625098, + "wql": 0.04471851517598069 + }, + "finetuned": { + "mae": 19.158668348524305, + "mase": 4.883394467981636, + "wql": 0.0451098164835342 + } + }, + { + "origin": "2026-01-02", + "base": { + "mae": 26.05136592804439, + "mase": 5.847894724783748, + "wql": 0.06679324561301948 + }, + "finetuned": { + "mae": 31.95166388617622, + "mase": 7.2848445736793135, + "wql": 0.08245967582756629 + } + }, + { + "origin": "2026-04-01", + "base": { + "mae": 39.78389035179502, + "mase": 8.402605344375406, + "wql": 0.10051845021148631 + }, + "finetuned": { + "mae": 30.221296861436635, + "mase": 6.300297542399798, + "wql": 0.07086281536311552 + } + }, + { + "origin": "2026-07-01", + "base": { + "mae": 30.176719350663443, + "mase": 5.580190526132947, + "wql": 0.06204832074676777 + }, + "finetuned": { + "mae": 23.596383909195193, + "mase": 4.385238087469757, + "wql": 0.045945342769384126 + } + } + ], + "dm": { + "mean_diff": 1.385759681408467, + "dm_stat": 1.681447410457044, + "dm_p": 0.0926760431945916, + "n": 19 + }, + "strategy": { + "base": { + "sharpe": 0.5876952849794809, + "cagr": 0.1599171018872516, + "max_dd": -0.2790582806766164, + "final_equity": 2.0231497085857915, + "n_rebalances": 19 + }, + "finetuned": { + "sharpe": 0.7455377688999513, + "cagr": 0.19937912269961133, + "max_dd": -0.34095499626797776, + "final_equity": 2.3716101661182125, + "n_rebalances": 19 + } + } +} \ No newline at end of file diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/measures/seed-42.json b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/measures/seed-42.json new file mode 100644 index 0000000000..8cccbc04d1 --- /dev/null +++ b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/measures/seed-42.json @@ -0,0 +1,273 @@ +{ + "per_origin": [ + { + "origin": "2022-01-03", + "base": { + "mae": 17.77222561911931, + "mase": 6.37310320270795, + "wql": 0.09317199843236994 + }, + "finetuned": { + "mae": 17.48824496799045, + "mase": 6.259213135253562, + "wql": 0.09675759272817334 + } + }, + { + "origin": "2022-04-01", + "base": { + "mae": 24.79408077966599, + "mase": 8.484391968401898, + "wql": 0.16516753055397998 + }, + "finetuned": { + "mae": 30.4053437096732, + "mase": 10.698399002879507, + "wql": 0.24388368458614806 + } + }, + { + "origin": "2022-07-01", + "base": { + "mae": 12.817951253860713, + "mase": 5.875999756312781, + "wql": 0.07389239722008255 + }, + "finetuned": { + "mae": 11.53521071237231, + "mase": 5.606788221177423, + "wql": 0.07214014766434476 + } + }, + { + "origin": "2022-10-03", + "base": { + "mae": 9.649188078017463, + "mase": 4.435610356125639, + "wql": 0.07149681594586801 + }, + "finetuned": { + "mae": 9.996672672695585, + "mase": 4.535960081787418, + "wql": 0.07446640622360721 + } + }, + { + "origin": "2023-01-03", + "base": { + "mae": 12.38764711713034, + "mase": 7.785916207065794, + "wql": 0.11112482431220494 + }, + "finetuned": { + "mae": 8.242120700412325, + "mase": 5.379777017819521, + "wql": 0.07410529353446793 + } + }, + { + "origin": "2023-04-03", + "base": { + "mae": 13.93667439052037, + "mase": 7.864740807678162, + "wql": 0.07337398384906456 + }, + "finetuned": { + "mae": 10.751853839934817, + "mase": 5.914045029180344, + "wql": 0.056990499599649316 + } + }, + { + "origin": "2023-07-03", + "base": { + "mae": 9.829068659222315, + "mase": 4.930962567669164, + "wql": 0.04114367959132122 + }, + "finetuned": { + "mae": 10.772593180338541, + "mase": 5.022953446384486, + "wql": 0.043292278404674284 + } + }, + { + "origin": "2023-10-02", + "base": { + "mae": 14.54718453543527, + "mase": 6.5180222772685195, + "wql": 0.05014160627261666 + }, + "finetuned": { + "mae": 9.12182899353996, + "mase": 4.373883240482156, + "wql": 0.033347530515199876 + } + }, + { + "origin": "2024-01-02", + "base": { + "mae": 15.573319292825365, + "mase": 7.707004954035614, + "wql": 0.09415353258100762 + }, + "finetuned": { + "mae": 14.852715083530972, + "mase": 7.620316424738471, + "wql": 0.09234689792607934 + } + }, + { + "origin": "2024-04-01", + "base": { + "mae": 11.308997332860551, + "mase": 4.671071480702364, + "wql": 0.0481928057690567 + }, + "finetuned": { + "mae": 11.217902652801028, + "mase": 4.548331610881923, + "wql": 0.047989844674068236 + } + }, + { + "origin": "2024-07-01", + "base": { + "mae": 19.06674625457279, + "mase": 6.491409830465822, + "wql": 0.06661110729273978 + }, + "finetuned": { + "mae": 20.29634086972191, + "mase": 6.896829059742433, + "wql": 0.07078048766871961 + } + }, + { + "origin": "2024-10-01", + "base": { + "mae": 13.236307997930618, + "mase": 4.869891343511878, + "wql": 0.05150909767984644 + }, + "finetuned": { + "mae": 13.632048325311569, + "mase": 4.664383130771989, + "wql": 0.039256277537598604 + } + }, + { + "origin": "2025-01-02", + "base": { + "mae": 19.59426511734251, + "mase": 5.325298496824117, + "wql": 0.06021561361397641 + }, + "finetuned": { + "mae": 21.220824250720796, + "mase": 5.742002701115111, + "wql": 0.06655037139711725 + } + }, + { + "origin": "2025-04-01", + "base": { + "mae": 22.29641590421162, + "mase": 5.687334889991485, + "wql": 0.07251090537950837 + }, + "finetuned": { + "mae": 21.091515725756448, + "mase": 5.4138313361779655, + "wql": 0.06941840772564808 + } + }, + { + "origin": "2025-07-01", + "base": { + "mae": 18.723279971168154, + "mase": 7.563205118855348, + "wql": 0.06289960873006444 + }, + "finetuned": { + "mae": 15.317030746217757, + "mase": 6.010471878614314, + "wql": 0.048575624194647275 + } + }, + { + "origin": "2025-10-01", + "base": { + "mae": 18.76023288302951, + "mase": 5.339240969202885, + "wql": 0.042659501903981524 + }, + "finetuned": { + "mae": 20.631201462518597, + "mase": 5.471035346168572, + "wql": 0.04856228647626735 + } + }, + { + "origin": "2026-01-02", + "base": { + "mae": 24.027901834154882, + "mase": 5.21642452875602, + "wql": 0.061348772620823 + }, + "finetuned": { + "mae": 28.036732507130456, + "mase": 5.9906097563615495, + "wql": 0.07045652359108559 + } + }, + { + "origin": "2026-04-01", + "base": { + "mae": 40.68702809167287, + "mase": 8.770697206662716, + "wql": 0.09986741951582842 + }, + "finetuned": { + "mae": 28.39652060856895, + "mase": 5.970690249002982, + "wql": 0.07066036976225779 + } + }, + { + "origin": "2026-07-01", + "base": { + "mae": 28.943643769763764, + "mase": 5.264287698139678, + "wql": 0.06020927550342538 + }, + "finetuned": { + "mae": 25.747195773654518, + "mase": 4.6817299756992, + "wql": 0.05266972609114336 + } + } + ], + "dm": { + "mean_diff": 1.010434847348117, + "dm_stat": 1.136514930715151, + "dm_p": 0.2557411204149218, + "n": 19 + }, + "strategy": { + "base": { + "sharpe": 0.5594982829762936, + "cagr": 0.13544912138268494, + "max_dd": -0.3283134203571496, + "final_equity": 1.8282958491810062, + "n_rebalances": 19 + }, + "finetuned": { + "sharpe": 0.6753315076919172, + "cagr": 0.1733903838771873, + "max_dd": -0.32340423191275747, + "final_equity": 2.1372335975079917, + "n_rebalances": 19 + } + } +} \ No newline at end of file diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/measures/summary.json b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/measures/summary.json new file mode 100644 index 0000000000..8f233fcda8 --- /dev/null +++ b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/measures/summary.json @@ -0,0 +1,1119 @@ +{ + "config": { + "universe": [ + "AAPL", + "MSFT", + "NVDA", + "AMZN", + "GOOGL" + ], + "train_end": "2021-12-31", + "oos_start": "2022-01-01", + "context": 126, + "prediction": 63, + "max_steps": 300, + "seeds": [ + 1, + 2, + 3, + 42 + ], + "cost_bps": 5.0, + "base_model": "amazon/chronos-t5-tiny", + "rows": 2705 + }, + "seeds": { + "1": { + "per_origin": [ + { + "origin": "2022-01-03", + "base": { + "mae": 19.112240885174465, + "mase": 6.715310179770936, + "wql": 0.09924832974802081 + }, + "finetuned": { + "mae": 20.493394736638145, + "mase": 7.326273040439956, + "wql": 0.1082552414214325 + } + }, + { + "origin": "2022-04-01", + "base": { + "mae": 27.043341430785166, + "mase": 9.06461132645371, + "wql": 0.18836133795248183 + }, + "finetuned": { + "mae": 33.06470503125872, + "mase": 11.342710261402814, + "wql": 0.2637204596395267 + } + }, + { + "origin": "2022-07-01", + "base": { + "mae": 11.951092798747712, + "mase": 5.432116571175683, + "wql": 0.07046427566274908 + }, + "finetuned": { + "mae": 11.199436796279182, + "mase": 5.483097114827757, + "wql": 0.07158508346999515 + } + }, + { + "origin": "2022-10-03", + "base": { + "mae": 12.252073069981165, + "mase": 5.2359021067876315, + "wql": 0.08582680810405671 + }, + "finetuned": { + "mae": 12.781836337134951, + "mase": 5.537484910061301, + "wql": 0.09204240183338079 + } + }, + { + "origin": "2023-01-03", + "base": { + "mae": 10.09796972426157, + "mase": 6.406238714293873, + "wql": 0.0927650540302959 + }, + "finetuned": { + "mae": 7.825651020473903, + "mase": 5.269053476930168, + "wql": 0.06989706890940843 + } + }, + { + "origin": "2023-04-03", + "base": { + "mae": 14.975024613879976, + "mase": 8.079705452724259, + "wql": 0.0764614260218213 + }, + "finetuned": { + "mae": 10.218651938059972, + "mase": 5.884669916278058, + "wql": 0.06756602456747748 + } + }, + { + "origin": "2023-07-03", + "base": { + "mae": 10.35564916701544, + "mase": 5.202818064252516, + "wql": 0.04306302185560437 + }, + "finetuned": { + "mae": 10.58020054650685, + "mase": 4.992265651478332, + "wql": 0.03979578819364563 + } + }, + { + "origin": "2023-10-02", + "base": { + "mae": 12.099410320463635, + "mase": 5.887076163057914, + "wql": 0.04678358175478743 + }, + "finetuned": { + "mae": 8.894339183020211, + "mase": 4.2498191685318645, + "wql": 0.034558537238122286 + } + }, + { + "origin": "2024-01-02", + "base": { + "mae": 15.044763619559154, + "mase": 7.821216948087307, + "wql": 0.09788473543274959 + }, + "finetuned": { + "mae": 15.283677733890595, + "mase": 7.918241486041909, + "wql": 0.09391008133007414 + } + }, + { + "origin": "2024-04-01", + "base": { + "mae": 12.93625270298549, + "mase": 5.130334760564909, + "wql": 0.05380064966230843 + }, + "finetuned": { + "mae": 11.925704810732887, + "mase": 4.865963458537815, + "wql": 0.052605293775461824 + } + }, + { + "origin": "2024-07-01", + "base": { + "mae": 19.129212757897754, + "mase": 6.653539599583444, + "wql": 0.07124206257952674 + }, + "finetuned": { + "mae": 19.85181548103454, + "mase": 6.956537426617999, + "wql": 0.06953813768270228 + } + }, + { + "origin": "2024-10-01", + "base": { + "mae": 13.956217593238467, + "mase": 5.173618148239184, + "wql": 0.0546707949386418 + }, + "finetuned": { + "mae": 13.289636182028147, + "mase": 4.814990531501581, + "wql": 0.04666304004655715 + } + }, + { + "origin": "2025-01-02", + "base": { + "mae": 19.047051469106524, + "mase": 5.21132946204933, + "wql": 0.061964934565550425 + }, + "finetuned": { + "mae": 20.692156933981273, + "mase": 5.7089908081309, + "wql": 0.07149593014706293 + } + }, + { + "origin": "2025-04-01", + "base": { + "mae": 23.59232713003007, + "mase": 5.962642861827978, + "wql": 0.08171056568355033 + }, + "finetuned": { + "mae": 19.232701498364644, + "mase": 4.823220904333876, + "wql": 0.06594535220200567 + } + }, + { + "origin": "2025-07-01", + "base": { + "mae": 17.38633786156064, + "mase": 6.9582471426332955, + "wql": 0.06279104751569271 + }, + "finetuned": { + "mae": 13.667767624627976, + "mase": 5.377368618436665, + "wql": 0.0439684938499635 + } + }, + { + "origin": "2025-10-01", + "base": { + "mae": 16.622267998589408, + "mase": 4.295474368728061, + "wql": 0.045797900931628846 + }, + "finetuned": { + "mae": 22.225901624891492, + "mase": 5.863159238072177, + "wql": 0.05156435069471923 + } + }, + { + "origin": "2026-01-02", + "base": { + "mae": 24.067667933872762, + "mase": 5.480242706218102, + "wql": 0.06211389207195315 + }, + "finetuned": { + "mae": 29.348118421766497, + "mase": 6.65787568602988, + "wql": 0.07150832383322832 + } + }, + { + "origin": "2026-04-01", + "base": { + "mae": 40.83081970214844, + "mase": 8.662998868974928, + "wql": 0.10507715182427524 + }, + "finetuned": { + "mae": 29.669935293046258, + "mase": 6.304983383328478, + "wql": 0.07606022818210197 + } + }, + { + "origin": "2026-07-01", + "base": { + "mae": 29.062042526971727, + "mase": 5.204465898019073, + "wql": 0.06227272534086028 + }, + "finetuned": { + "mae": 24.406141347733758, + "mase": 4.511598129619542, + "wql": 0.052585148665675444 + } + } + ], + "dm": { + "mean_diff": 0.7847363560420826, + "dm_stat": 0.8262043599297081, + "dm_p": 0.40868817543074304, + "n": 19 + }, + "strategy": { + "base": { + "sharpe": 0.745783066608768, + "cagr": 0.21691535915102955, + "max_dd": -0.2696805452808133, + "final_equity": 2.540895297857738, + "n_rebalances": 19 + }, + "finetuned": { + "sharpe": 0.6027453160254682, + "cagr": 0.14300055636712705, + "max_dd": -0.3392080485926423, + "final_equity": 1.8867771086199885, + "n_rebalances": 19 + } + } + }, + "2": { + "per_origin": [ + { + "origin": "2022-01-03", + "base": { + "mae": 19.240389554462734, + "mase": 7.023998112107421, + "wql": 0.09894969938376562 + }, + "finetuned": { + "mae": 16.591725964016387, + "mase": 6.289248166278516, + "wql": 0.09644956003610414 + } + }, + { + "origin": "2022-04-01", + "base": { + "mae": 25.575135515606593, + "mase": 8.880534424505063, + "wql": 0.17040201080994988 + }, + "finetuned": { + "mae": 29.848317606486972, + "mase": 10.572572541237077, + "wql": 0.231936547396064 + } + }, + { + "origin": "2022-07-01", + "base": { + "mae": 11.015293678404792, + "mase": 5.212918914509073, + "wql": 0.06413789780408859 + }, + "finetuned": { + "mae": 10.086685534885952, + "mase": 4.853558689481431, + "wql": 0.06194327627197342 + } + }, + { + "origin": "2022-10-03", + "base": { + "mae": 8.605987981765988, + "mase": 4.158369332402529, + "wql": 0.0688811345158282 + }, + "finetuned": { + "mae": 10.11735132913741, + "mase": 4.565337839263637, + "wql": 0.0801429799715745 + } + }, + { + "origin": "2023-01-03", + "base": { + "mae": 11.175294219122994, + "mase": 6.919649610065283, + "wql": 0.09761679194889952 + }, + "finetuned": { + "mae": 8.65040077633328, + "mase": 5.51351429126901, + "wql": 0.07957510821230626 + } + }, + { + "origin": "2023-04-03", + "base": { + "mae": 10.800493579440646, + "mase": 6.330310883782866, + "wql": 0.0705801312350444 + }, + "finetuned": { + "mae": 9.264314457726858, + "mase": 5.3625057321610825, + "wql": 0.055240575140493554 + } + }, + { + "origin": "2023-07-03", + "base": { + "mae": 9.005152747744607, + "mase": 4.683129228704184, + "wql": 0.04245093829148341 + }, + "finetuned": { + "mae": 10.235760049971324, + "mase": 4.963969165588479, + "wql": 0.042578702594097094 + } + }, + { + "origin": "2023-10-02", + "base": { + "mae": 12.404869442894347, + "mase": 5.808955280069314, + "wql": 0.04411252606167688 + }, + "finetuned": { + "mae": 9.91220709180075, + "mase": 4.821603713108225, + "wql": 0.03647561511059898 + } + }, + { + "origin": "2024-01-02", + "base": { + "mae": 14.103005545479911, + "mase": 7.166527587717094, + "wql": 0.09136786889803836 + }, + "finetuned": { + "mae": 14.108838435581756, + "mase": 7.288597527835418, + "wql": 0.08501738803960976 + } + }, + { + "origin": "2024-04-01", + "base": { + "mae": 13.712336077008928, + "mase": 5.4225618029052916, + "wql": 0.052747260464580205 + }, + "finetuned": { + "mae": 11.544982377309646, + "mase": 4.493152408950246, + "wql": 0.041862125090910376 + } + }, + { + "origin": "2024-07-01", + "base": { + "mae": 18.325657992892797, + "mase": 6.304854215111592, + "wql": 0.06092091443955248 + }, + "finetuned": { + "mae": 18.064010111490887, + "mase": 6.232632112519619, + "wql": 0.06292170120626835 + } + }, + { + "origin": "2024-10-01", + "base": { + "mae": 13.215916491311692, + "mase": 4.850769066787696, + "wql": 0.04839476189385877 + }, + "finetuned": { + "mae": 11.898759484669517, + "mase": 4.294095976248818, + "wql": 0.041729056694459 + } + }, + { + "origin": "2025-01-02", + "base": { + "mae": 18.965100775824652, + "mase": 5.2330212784984, + "wql": 0.05943303924749856 + }, + "finetuned": { + "mae": 21.13191378154452, + "mase": 5.655052841700413, + "wql": 0.06259368087244278 + } + }, + { + "origin": "2025-04-01", + "base": { + "mae": 22.464964512416294, + "mase": 5.657843453030179, + "wql": 0.06907014679313236 + }, + "finetuned": { + "mae": 19.56221715533544, + "mase": 4.988403407206558, + "wql": 0.06114565082607025 + } + }, + { + "origin": "2025-07-01", + "base": { + "mae": 17.6908197796534, + "mase": 6.995424381565494, + "wql": 0.05693571332754854 + }, + "finetuned": { + "mae": 14.046976483057415, + "mase": 5.438470064331862, + "wql": 0.046728126095048506 + } + }, + { + "origin": "2025-10-01", + "base": { + "mae": 18.63142365955171, + "mase": 4.852930094325646, + "wql": 0.0412270541021487 + }, + "finetuned": { + "mae": 19.37632344563802, + "mase": 5.060783954088473, + "wql": 0.04295808673659006 + } + }, + { + "origin": "2026-01-02", + "base": { + "mae": 26.47312278142051, + "mase": 5.9423433373274674, + "wql": 0.06250326078623801 + }, + "finetuned": { + "mae": 30.139845203218005, + "mase": 6.762423515387857, + "wql": 0.07883904923042531 + } + }, + { + "origin": "2026-04-01", + "base": { + "mae": 38.492870609343996, + "mase": 8.110046005695054, + "wql": 0.10363441518732333 + }, + "finetuned": { + "mae": 31.133926197839163, + "mase": 6.449502023136193, + "wql": 0.08079452222494701 + } + }, + { + "origin": "2026-07-01", + "base": { + "mae": 27.998204355391245, + "mase": 5.008481696565779, + "wql": 0.055339928012947195 + }, + "finetuned": { + "mae": 24.792178150964162, + "mase": 4.519070245645079, + "wql": 0.046250700025709475 + } + } + ], + "dm": { + "mean_diff": 0.9152266138279145, + "dm_stat": 1.4289291192568372, + "dm_p": 0.1530246069878926, + "n": 19 + }, + "strategy": { + "base": { + "sharpe": 0.6642300946206589, + "cagr": 0.18811708844821018, + "max_dd": -0.287988405479613, + "final_equity": 2.2676780739147313, + "n_rebalances": 19 + }, + "finetuned": { + "sharpe": 0.7510316224582633, + "cagr": 0.20356756166906131, + "max_dd": -0.33740640670720123, + "final_equity": 2.411208425076607, + "n_rebalances": 19 + } + } + }, + "3": { + "per_origin": [ + { + "origin": "2022-01-03", + "base": { + "mae": 19.187569742354135, + "mase": 6.504784341888853, + "wql": 0.09323511243167339 + }, + "finetuned": { + "mae": 19.07165127103291, + "mase": 6.627630351716027, + "wql": 0.09366091735788966 + } + }, + { + "origin": "2022-04-01", + "base": { + "mae": 29.577983895559157, + "mase": 10.015682203402648, + "wql": 0.1893461299995895 + }, + "finetuned": { + "mae": 33.590193524436344, + "mase": 11.757195071821455, + "wql": 0.2756116723705998 + } + }, + { + "origin": "2022-07-01", + "base": { + "mae": 12.300530227782236, + "mase": 5.700427777527996, + "wql": 0.07221312828803769 + }, + "finetuned": { + "mae": 12.218761210971408, + "mase": 5.611972682801342, + "wql": 0.06760616703358126 + } + }, + { + "origin": "2022-10-03", + "base": { + "mae": 9.827110814291334, + "mase": 4.470741149321305, + "wql": 0.06734046906188715 + }, + "finetuned": { + "mae": 11.765081956651477, + "mase": 5.432541436523687, + "wql": 0.08149074682559745 + } + }, + { + "origin": "2023-01-03", + "base": { + "mae": 11.400473479619102, + "mase": 7.173366788391671, + "wql": 0.1087881423946215 + }, + "finetuned": { + "mae": 6.993351482209704, + "mase": 4.782363520801956, + "wql": 0.07702990568691749 + } + }, + { + "origin": "2023-04-03", + "base": { + "mae": 13.419580326383079, + "mase": 7.427877862619812, + "wql": 0.0767118636068171 + }, + "finetuned": { + "mae": 10.988303611392066, + "mase": 6.1206767621236455, + "wql": 0.05539038532906495 + } + }, + { + "origin": "2023-07-03", + "base": { + "mae": 13.063607509552487, + "mase": 6.115218781016517, + "wql": 0.04594152613476126 + }, + "finetuned": { + "mae": 13.641395278204055, + "mase": 6.276273920998695, + "wql": 0.04486114382685417 + } + }, + { + "origin": "2023-10-02", + "base": { + "mae": 12.551746271527005, + "mase": 5.92763812459795, + "wql": 0.04430073847226629 + }, + "finetuned": { + "mae": 8.30003106253488, + "mase": 4.048028592285309, + "wql": 0.033792901952940296 + } + }, + { + "origin": "2024-01-02", + "base": { + "mae": 15.444296991257442, + "mase": 7.863943554026179, + "wql": 0.09702248718097463 + }, + "finetuned": { + "mae": 15.273911636594743, + "mase": 7.830120937236996, + "wql": 0.09215270770148329 + } + }, + { + "origin": "2024-04-01", + "base": { + "mae": 13.718541463216145, + "mase": 5.321502922133572, + "wql": 0.0508694740402833 + }, + "finetuned": { + "mae": 11.580626835898746, + "mase": 4.649825981576372, + "wql": 0.04722500880905197 + } + }, + { + "origin": "2024-07-01", + "base": { + "mae": 19.448245675223212, + "mase": 6.532271859424823, + "wql": 0.06693209405123593 + }, + "finetuned": { + "mae": 19.999737573048424, + "mase": 6.654714522818549, + "wql": 0.06749723162136774 + } + }, + { + "origin": "2024-10-01", + "base": { + "mae": 15.39874257890005, + "mase": 5.6190274840720145, + "wql": 0.06038869184178612 + }, + "finetuned": { + "mae": 12.028678191654263, + "mase": 4.331683606977913, + "wql": 0.041171675418957804 + } + }, + { + "origin": "2025-01-02", + "base": { + "mae": 21.080347551618303, + "mase": 5.988196995621358, + "wql": 0.06479205834998664 + }, + "finetuned": { + "mae": 21.584850250728543, + "mase": 6.030662965966997, + "wql": 0.0681672676787988 + } + }, + { + "origin": "2025-04-01", + "base": { + "mae": 21.177659655374196, + "mase": 5.461816801269606, + "wql": 0.06827351779370362 + }, + "finetuned": { + "mae": 20.19732380216084, + "mase": 5.274280184778867, + "wql": 0.07119972389902389 + } + }, + { + "origin": "2025-07-01", + "base": { + "mae": 20.285511028955852, + "mase": 7.870041714298314, + "wql": 0.06338324437980324 + }, + "finetuned": { + "mae": 16.48996102469308, + "mase": 6.00734955914148, + "wql": 0.048910138443719026 + } + }, + { + "origin": "2025-10-01", + "base": { + "mae": 21.08738282218812, + "mase": 5.7570886625098, + "wql": 0.04471851517598069 + }, + "finetuned": { + "mae": 19.158668348524305, + "mase": 4.883394467981636, + "wql": 0.0451098164835342 + } + }, + { + "origin": "2026-01-02", + "base": { + "mae": 26.05136592804439, + "mase": 5.847894724783748, + "wql": 0.06679324561301948 + }, + "finetuned": { + "mae": 31.95166388617622, + "mase": 7.2848445736793135, + "wql": 0.08245967582756629 + } + }, + { + "origin": "2026-04-01", + "base": { + "mae": 39.78389035179502, + "mase": 8.402605344375406, + "wql": 0.10051845021148631 + }, + "finetuned": { + "mae": 30.221296861436635, + "mase": 6.300297542399798, + "wql": 0.07086281536311552 + } + }, + { + "origin": "2026-07-01", + "base": { + "mae": 30.176719350663443, + "mase": 5.580190526132947, + "wql": 0.06204832074676777 + }, + "finetuned": { + "mae": 23.596383909195193, + "mase": 4.385238087469757, + "wql": 0.045945342769384126 + } + } + ], + "dm": { + "mean_diff": 1.385759681408467, + "dm_stat": 1.681447410457044, + "dm_p": 0.0926760431945916, + "n": 19 + }, + "strategy": { + "base": { + "sharpe": 0.5876952849794809, + "cagr": 0.1599171018872516, + "max_dd": -0.2790582806766164, + "final_equity": 2.0231497085857915, + "n_rebalances": 19 + }, + "finetuned": { + "sharpe": 0.7455377688999513, + "cagr": 0.19937912269961133, + "max_dd": -0.34095499626797776, + "final_equity": 2.3716101661182125, + "n_rebalances": 19 + } + } + }, + "42": { + "per_origin": [ + { + "origin": "2022-01-03", + "base": { + "mae": 17.77222561911931, + "mase": 6.37310320270795, + "wql": 0.09317199843236994 + }, + "finetuned": { + "mae": 17.48824496799045, + "mase": 6.259213135253562, + "wql": 0.09675759272817334 + } + }, + { + "origin": "2022-04-01", + "base": { + "mae": 24.79408077966599, + "mase": 8.484391968401898, + "wql": 0.16516753055397998 + }, + "finetuned": { + "mae": 30.4053437096732, + "mase": 10.698399002879507, + "wql": 0.24388368458614806 + } + }, + { + "origin": "2022-07-01", + "base": { + "mae": 12.817951253860713, + "mase": 5.875999756312781, + "wql": 0.07389239722008255 + }, + "finetuned": { + "mae": 11.53521071237231, + "mase": 5.606788221177423, + "wql": 0.07214014766434476 + } + }, + { + "origin": "2022-10-03", + "base": { + "mae": 9.649188078017463, + "mase": 4.435610356125639, + "wql": 0.07149681594586801 + }, + "finetuned": { + "mae": 9.996672672695585, + "mase": 4.535960081787418, + "wql": 0.07446640622360721 + } + }, + { + "origin": "2023-01-03", + "base": { + "mae": 12.38764711713034, + "mase": 7.785916207065794, + "wql": 0.11112482431220494 + }, + "finetuned": { + "mae": 8.242120700412325, + "mase": 5.379777017819521, + "wql": 0.07410529353446793 + } + }, + { + "origin": "2023-04-03", + "base": { + "mae": 13.93667439052037, + "mase": 7.864740807678162, + "wql": 0.07337398384906456 + }, + "finetuned": { + "mae": 10.751853839934817, + "mase": 5.914045029180344, + "wql": 0.056990499599649316 + } + }, + { + "origin": "2023-07-03", + "base": { + "mae": 9.829068659222315, + "mase": 4.930962567669164, + "wql": 0.04114367959132122 + }, + "finetuned": { + "mae": 10.772593180338541, + "mase": 5.022953446384486, + "wql": 0.043292278404674284 + } + }, + { + "origin": "2023-10-02", + "base": { + "mae": 14.54718453543527, + "mase": 6.5180222772685195, + "wql": 0.05014160627261666 + }, + "finetuned": { + "mae": 9.12182899353996, + "mase": 4.373883240482156, + "wql": 0.033347530515199876 + } + }, + { + "origin": "2024-01-02", + "base": { + "mae": 15.573319292825365, + "mase": 7.707004954035614, + "wql": 0.09415353258100762 + }, + "finetuned": { + "mae": 14.852715083530972, + "mase": 7.620316424738471, + "wql": 0.09234689792607934 + } + }, + { + "origin": "2024-04-01", + "base": { + "mae": 11.308997332860551, + "mase": 4.671071480702364, + "wql": 0.0481928057690567 + }, + "finetuned": { + "mae": 11.217902652801028, + "mase": 4.548331610881923, + "wql": 0.047989844674068236 + } + }, + { + "origin": "2024-07-01", + "base": { + "mae": 19.06674625457279, + "mase": 6.491409830465822, + "wql": 0.06661110729273978 + }, + "finetuned": { + "mae": 20.29634086972191, + "mase": 6.896829059742433, + "wql": 0.07078048766871961 + } + }, + { + "origin": "2024-10-01", + "base": { + "mae": 13.236307997930618, + "mase": 4.869891343511878, + "wql": 0.05150909767984644 + }, + "finetuned": { + "mae": 13.632048325311569, + "mase": 4.664383130771989, + "wql": 0.039256277537598604 + } + }, + { + "origin": "2025-01-02", + "base": { + "mae": 19.59426511734251, + "mase": 5.325298496824117, + "wql": 0.06021561361397641 + }, + "finetuned": { + "mae": 21.220824250720796, + "mase": 5.742002701115111, + "wql": 0.06655037139711725 + } + }, + { + "origin": "2025-04-01", + "base": { + "mae": 22.29641590421162, + "mase": 5.687334889991485, + "wql": 0.07251090537950837 + }, + "finetuned": { + "mae": 21.091515725756448, + "mase": 5.4138313361779655, + "wql": 0.06941840772564808 + } + }, + { + "origin": "2025-07-01", + "base": { + "mae": 18.723279971168154, + "mase": 7.563205118855348, + "wql": 0.06289960873006444 + }, + "finetuned": { + "mae": 15.317030746217757, + "mase": 6.010471878614314, + "wql": 0.048575624194647275 + } + }, + { + "origin": "2025-10-01", + "base": { + "mae": 18.76023288302951, + "mase": 5.339240969202885, + "wql": 0.042659501903981524 + }, + "finetuned": { + "mae": 20.631201462518597, + "mase": 5.471035346168572, + "wql": 0.04856228647626735 + } + }, + { + "origin": "2026-01-02", + "base": { + "mae": 24.027901834154882, + "mase": 5.21642452875602, + "wql": 0.061348772620823 + }, + "finetuned": { + "mae": 28.036732507130456, + "mase": 5.9906097563615495, + "wql": 0.07045652359108559 + } + }, + { + "origin": "2026-04-01", + "base": { + "mae": 40.68702809167287, + "mase": 8.770697206662716, + "wql": 0.09986741951582842 + }, + "finetuned": { + "mae": 28.39652060856895, + "mase": 5.970690249002982, + "wql": 0.07066036976225779 + } + }, + { + "origin": "2026-07-01", + "base": { + "mae": 28.943643769763764, + "mase": 5.264287698139678, + "wql": 0.06020927550342538 + }, + "finetuned": { + "mae": 25.747195773654518, + "mase": 4.6817299756992, + "wql": 0.05266972609114336 + } + } + ], + "dm": { + "mean_diff": 1.010434847348117, + "dm_stat": 1.136514930715151, + "dm_p": 0.2557411204149218, + "n": 19 + }, + "strategy": { + "base": { + "sharpe": 0.5594982829762936, + "cagr": 0.13544912138268494, + "max_dd": -0.3283134203571496, + "final_equity": 1.8282958491810062, + "n_rebalances": 19 + }, + "finetuned": { + "sharpe": 0.6753315076919172, + "cagr": 0.1733903838771873, + "max_dd": -0.32340423191275747, + "final_equity": 2.1372335975079917, + "n_rebalances": 19 + } + } + } + } +} \ No newline at end of file diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/run_finetune_chronos.py b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/run_finetune_chronos.py new file mode 100644 index 0000000000..531c865295 --- /dev/null +++ b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/finetune/run_finetune_chronos.py @@ -0,0 +1,352 @@ +"""Port local GPU du re-entrainement Chronos du livre (06/18/02, HandsOnAITradingBook e025f21). + +Le livre entraine `amazon/chronos-t5-tiny` DANS l'algorithme LEAN (a chaque +rebalancement trimestriel, `_train_chronos` puis `ChronosPipeline.predict`). +Ce harnais sort le re-entrainement de l'algorithme pour l'executer sur un GPU +local, et compare modele de base et modele re-entraine hors echantillon : + + 1. `--stage finetune` : re-entraine le modele par graine (recette du livre : + context 126 j, prediction 63 j, lr 1e-5, adamw_torch_fused, batch 32, + accumulation 2, tf32 Ampere). Ecarts documentes : `max_steps` configurable + (le livre utilise 3, budget de runtime cloud) et `torch_compile=False` + (portabilite Windows). + 2. `--stage eval` : erreur de prevision hors echantillon (2022-2026) par + origine trimestrielle — MAE, MASE (naif m=1 sur la fenetre reelle) et + WQL (quantiles 0.1/0.25/0.5/0.75/0.9, forme publiee normalisee par + somme|a|), modele de base vs re-entraine, meme graine de tirage pour + les deux bras (appariement par origine). + 3. Test de Diebold-Mariano apparie par origine (origines trimestrielles + NON chevauchantes) sur la perte MAE. + 4. Simulation locale de la strategie du livre (poids SLSQP maximisant le + Sharpe des courbes prevues, long-only, rebalancement trimestriel, couts + 5 bps sur le turnover) sur la fenetre OOS, pour les deux bras. + +Donnees : cloture ajustee quotidienne (yfinance) de 5 mega-caps US, cache +local. Univers fixe (AAPL, MSFT, NVDA, AMZN, GOOGL) — ecart documente vs la +selection dynamique top-5 dollar-volume du livre (indisponible hors QC). + +Usage : + set CUDA_VISIBLE_DEVICES=1 # enumeration torch par defaut : 1 = 3080 Ti (libre) + python run_finetune_chronos.py --stage all --seeds 1 2 3 42 --run-dir D:\\Dev\\CoursIA-18962-run +""" + +from __future__ import annotations + +import argparse +import json +import logging +import math +import os +import sys +import time +from ast import literal_eval +from functools import partial +from pathlib import Path + +import numpy as np +import pandas as pd +import torch + +FINETUNE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(FINETUNE_DIR)) + +from chronos_training import ( # noqa: E402 (vendored v2.3.2, voir en-tete du fichier) + ChronosDataset, + has_enough_observations, + load_model, +) + +from chronos import ChronosConfig, ChronosPipeline # noqa: E402 +from gluonts.dataset.pandas import PandasDataset # noqa: E402 +from gluonts.itertools import Filter # noqa: E402 +from scipy.optimize import minimize # noqa: E402 +from transformers import Trainer, TrainingArguments, set_seed # noqa: E402 + +UNIVERSE = ["AAPL", "MSFT", "NVDA", "AMZN", "GOOGL"] +DATA_START = "2016-01-01" +TRAIN_END = "2021-12-31" +OOS_START = "2022-01-01" +CONTEXT_LENGTH = 126 +PREDICTION_LENGTH = 63 +NUM_SAMPLES = 20 +BASE_MODEL = "amazon/chronos-t5-tiny" +QUANTILES = [0.1, 0.25, 0.5, 0.75, 0.9] +COST_BPS = 5.0 + + +def log(msg: str) -> None: + print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True) + + +def device_banner() -> str: + """Imprime le GPU reellement selectionne (l'index torch ne suit pas nvidia-smi).""" + if not torch.cuda.is_available(): + raise SystemExit("CUDA indisponible — verifier l'env (bonsai) et le pilote.") + name = torch.cuda.get_device_name(0) + props = torch.cuda.get_device_properties(0) + log(f"GPU: {name} ({props.total_memory / 2**30:.1f} GiB) — " + f"CUDA_VISIBLE_DEVICES={os.environ.get('CUDA_VISIBLE_DEVICES', '(non pose)')}") + return name + + +def load_prices(run_dir: Path) -> pd.DataFrame: + cache = run_dir / "data" / "closes.csv" + if cache.exists(): + df = pd.read_csv(cache, index_col=0, parse_dates=True) + log(f"donnees: cache {cache} ({df.shape[0]} j x {df.shape[1]} series)") + return df + import yfinance as yf + + log("donnees: telechargement yfinance...") + raw = yf.download(UNIVERSE, start=DATA_START, interval="1d", auto_adjust=True, progress=False) + df = raw["Close"][UNIVERSE].dropna(how="all") + cache.parent.mkdir(parents=True, exist_ok=True) + df.to_csv(cache) + log(f"donnees: {df.shape[0]} j x {df.shape[1]} series -> {cache}") + return df + + +def build_training_frames(prices: pd.DataFrame) -> list[pd.DataFrame]: + """Frames d'entrainement au format du livre : index quotidien calendaire (asfreq).""" + train = prices.loc[:TRAIN_END] + frames = [] + for symbol in UNIVERSE: + s = train[symbol].dropna() + frame = s.reset_index() + frame.columns = ["time", "target"] + frame["time"] = pd.to_datetime(frame["time"]) + frames.append(frame.set_index("time").resample("D").asfreq()) + return frames + + +def train_chronos(training_data, output_dir: Path, seed: int, max_steps: int, + context_length: int = CONTEXT_LENGTH, prediction_length: int = PREDICTION_LENGTH, + min_past: int = 64, per_device_train_batch_size: int = 32, + learning_rate: float = 1e-5, optim: str = "adamw_torch_fused", + gradient_accumulation_steps: int = 2, model_id: str = BASE_MODEL, + model_type: str = "seq2seq", tf32: bool = True, + torch_compile: bool = False) -> Path: + """Corps de `_train_chronos` du livre (06/18/02), hors algorithme, seed parametrable.""" + import chronos_training as chronos_train_module + + chronos_train_module.logger = logging.getLogger() + chronos_train_module.logger.setLevel(logging.INFO) + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + probabilities = [1.0 / len(training_data)] * len(training_data) + tokenizer_kwargs = literal_eval("{'low_limit': -15.0, 'high_limit': 15.0}") + set_seed(seed, True) + + train_datasets = [ + Filter( + partial(has_enough_observations, min_length=min_past + prediction_length, max_missing_prop=0.9), + PandasDataset(data_frame, freq="D"), + ) + for data_frame in training_data + ] + model = load_model(model_id=model_id, model_type=model_type, vocab_size=4096, + random_init=False, tie_embeddings=False, pad_token_id=0, eos_token_id=1) + chronos_config = ChronosConfig( + tokenizer_class="MeanScaleUniformBins", tokenizer_kwargs=tokenizer_kwargs, + n_tokens=4096, n_special_tokens=2, pad_token_id=0, eos_token_id=1, + use_eos_token=True, model_type=model_type, context_length=context_length, + prediction_length=prediction_length, num_samples=NUM_SAMPLES, + temperature=1.0, top_k=50, top_p=1.0, + ) + model.config.chronos_config = chronos_config.__dict__ + shuffled = ChronosDataset( + datasets=train_datasets, probabilities=probabilities, + tokenizer=chronos_config.create_tokenizer(), context_length=context_length, + prediction_length=prediction_length, min_past=min_past, mode="training", + ).shuffle(100) + + args = TrainingArguments( + output_dir=str(output_dir), per_device_train_batch_size=per_device_train_batch_size, + learning_rate=learning_rate, lr_scheduler_type="linear", warmup_ratio=0.0, + optim=optim, logging_dir=str(output_dir / "train-logs"), logging_strategy="steps", + logging_steps=25, save_strategy="no", report_to=[], max_steps=max_steps, + gradient_accumulation_steps=gradient_accumulation_steps, dataloader_num_workers=1, + tf32=tf32, torch_compile=torch_compile, ddp_find_unused_parameters=False, + remove_unused_columns=False, seed=seed, + ) + Trainer(model=model, args=args, train_dataset=shuffled).train() + model.save_pretrained(output_dir) + return output_dir + + +def quarter_origins(index: pd.DatetimeIndex, warmup: int) -> list[pd.Timestamp]: + """Premier jour de bourse de chaque trimestre de la fenetre OOS, apres warmup.""" + seen, origins = set(), [] + for i, ts in enumerate(index): + if ts < pd.Timestamp(OOS_START) or i < warmup: + continue + key = (ts.year, (ts.month - 1) // 3) + if key not in seen: + seen.add(key) + origins.append(ts) + while origins and index.get_loc(origins[-1]) + PREDICTION_LENGTH >= len(index): + origins.pop() + return origins + + +def forecast_quantiles(pipeline: ChronosPipeline, context: np.ndarray, seed: int) -> dict: + torch.manual_seed(seed) + samples = pipeline.predict([torch.tensor(context, dtype=torch.float32)], + PREDICTION_LENGTH, num_samples=NUM_SAMPLES) + arr = samples[0].float().cpu().numpy() + return {q: np.quantile(arr, q, axis=0) for q in QUANTILES} + + +def metrics_for_origin(med: np.ndarray, quants: dict, actual: np.ndarray) -> dict: + mae = float(np.mean(np.abs(med - actual))) + naive_den = float(np.mean(np.abs(np.diff(actual)))) if len(actual) > 1 else float("nan") + mase = mae / naive_den if naive_den else float("nan") + denom = float(np.sum(np.abs(actual))) or 1.0 + wql = float(np.mean([ + 2.0 * np.sum(np.maximum(q * (actual - quants[q]), (1 - q) * (quants[q] - actual))) / denom + for q in QUANTILES + ])) + return {"mae": mae, "mase": mase, "wql": wql} + + +def dm_test(loss_a: np.ndarray, loss_b: np.ndarray) -> dict: + """Diebold-Mariano apparie (origines non chevauchantes) sur loss_a - loss_b.""" + d = np.asarray(loss_a) - np.asarray(loss_b) + n = len(d) + mean_d = float(np.mean(d)) + sd = float(np.std(d, ddof=1)) if n > 1 else float("nan") + if not sd or math.isnan(sd): + return {"mean_diff": mean_d, "dm_stat": float("nan"), "dm_p": float("nan"), "n": n} + stat = mean_d / (sd / math.sqrt(n)) + p = math.erfc(abs(stat) / math.sqrt(2)) + return {"mean_diff": mean_d, "dm_stat": float(stat), "dm_p": float(p), "n": n} + + +def optimize_weights(forecast_df: pd.DataFrame) -> np.ndarray: + """Poids SLSQP du livre : maximise le Sharpe des courbes prevues, long-only, somme=1.""" + returns = forecast_df.pct_change().dropna() + n = returns.shape[1] + + def neg_sharpe(weights): + mean_returns = returns.mean() * 252 + cov = returns.cov() * 252 + port_ret = float(np.sum(mean_returns * weights)) + port_std = float(np.sqrt(np.dot(weights.T, np.dot(cov, weights)))) + return -(port_ret / port_std) if port_std > 0 else 0.0 + + res = minimize(neg_sharpe, n * [1.0 / n], method="SLSQP", + bounds=tuple((0, 1) for _ in range(n)), + constraints=({"type": "eq", "fun": lambda w: np.sum(w) - 1})) + return res.x + + +def simulate_strategy(pairs: list[tuple[np.ndarray, np.ndarray]]) -> dict: + """Chaine les rebalancements (poids, rendements realises) avec couts sur le turnover.""" + equity, prev_w, rets = 1.0, np.zeros(len(UNIVERSE)), [] + for w, realized in pairs: + turnover = float(np.sum(np.abs(w - prev_w))) + r = float(np.dot(w, realized)) - turnover * COST_BPS / 1e4 + equity *= 1.0 + r + rets.append(r) + prev_w = w + stats = curve_stats(rets) + stats["n_rebalances"] = len(rets) + return stats + + +def curve_stats(returns: list) -> dict: + r = np.asarray(returns) + if len(r) < 2: + return {"sharpe": float("nan"), "cagr": float("nan"), "max_dd": float("nan"), "final_equity": float("nan")} + equity = np.cumprod(1 + r) + years = len(r) / 4.0 + cagr = float(equity[-1] ** (1 / years) - 1) if years > 0 else float("nan") + sd = float(np.std(r, ddof=1)) + sharpe = float(np.mean(r) / sd * math.sqrt(4)) if sd > 0 else float("nan") + peak = np.maximum.accumulate(equity) + return {"sharpe": sharpe, "cagr": cagr, "max_dd": float(np.min(equity / peak - 1)), + "final_equity": float(equity[-1])} + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--stage", choices=["finetune", "eval", "all"], default="all") + ap.add_argument("--seeds", type=int, nargs="+", default=[1, 2, 3, 42]) + ap.add_argument("--max-steps", type=int, default=300) + ap.add_argument("--run-dir", type=str, required=True) + ap.add_argument("--smoke", action="store_true") + args = ap.parse_args() + + run_dir = Path(args.run_dir) + (run_dir / "models").mkdir(parents=True, exist_ok=True) + (run_dir / "results").mkdir(parents=True, exist_ok=True) + + device_banner() + prices = load_prices(run_dir) + index = prices.index + origins = quarter_origins(index, warmup=CONTEXT_LENGTH + 1) + if args.smoke: + origins = origins[:1] + args.seeds = args.seeds[:1] + args.max_steps = min(args.max_steps, 20) + log(f"origines OOS: {len(origins)} ({origins[0].date()} -> {origins[-1].date()})") + + summary = {"config": { + "universe": UNIVERSE, "train_end": TRAIN_END, "oos_start": OOS_START, + "context": CONTEXT_LENGTH, "prediction": PREDICTION_LENGTH, + "max_steps": args.max_steps, "seeds": args.seeds, "cost_bps": COST_BPS, + "base_model": BASE_MODEL, "rows": int(len(prices)), + }, "seeds": {}} + + for seed in args.seeds: + seed_dir = run_dir / "models" / f"seed-{seed}" + if args.stage in ("finetune", "all") and not (seed_dir / "config.json").exists(): + t0 = time.time() + log(f"graine {seed}: re-entrainement ({args.max_steps} pas)...") + train_chronos(build_training_frames(prices), seed_dir, seed=seed, max_steps=args.max_steps) + log(f"graine {seed}: entraine en {time.time() - t0:.0f} s") + + if args.stage not in ("eval", "all"): + continue + + log(f"graine {seed}: evaluation base vs re-entraine sur {len(origins)} origines") + base_pipe = ChronosPipeline.from_pretrained(BASE_MODEL, device_map="cuda", torch_dtype=torch.bfloat16) + ft_pipe = ChronosPipeline.from_pretrained(str(seed_dir), device_map="cuda", torch_dtype=torch.bfloat16) + per_origin, strat_pairs = [], {"base": [], "finetuned": []} + for k, origin in enumerate(origins): + i = index.get_loc(origin) + ctx = prices.iloc[i - CONTEXT_LENGTH:i][UNIVERSE].values + act = prices.iloc[i:i + PREDICTION_LENGTH][UNIVERSE].values + realized = prices.iloc[i + PREDICTION_LENGTH][UNIVERSE].values / prices.iloc[i][UNIVERSE].values - 1.0 + row = {"origin": str(origin.date())} + for arm, pipe in (("base", base_pipe), ("finetuned", ft_pipe)): + med = np.empty((PREDICTION_LENGTH, len(UNIVERSE))) + per_asset = [] + for j in range(len(UNIVERSE)): + qs = forecast_quantiles(pipe, ctx[:, j], seed=seed * 1000 + k) + med[:, j] = qs[0.5] + per_asset.append(metrics_for_origin(med[:, j], qs, act[:, j])) + row[arm] = {kk: float(np.mean([m[kk] for m in per_asset])) for kk in ("mae", "mase", "wql")} + strat_pairs[arm].append((optimize_weights(pd.DataFrame(med, columns=UNIVERSE)), realized)) + per_origin.append(row) + log(f" {origin.date()}: MAE base {row['base']['mae']:.4f} / ft {row['finetuned']['mae']:.4f}") + + dm = dm_test([r["base"]["mae"] for r in per_origin], [r["finetuned"]["mae"] for r in per_origin]) + strat = {arm: simulate_strategy(pairs) for arm, pairs in strat_pairs.items()} + entry = {"per_origin": per_origin, "dm": dm, "strategy": strat} + summary["seeds"][str(seed)] = entry + with open(run_dir / "results" / f"seed-{seed}.json", "w", encoding="utf-8") as fh: + json.dump(entry, fh, indent=1) + log(f"graine {seed}: DM stat {dm['dm_stat']:.3f} p {dm['dm_p']:.4f} " + f"(MAE base-ft {dm['mean_diff']:+.5f}) | strat Sharpe base {strat['base']['sharpe']:.3f} " + f"ft {strat['finetuned']['sharpe']:.3f}") + + with open(run_dir / "results" / "summary.json", "w", encoding="utf-8") as fh: + json.dump(summary, fh, indent=1) + log(f"resultats ecrits dans {run_dir / 'results'}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/main_finetuned.py b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/main_finetuned.py new file mode 100644 index 0000000000..62f6aad4e1 --- /dev/null +++ b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/main_finetuned.py @@ -0,0 +1,372 @@ +# region imports +from AlgorithmImports import * + +from scipy.optimize import minimize + +import torch +from ast import literal_eval +from pathlib import Path +from functools import partial +from transformers import Trainer, TrainingArguments, set_seed +from gluonts.dataset.pandas import PandasDataset +from gluonts.itertools import Filter +from chronos import ChronosConfig, ChronosPipeline + +# Shim d'import : le livre importe `chronos.scripts.training.train`, module absent +# du wheel PyPI (il vit a la racine du depot amazon-science/chronos-forecasting). +# Sur un environnement qui ne l'expose pas, on retombe sur le script vendorise +# du projet (finetune/chronos_training.py, v2.3.2, Apache-2.0) — voir README. +try: + from chronos.scripts.training.train import ( + ChronosDataset, + has_enough_observations, + load_model, + ) +except ImportError: # pragma: no cover - environnement local du harnais + import sys as _sys + + _sys.path.insert(0, str(Path(__file__).resolve().parent / "finetune")) + from chronos_training import ( # noqa: F401 + ChronosDataset, + has_enough_observations, + load_model, + ) + +from logging import getLogger, INFO +import sys +# endregion + +# Port fidele de `06 Applied Machine Learning/18 Amazon Chronos Model/02 Fine-Tuned +# Model/main.py` (QuantConnect/HandsOnAITradingBook, commit e025f21). Le +# re-entrainement tourne DANS l'algorithme a chaque rebalancement trimestriel : +# il exige un noeud de recherche avec GPU et les dependances chronos/gluonts — +# il n'est donc PAS executable en CI (voir finetune/ et le README du projet pour +# la mesure hors echantillon base vs re-entraine, faite par harnais local). +# Adaptations : parametre `seed` du re-entrainement (le livre fige set_seed(1)), +# import shim ci-dessus, et `self.get_parameter` conserves tels quels. + + +class HuggingFaceFineTunedDemo(QCAlgorithm): + """ + This algorithm demonstrates how to fine-tune a HuggingFace model. + It uses the "amazon/chronos-t5-tiny" model to forecast the + future equity curves of the 5 most liquid assets in the market, + then it uses the SciPy package to find the portfolio weights + that will maximize the future Sharpe ratio of the portfolio. + The model is retrained and the portfolio is rebalanced every 3 + months. + """ + + def initialize(self): + self.set_start_date(2019, 1, 1) + self.set_end_date(2024, 4, 1) + self.set_cash(100_000) + + # Disable the daily precise end time because of the time rules. + self.settings.daily_precise_end_time = False + self.settings.min_absolute_portfolio_target_percentage = 0 + + # Define the universe. + spy = Symbol.create("SPY", SecurityType.EQUITY, Market.USA) + self.universe_settings.schedule.on(self.date_rules.month_start(spy)) + self.universe_settings.resolution = Resolution.DAILY + self._universe = self.add_universe( + self.universe.dollar_volume.top( + self.get_parameter('universe_size', 5) + ) + ) + + # Define some trading parameters. + self._lookback_period = timedelta( + 365 * self.get_parameter('lookback_years', 1) + ) + self._prediction_length = 3*21 # Three months of trading days + + # Schedule rebalances. + self._last_rebalance = datetime.min + self.schedule.on( + self.date_rules.month_start(spy, 1), + self.time_rules.midnight, + self._trade + ) + + # Add warm up so the algorithm trades on deployment. + self.set_warm_up(timedelta(31)) + + # Define the model and some of its settings. + self._device_map = "cuda" if torch.cuda.is_available() else "cpu" + self._optimizer = 'adamw_torch_fused' if torch.cuda.is_available() else 'adamw_torch' + self._model_name = "amazon/chronos-t5-tiny" + self._seed = int(self.get_parameter('seed', 1)) + self._model_path = self.object_store.get_file_path( + f"llm/fine-tune/{self._model_name.replace('/', '-')}/" + ) + + def on_warmup_finished(self): + # Trade right after warm up is done. + self._trade() + + def _sharpe_ratio( + self, weights, returns, risk_free_rate, trading_days_per_year=252): + # Define how to calculate the Sharpe ratio so we can use + # it to optimize the portfolio weights. + + # Calculate the annualized returns and covariance matrix. + mean_returns = returns.mean() * trading_days_per_year + cov_matrix = returns.cov() * trading_days_per_year + + # Calculate the Sharpe ratio. + portfolio_return = np.sum(mean_returns * weights) + portfolio_std = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights))) + sharpe_ratio = (portfolio_return - risk_free_rate) / portfolio_std + + # Return negative Sharpe ratio because we minimize this + # function in optimization. + return -sharpe_ratio + + def _optimize_portfolio(self, equity_curves): + returns = equity_curves.pct_change().dropna() + num_assets = returns.shape[1] + initial_guess = num_assets * [1. / num_assets,] + # Find portfolio weights that mazimize the forward Sharpe + # ratio. + result = minimize( + self._sharpe_ratio, + initial_guess, + args=( + returns, + self.risk_free_interest_rate_model.get_interest_rate(self.time) + ), + method='SLSQP', + bounds=tuple((0, 1) for _ in range(num_assets)), + constraints=( + {'type': 'eq', 'fun': lambda weights: np.sum(weights) - 1} + ) + ) + return result.x + + def _trade(self): + # Don't rebalance during warm-up. + if self.is_warming_up: + return + # Only rebalance on a quarterly basis. + if self.time - self._last_rebalance < timedelta(80): + return + self._last_rebalance = self.time + + symbols = list(self._universe.selected) + + # Get historical equity curves. + history = self.history(symbols, self._lookback_period)['close'].unstack(0) + + # Gather the training data. + training_data_by_symbol = {} + for symbol in symbols: + df = history[[symbol]].dropna() + if df.shape[0] < 10: # Skip this asset if there is very little data + continue + adjusted_df = df.reset_index()[['time', symbol]] + adjusted_df = adjusted_df.rename(columns={str(symbol.id): 'target'}) + adjusted_df['time'] = pd.to_datetime(adjusted_df['time']) + adjusted_df.set_index('time', inplace=True) + adjusted_df = adjusted_df.resample('D').asfreq() + training_data_by_symbol[symbol] = adjusted_df + tradable_symbols = list(training_data_by_symbol.keys()) + + # Fine-tune the model. + output_dir_path = self._train_chronos( + list(training_data_by_symbol.values()), + context_length=int(252/2), # 6 months + prediction_length=self._prediction_length, + optim=self._optimizer, + model_id=self._model_name, + output_dir=self._model_path, + learning_rate=1e-5, + # Requires Ampere GPUs (e.g., A100) + tf32=False, + max_steps=3 + ) + + # Load the fine-tuned model. + pipeline = ChronosPipeline.from_pretrained( + output_dir_path, + device_map=self._device_map, + torch_dtype=torch.bfloat16, + ) + + # Forecast the future equity curves. + all_forecasts = pipeline.predict( + [ + torch.tensor(history[symbol].dropna()) + for symbol in tradable_symbols + ], + self._prediction_length + ) + + # Take the median forecast for each asset. + forecasts_df = pd.DataFrame( + { + symbol: np.quantile( + all_forecasts[i].numpy(), 0.5, axis=0 # 0.5 = median + ) + for i, symbol in enumerate(tradable_symbols) + } + ) + + # Find the weights that maximize the forward Sharpe + # ratio of the portfolio. + optimal_weights = self._optimize_portfolio(forecasts_df) + + # Rebalance the portfolio. + self.set_holdings( + [ + PortfolioTarget(symbol, optimal_weights[i]) + for i, symbol in enumerate(tradable_symbols) + ], + True + ) + + def _train_chronos( + self, training_data, + probability: Optional[str] = None, + context_length: int = 512, + prediction_length: int = 64, + min_past: int = 64, + max_steps: int = 200_000, + save_steps: int = 50_000, + log_steps: int = 500, + per_device_train_batch_size: int = 32, + learning_rate: float = 1e-3, + optim: str = "adamw_torch_fused", + shuffle_buffer_length: int = 100, + gradient_accumulation_steps: int = 2, + model_id: str = "google/t5-efficient-tiny", + model_type: str = "seq2seq", + random_init: bool = False, + tie_embeddings: bool = False, + output_dir: str = "./output/", + tf32: bool = True, + torch_compile: bool = True, + tokenizer_class: str = "MeanScaleUniformBins", + tokenizer_kwargs: str = "{'low_limit': -15.0, 'high_limit': 15.0}", + n_tokens: int = 4096, + n_special_tokens: int = 2, + pad_token_id: int = 0, + eos_token_id: int = 1, + use_eos_token: bool = True, + lr_scheduler_type: str = "linear", + warmup_ratio: float = 0.0, + dataloader_num_workers: int = 1, + max_missing_prop: float = 0.9, + num_samples: int = 20, + temperature: float = 1.0, + top_k: int = 50, + top_p: float = 1.0): + + # Set up logging for the train object. + train.logger = getLogger() + train.logger.setLevel(INFO) + # Ensure output_dir is a Path object. + output_dir = Path(output_dir) + # Convert probability from string to a list, or set default if + # None. + if isinstance(probability, str): + probability = literal_eval(probability) + elif probability is None: + probability = [1.0 / len(training_data)] * len(training_data) + # Convert tokenizer_kwargs from string to a dictionary. + if isinstance(tokenizer_kwargs, str): + tokenizer_kwargs = literal_eval(tokenizer_kwargs) + # Enable reproducibility. + set_seed(self._seed, True) + # Create datasets for training, filtered by criteria. + train_datasets = [ + Filter( + partial( + has_enough_observations, + min_length=min_past + prediction_length, + max_missing_prop=max_missing_prop, + ), + PandasDataset(data_frame, freq="D"), + ) + for data_frame in training_data + ] + # Load the model with the specified configuration. + model = load_model( + model_id=model_id, + model_type=model_type, + vocab_size=n_tokens, + random_init=random_init, + tie_embeddings=tie_embeddings, + pad_token_id=pad_token_id, + eos_token_id=eos_token_id, + ) + # Define the configuration for the Chronos + # tokenizer and other settings. + chronos_config = ChronosConfig( + tokenizer_class=tokenizer_class, + tokenizer_kwargs=tokenizer_kwargs, + n_tokens=n_tokens, + n_special_tokens=n_special_tokens, + pad_token_id=pad_token_id, + eos_token_id=eos_token_id, + use_eos_token=use_eos_token, + model_type=model_type, + context_length=context_length, + prediction_length=prediction_length, + num_samples=num_samples, + temperature=temperature, + top_k=top_k, + top_p=top_p, + ) + + # Add extra items to model config so that + # it's saved in the ckpt. + model.config.chronos_config = chronos_config.__dict__ + # Create a shuffled training dataset with the + # specified parameters. + shuffled_train_dataset = ChronosDataset( + datasets=train_datasets, + probabilities=probability, + tokenizer=chronos_config.create_tokenizer(), + context_length=context_length, + prediction_length=prediction_length, + min_past=min_past, + mode="training", + ).shuffle(shuffle_buffer_length=shuffle_buffer_length) + + # Define the training arguments. + training_args = TrainingArguments( + output_dir=str(output_dir), + per_device_train_batch_size=per_device_train_batch_size, + learning_rate=learning_rate, + lr_scheduler_type=lr_scheduler_type, + warmup_ratio=warmup_ratio, + optim=optim, + logging_dir=str(output_dir / "train-logs"), + logging_strategy="steps", + logging_steps=log_steps, + save_strategy="steps", + save_steps=save_steps, + report_to=["tensorboard"], + max_steps=max_steps, + gradient_accumulation_steps=gradient_accumulation_steps, + dataloader_num_workers=dataloader_num_workers, + tf32=tf32, # remove this if not using Ampere GPUs (e.g., A100) + torch_compile=torch_compile, + ddp_find_unused_parameters=False, + remove_unused_columns=False, + ) + + # Create a Trainer instance for training the model. + trainer = Trainer( + model=model, + args=training_args, + train_dataset=shuffled_train_dataset, + ) + # Start the training process. + trainer.train() + # Save the trained model to the output directory. + model.save_pretrained(output_dir) + # Return the path to the output directory. + return output_dir From b609b91dd57f38c3594efecd20f4c1d7fc4b93cb Mon Sep 17 00:00:00 2001 From: jsboige Date: Wed, 7 Oct 2026 10:06:15 +0200 Subject: [PATCH 2/4] Fix(qc,#18962): Chronos -- binding du module train dans le shim + fenetre OOS documentaire alignee sur les mesures Bloc [ADJOINT] du 2026-10-07T07:04Z, deux defauts : 1. Binding du port : les assignments `train.logger = getLogger()` / `setLevel(INFO)` (l.267-268) exigeaient le NOM de module `train`, que le shim d'import ne liait pas (seuls ChronosDataset, has_enough_observations et load_model l'etaient) -> NameError a l'execution. Les DEUX branches du shim lient desormais le module : `from chronos.scripts.training import train` cote amont, `import chronos_training as train` cote vendored. 2. Fenetre OOS documentaire : le body et les deux READMEs annoncaient 2022-01-01 -> 2026-01-01 ; les mesures committes (seed-1..42.json, verifiees identiques sur les quatre graines) portent 19 origines trimestrielles 2022-01-03 -> 2026-07-01, chacune evaluee sur son horizon de 63 jours (dernier horizon jusqu'a debut septembre 2026). READMEs FR/EN corriges en distinguant date d'origine et fin d'horizon evalue. Validation post-fix PAR EXECUTION (env bonsai, chronos-forecasting 2.3.2) : - shim reel du fichier execute (branche vendored prise) : train IS chronos_training, trois symboles lies ; - lignes 267-268 executees verbatim : train.logger=root a INFO -- les deux instructions qui levaient NameError pre-fix passent ; - chemin d'entrainement reel mirant la sequence _train (PandasDataset -> Filter/partial -> ChronosDataset -> load_model(chronos-t5-tiny) -> TrainingArguments -> Trainer) : global_step=2, losses 2.600 -> 2.053, save_pretrained OK. Co-Authored-By: Claude Sonnet 5.5 --- .../QuantConnect/projects/ML-Chronos-Foundation/README.en.md | 5 ++++- .../QuantConnect/projects/ML-Chronos-Foundation/README.md | 5 ++++- .../projects/ML-Chronos-Foundation/main_finetuned.py | 4 ++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.en.md b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.en.md index e080a6419e..3a1cb9db97 100644 --- a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.en.md +++ b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.en.md @@ -45,7 +45,10 @@ that makes it measurable on a local GPU: ### Protocol - **Universe**: `AAPL, MSFT, NVDA, AMZN, GOOGL` — a fixed basket of five mega-caps. -- **Training**: 2016-01-01 → 2021-12-31. **Out of sample**: 2022-01-01 → 2026-01-01. +- **Training**: 2016-01-01 → 2021-12-31. **Out of sample**: 19 quarterly origins + 2022-01-03 → 2026-07-01 (first trading day of each quarter from 2022-01-01), each + evaluated on its 63-day forecast horizon — the last evaluated horizon runs into + early September 2026. - Book recipe: `context_length` 126 days, `prediction_length` 63 days, `learning_rate` 1e-5, `adamw_torch_fused`, batch 32, accumulation 2, `tf32` (Ampere), 20 forecast samples per origin. diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.md b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.md index 0ee541c31c..941266f7c4 100644 --- a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.md +++ b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.md @@ -46,7 +46,10 @@ Le ré-entraînement est donc aussi porté **hors** de l'algorithme, par un harn - **Univers** : `AAPL, MSFT, NVDA, AMZN, GOOGL` — panier fixe de cinq méga-capitalisations. -- **Entraînement** : 2016-01-01 → 2021-12-31. **Hors échantillon** : 2022-01-01 → 2026-01-01. +- **Entraînement** : 2016-01-01 → 2021-12-31. **Hors échantillon** : 19 origines + trimestrielles 2022-01-03 → 2026-07-01 (premier jour de bourse de chaque trimestre + depuis 2022-01-01), chacune évaluée sur son horizon de prévision de 63 jours — le + dernier horizon évalué court jusqu'à début septembre 2026. - Recette du livre : `context_length` 126 jours, `prediction_length` 63 jours, `learning_rate` 1e-5, `adamw_torch_fused`, lot 32, accumulation 2, `tf32` (Ampere), 20 échantillons de prévision par origine. diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/main_finetuned.py b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/main_finetuned.py index 62f6aad4e1..66790d6142 100644 --- a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/main_finetuned.py +++ b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/main_finetuned.py @@ -16,7 +16,10 @@ # du wheel PyPI (il vit a la racine du depot amazon-science/chronos-forecasting). # Sur un environnement qui ne l'expose pas, on retombe sur le script vendorise # du projet (finetune/chronos_training.py, v2.3.2, Apache-2.0) — voir README. +# Le livre lie AUSSI le module lui-meme (assignments `train.logger` dans _train) : +# les deux branches lient le nom `train` en plus des trois symboles. try: + from chronos.scripts.training import train from chronos.scripts.training.train import ( ChronosDataset, has_enough_observations, @@ -26,6 +29,7 @@ import sys as _sys _sys.path.insert(0, str(Path(__file__).resolve().parent / "finetune")) + import chronos_training as train # noqa: F401 from chronos_training import ( # noqa: F401 ChronosDataset, has_enough_observations, From e86258cbe08cd76235ceec28f305b79d50809adf Mon Sep 17 00:00:00 2001 From: jsboige Date: Wed, 7 Oct 2026 11:03:54 +0200 Subject: [PATCH 3/4] Fix(qc,#18962): Chronos -- dates exactes du dernier horizon (63 seances), fin septembre et non debut MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clause 1 du bloc adjoint (c6034213278, 2026-10-07T10:44Z) : le body et les deux READMEs disaient « dernier horizon jusqu a debut septembre 2026 ». Recalcul depuis le cache du run (D:\Dev\CoursIA-18962-run\data\closes.csv, 2705 lignes = rows du summary) avec la logique exacte du harnais (quarter_origins + iloc[i:i+63] / valorisation index[i+63]) : - 19 origines confirmees, derniere origine 2026-07-01 ; - dernier horizon (63 SEANCES de bourse, origine incluse) court jusqu au 2026-09-29 (index[i+62], derniere prevision) -- fin septembre, pas debut ; - valorisation du dernier rebalancement strategique : 2026-09-30 (index[i+63]), distincte de la fin d horizon -- la distinction demandee. - « horizon de 63 jours » reformule en « 63 seances » (la recette du livre reste en jours calendaires : frames asfreq(D)). READMEs FR/EN corriges. Body de PR corrige dans le meme geste. Co-Authored-By: Claude Sonnet 5.5 --- .../projects/ML-Chronos-Foundation/README.en.md | 7 +++++-- .../QuantConnect/projects/ML-Chronos-Foundation/README.md | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.en.md b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.en.md index 3a1cb9db97..d5e3bdc2be 100644 --- a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.en.md +++ b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.en.md @@ -47,8 +47,11 @@ that makes it measurable on a local GPU: - **Universe**: `AAPL, MSFT, NVDA, AMZN, GOOGL` — a fixed basket of five mega-caps. - **Training**: 2016-01-01 → 2021-12-31. **Out of sample**: 19 quarterly origins 2022-01-03 → 2026-07-01 (first trading day of each quarter from 2022-01-01), each - evaluated on its 63-day forecast horizon — the last evaluated horizon runs into - early September 2026. + evaluated on its **63-session** forecast horizon (index trading sessions, origin + included) — the last evaluated horizon runs from 2026-07-01 to **2026-09-29** (last + forecast), and the last strategic rebalancement is valued on **2026-09-30**, the + session following the last forecast (harness `index[i+63]`, distinct from the horizon + end `index[i+62]`). - Book recipe: `context_length` 126 days, `prediction_length` 63 days, `learning_rate` 1e-5, `adamw_torch_fused`, batch 32, accumulation 2, `tf32` (Ampere), 20 forecast samples per origin. diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.md b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.md index 941266f7c4..66cdb1d2da 100644 --- a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.md +++ b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/README.md @@ -48,8 +48,11 @@ Le ré-entraînement est donc aussi porté **hors** de l'algorithme, par un harn méga-capitalisations. - **Entraînement** : 2016-01-01 → 2021-12-31. **Hors échantillon** : 19 origines trimestrielles 2022-01-03 → 2026-07-01 (premier jour de bourse de chaque trimestre - depuis 2022-01-01), chacune évaluée sur son horizon de prévision de 63 jours — le - dernier horizon évalué court jusqu'à début septembre 2026. + depuis 2022-01-01), chacune évaluée sur son horizon de prévision de **63 séances** + (séances de bourse de l'index, origine incluse) — le dernier horizon évalué court du + 2026-07-01 au **2026-09-29** (dernière prévision), et la valorisation du dernier + rebalancement stratégique porte sur le **2026-09-30**, la séance qui suit la dernière + prévision (`index[i+63]` du harnais, distincte de la fin d'horizon `index[i+62]`). - Recette du livre : `context_length` 126 jours, `prediction_length` 63 jours, `learning_rate` 1e-5, `adamw_torch_fused`, lot 32, accumulation 2, `tf32` (Ampere), 20 échantillons de prévision par origine. From 03520b7b831fcb6354ef2bd966d6ec179f0d904c Mon Sep 17 00:00:00 2001 From: jsboige Date: Wed, 7 Oct 2026 17:34:00 +0200 Subject: [PATCH 4/4] test(qc,#19622): committer le script de validation du port -- preuve relisible Le journal de validation citee en ligne provenait d'un scratchpad non commite (reserve de l'adjoint). Le script vit desormais dans le projet, resout ses chemins via __file__ et sort ses artefacts en tempfile hors depot. Re-execute depuis le worktree : PASS (shim + binding logger + 2 pas d'entrainement reels, RC=0). Co-Authored-By: Claude Sonnet 5.5 --- .../validate_19622_port.py | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/validate_19622_port.py diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/validate_19622_port.py b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/validate_19622_port.py new file mode 100644 index 0000000000..37c9093fbf --- /dev/null +++ b/MyIA.AI.Notebooks/QuantConnect/projects/ML-Chronos-Foundation/validate_19622_port.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +"""Validation post-fix du port Chronos (main_finetuned.py) PAR EXECUTION. + +Preuve relisible citee par la PR #19622 : ce script s'execute depuis le +dossier du projet et valide les trois points du fix par exécution réelle, +pas par lecture de code : + +1. exec du shim d'import REEL (lignes extraites du fichier committé, pas + recopiées) avec `AlgorithmImports` stubbé (runtime QC indisponible + localement) ; +2. exec verbatim des lignes `train.logger = getLogger()` / + `train.logger.setLevel(INFO)` — les deux instructions qui levaient + NameError pré-fix (méthode de reproduction de l'adjoint, avec + getLogger/INFO réels cette fois) ; +3. chemin d'entraînement REEL mirant la séquence `_train` du port : + PandasDataset -> Filter/partial -> ChronosDataset -> + load_model(chronos-t5-tiny) -> TrainingArguments(max_steps=2) -> + Trainer.train() -> save_pretrained. + +Environnement de référence (run de validation de la PR) : env conda +``bonsai`` — chronos-forecasting 2.3.2, transformers 5.10.2, CPU. + +Usage : + + python validate_19622_port.py + +Sortie 0 = port validé par exécution ; tout échec sort non-nul. Les +artefacts d'entraînement (2 pas, chronos-t5-tiny) vont dans un répertoire +temporaire hors du dépôt, supprimé à la sortie. +""" +import sys +import tempfile +import shutil +from pathlib import Path + +PORT = Path(__file__).resolve().parent / "main_finetuned.py" +OUT = Path(tempfile.mkdtemp(prefix="chrono_val_")) + +try: + src = PORT.read_text(encoding="utf-8") + lines = src.splitlines() + + # --- 1. extraire la région REELLE des imports du port : de "from scipy.optimize" + # (l.4, juste après le stub AlgorithmImports) à "from logging import" incluse -- + # shim ET ses dépendances (Path, torch, transformers, gluonts, chronos) exécutés tels quels. + i_shim = next(i for i, l in enumerate(lines) if l.startswith("from scipy.optimize")) + i_log = next(i for i, l in enumerate(lines) if l.startswith("from logging import")) + shim_region = "\n".join(lines[i_shim:i_log + 1]) + + stub = ( + "import sys, types\n" + "_alg = types.ModuleType('AlgorithmImports')\n" + "_alg.__dict__.update({'QCAlgorithm': object, 'QuantBook': object})\n" + "sys.modules['AlgorithmImports'] = _alg\n" + ) + g = {"__file__": str(PORT), "__name__": "main_finetuned_validation"} + exec(compile(stub + shim_region, str(PORT) + " ", "exec"), g) + + train = g["train"] + import chronos_training # doit être LE même module objet que le shim a lié + assert train is chronos_training, "shim: `train` n'est pas le module vendored" + print("[1] shim OK : branche vendored exécutée, train IS chronos_training (%s)" % train.__name__) + assert callable(g["ChronosDataset"]) and callable(g["has_enough_observations"]) and callable(g["load_model"]) + print("[1] trois symboles liés :", g["ChronosDataset"].__module__, "/", g["load_model"].__module__) + + # --- 2. les deux instructions qui levaient NameError, exécutées telles quelles. + i_logger = next(i for i, l in enumerate(lines) if l.strip() == "train.logger = getLogger()") + i_level = next(i for i, l in enumerate(lines) if l.strip() == "train.logger.setLevel(INFO)") + import textwrap + stmts = textwrap.dedent("\n".join(lines[i_logger:i_level + 1])) + exec(compile(stmts, str(PORT) + " ", "exec"), g) + assert train.logger is not None and train.logger.level == 20 # INFO == 20 + print("[2] lignes du binding logger exécutées verbatim : train.logger = %r (level INFO=%d)" + % (train.logger.name, train.logger.level)) + + # --- 3. chemin d'entraînement réel (séquence _train du port, pas de compilation seule). + import numpy as np + import pandas as pd + from functools import partial + import torch + from transformers import Trainer, TrainingArguments, set_seed + from gluonts.dataset.pandas import PandasDataset + from gluonts.itertools import Filter + from chronos import ChronosConfig + + set_seed(7, True) + idx = pd.date_range("2019-01-01", periods=260, freq="D") + frame = pd.DataFrame({"target": 100.0 + np.cumsum(np.random.default_rng(7).normal(0, 1, 260).cumsum())}, index=idx) + frame.index.name = "time" + + CONTEXT_LENGTH, PREDICTION_LENGTH, MIN_PAST = 126, 63, 64 + prob = [1.0] + tok_kwargs = {"low_limit": -15.0, "high_limit": 15.0} + + train_datasets = [ + Filter( + partial(g["has_enough_observations"], min_length=MIN_PAST + PREDICTION_LENGTH, max_missing_prop=0.9), + PandasDataset(frame, freq="D"), + ) + ] + print("[3] PandasDataset+Filter construits (260 jours synthétiques)") + + model = g["load_model"]( + model_id="amazon/chronos-t5-tiny", model_type="seq2seq", vocab_size=4096, + random_init=False, tie_embeddings=True, pad_token_id=0, eos_token_id=1, + ) + chronos_config = ChronosConfig( + tokenizer_class="MeanScaleUniformBins", tokenizer_kwargs=tok_kwargs, + n_tokens=4096, n_special_tokens=2, pad_token_id=0, eos_token_id=1, + use_eos_token=True, model_type="seq2seq", + context_length=CONTEXT_LENGTH, prediction_length=PREDICTION_LENGTH, + num_samples=20, temperature=1.0, top_k=50, top_p=1.0, + ) + model.config.chronos_config = chronos_config.__dict__ + shuffled = g["ChronosDataset"]( + datasets=train_datasets, probabilities=prob, + tokenizer=chronos_config.create_tokenizer(), + context_length=CONTEXT_LENGTH, prediction_length=PREDICTION_LENGTH, + min_past=MIN_PAST, mode="training", + ).shuffle(shuffle_buffer_length=10) + print("[3] load_model + ChronosDataset OK (device=%s, tf32=%s)" % (torch.get_default_device(), torch.backends.cuda.matmul.allow_tf32)) + + args = TrainingArguments( + output_dir=str(OUT), per_device_train_batch_size=4, learning_rate=1e-5, + lr_scheduler_type="linear", warmup_ratio=0.0, optim="adamw_torch_fused", + logging_strategy="steps", logging_steps=1, save_strategy="no", + report_to=[], max_steps=2, gradient_accumulation_steps=1, + dataloader_num_workers=0, tf32=True, torch_compile=False, + ddp_find_unused_parameters=False, remove_unused_columns=False, + ) + trainer = Trainer(model=model, args=args, train_dataset=shuffled) + result = trainer.train() + model.save_pretrained(OUT) + steps = int(result.global_step) + assert steps >= 2, "trainer n'a pas atteint 2 pas" + losses = [h.get("loss") for h in trainer.state.log_history if h.get("loss") is not None] + print("[3] Trainer.train() REEL : global_step=%d, losses=%s" % (steps, ["%.3f" % l for l in losses])) + print("[3] save_pretrained -> %s (config.json=%s)" % (OUT, (OUT / "config.json").exists())) + + print("\nVALIDATION PORT #19622 : PASS (shim exécuté + binding logger exécuté + chemin d'entraînement réel 2 pas)") +finally: + shutil.rmtree(OUT, ignore_errors=True)