From 4c0e3ba39ae40aebf342f79f24af16d7413d925c Mon Sep 17 00:00:00 2001 From: Your Date: Wed, 7 Oct 2026 10:33:37 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat(qc,#19680):=20port=20CAPM=20alpha=20ra?= =?UTF-8?q?nking=20Dow=2030=20=E2=80=94=20NO=20BEATS=20=C3=973,=20levier?= =?UTF-8?q?=202=20non=20tenu=20sous=20Lean?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port de l'article QuantConnect Research 15345 (regression inversee du code publie conservee, bras A ; sens CAPM, bras B ; variante 0,5 par ligne, bras C). Mesure preinscrite sur l'issue : NO BEATS pour les trois bras contre SPY (p Holm 1,00). Les journaux montrent des achats refuses a levier 2 en resolution journaliere ; le parametre timing=intent, ajoute apres coup et mesure hors verdict, ne recupere pas l'exposition et fait moins bien. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../CAPMAlphaRankingDow30-QC/README.md | 149 +++++++++++++ .../CAPMAlphaRankingDow30-QC/config.json | 3 + .../projects/CAPMAlphaRankingDow30-QC/main.py | 201 ++++++++++++++++++ 3 files changed, 353 insertions(+) create mode 100644 MyIA.AI.Notebooks/QuantConnect/projects/CAPMAlphaRankingDow30-QC/README.md create mode 100644 MyIA.AI.Notebooks/QuantConnect/projects/CAPMAlphaRankingDow30-QC/config.json create mode 100644 MyIA.AI.Notebooks/QuantConnect/projects/CAPMAlphaRankingDow30-QC/main.py diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/CAPMAlphaRankingDow30-QC/README.md b/MyIA.AI.Notebooks/QuantConnect/projects/CAPMAlphaRankingDow30-QC/README.md new file mode 100644 index 0000000000..165655aeaa --- /dev/null +++ b/MyIA.AI.Notebooks/QuantConnect/projects/CAPMAlphaRankingDow30-QC/README.md @@ -0,0 +1,149 @@ +# CAPMAlphaRankingDow30-QC + +**Classe d'actifs :** Actions US (composantes du Dow Jones) +**ID projet Cloud :** 37467233 (mesure #19680) + +## Description + +Port de l'article *CAPM Alpha Ranking Strategy On Dow 30 Companies* (Jing Wu, QuantConnect Research 15345, encore au statut de brouillon le 2026-10-07), avec la mise à jour proposée par Derek Melchin (équipe QuantConnect) dans le fil de l'article. + +Au premier jour de bourse de chaque mois, `main.py` : + +1. lit 21 clôtures quotidiennes des 30 titres et de SPY ; +2. régresse, pour chaque titre, les rendements de l'un sur ceux de l'autre (`np.linalg.lstsq`) et retient la constante de la régression comme « alpha » ; +3. garde les deux titres au plus grand alpha, vend les autres positions et place 100 % du capital sur **chacun** des deux. + +Le portefeuille de l'article est donc à levier 2, sur deux actions, sans couverture ni stop. La thèse annoncée est un momentum d'un mois : un titre qui a battu le marché le mois passé continuerait à le battre. + +## Ce que le code publié calcule : une régression inversée + +La prose de l'article annonce la régression du CAPM : le rendement de l'action expliqué par celui du marché, de pente β et de constante α. Le code fait l'inverse : + +```python +returns = np.vstack([returns, np.ones(len(returns))]).T +result = np.linalg.lstsq(returns, benchmark) # benchmark ≈ pente · action + constante +alphas[symbol] = result[0][1] +``` + +La matrice de régression porte les rendements de l'**action** et la cible est le **benchmark**. La constante retenue est donc celle du marché régressé sur l'action, et non l'alpha de l'action. Le port reprend ce code tel quel (bras A, `regression=article`) et mesure à côté la régression dans le sens du CAPM (bras B, `regression=capm`). + +## Port : ce qui change et ce qui ne change pas + +Ce qui est repris de l'article sans changement : + +- la régression, le lookback de 21 séances, la sélection des deux premiers et l'exposition de 1,0 par ligne ; +- le rééquilibrage par `month_start` / `after_market_open` ; +- la vente des seules positions qui ne sont plus retenues (`liquidate`, puis `set_holdings`). + +Ce qui change : + +- **Noms d'API.** Le code publié est écrit pour une version antérieure de l'API Python de Lean. Deux noms ne passent plus en snake_case : `self.benchmark`, aujourd'hui pris par `QCAlgorithm.benchmark` (l'initialisation échoue), devient `self._benchmark` ; `self.portfolio.values` devient la méthode `self.portfolio.values()`. Deux backtests ont échoué sur ces deux noms avant le premier run mesuré ; leurs identifiants sont conservés dans les traces. +- **Courtier.** Le port tourne avec les frais IBKR, en compte sur marge, et un capital de 1 M$. +- **Initialiseur.** Il vient de la mise à jour de l'équipe QuantConnect : chaque titre reçoit son dernier prix connu dès son ajout. Il n'a pas d'effet sur la règle. +- **Instrumentation de mesure.** Un graphique `shadow` porte la valeur du portefeuille à chaque clôture, les frais cumulés et la rotation. Les appels de marge et leurs dates sont comptés. Chaque sélection est écrite au journal (`SEL ...`). +- **Paramètre `timing`, ajouté après la mesure des bras préinscrits.** Par défaut (`article`), ventes et achats partent le même jour, comme dans le code publié. Avec `intent`, les achats attendent la séance suivante. La raison est donnée dans « Exposition effective » ci-dessous. + +Paramètres (défauts = code de l'article) : + +| Paramètre | Valeurs | Rôle | +|---|---|---| +| `regression` | `article` (défaut), `capm` | sens de la régression | +| `exposure` | 1,0 (défaut) | poids de chacune des deux lignes ; 0,5 pour la variante de l'équipe QuantConnect | +| `mode` | `strategy` (défaut), `spy` | `spy` détient SPY à 100 % sur le même harnais, comme référence | +| `fee_mult` | 1 (défaut) | multiplicateur des frais IBKR | +| `timing` | `article` (défaut), `intent` | `intent` achète à la séance qui suit les ventes | +| `start`, `end` | 2015-03-19, 2026-09-30 | fenêtre | + +## Univers figé + +Le texte de l'article ne donne pas la liste des 30 titres ; elle ne figure que dans son backtest joint. Le port utilise la composition du Dow Jones au 2015-03-19, date de l'entrée d'Apple dans l'indice, la plus tardive de la liste. La fenêtre principale commence ce jour-là, comme dans l'article. + +La liste est connue à la date de départ : elle n'anticipe rien sur la fenêtre principale. En revanche, elle ne suit pas l'indice ensuite. Plusieurs titres l'ont quitté depuis, comme General Electric en 2018, et ses entrants ne sont jamais achetés. Le portefeuille ne mesure donc pas « le Dow 30 », mais 30 grandes capitalisations américaines choisies en 2015. + +Le run `y2015` commence le 2015-01-01 pour comparer l'année civile 2015 à l'article. Sur ses onze premières semaines, la liste contient Apple, qui n'était pas encore dans l'indice, à la place d'AT&T. Sur ces semaines, ce run connaît donc la suite. + +## Sources + +- **Article** : Jing Wu, *CAPM Alpha Ranking Strategy On Dow 30 Companies*, QuantConnect Research 15345 (brouillon), +- **Théorie du CAPM** : William F. Sharpe, *Capital Asset Prices with and without Negative Holdings*, Nobel Lecture, 7 décembre 1990. Copie : `G:\Mon Drive\MyIA\IA\Bibliographie IA\Trading\1990 - Sharpe - Capital Asset Prices with and without Negative Holdings (Nobel Lecture).pdf`. +- **Aucune source primaire ne teste le signal.** Sharpe fonde le modèle, pas cette règle de sélection. L'article ne cite aucune étude du classement mensuel par alpha sur 21 séances. L'horizon d'un mois va plutôt à l'encontre de la littérature : le momentum classique sur actions se mesure sur 12 mois en sautant le dernier, et le dernier mois tend à s'inverser. + +## Mesure (#19680) + +La règle de verdict a été inscrite sur l'issue avant le premier backtest ([commentaire de préinscription](https://github.com/jsboige/CoursIA/issues/19680#issuecomment-6033649014)). Elle est appliquée ici sans changement. + +**Protocole.** Fenêtre principale du 2015-03-19 au 2026-09-30, soit 2 900 séances, communes à tous les passages. La statistique est l'écart de Sharpe journalier entre le bras et SPY détenu sur le même harnais, sans taux sans risque. Le p unilatéral vient d'un bootstrap circulaire par blocs, corrigé par Holm sur les trois bras. Un bras ne gagne (**BEATS**) que s'il passe les trois conditions : + +- p Holm < 0,05 ; +- écart positif avec des frais doublés ; +- écart positif sur au moins deux des trois sous-fenêtres. + +### Résultats + +| Passage | Sharpe (sans taux) | Sharpe QC | CAGR | Drawdown max | Rotation / an | Frais / an, en part du capital initial | Ordres | +|---|---|---|---|---|---|---|---| +| `article` (bras A, régression publiée, 1,0 par ligne) | 0,53 | 0,35 | 11,4 % | −51,0 % | 19,2 | 0,18 % | 386 | +| `capm` (bras B, sens du CAPM, 1,0 par ligne) | 0,64 | 0,47 | 16,6 % | −53,5 % | 19,9 | 0,21 % | 391 | +| `staff` (bras C, régression publiée, 0,5 par ligne) | 0,50 | 0,32 | 10,0 % | −47,8 % | 22,0 | 0,28 % | 527 | +| `spy` (référence) | 0,82 | 0,54 | 13,7 % | −33,8 % | 0,09 | 0 | 1 | + +Le « Sharpe QC » est celui des statistiques de QuantConnect, calculé avec un taux sans risque. La rotation compte la valeur échangée, rapportée à la valeur du portefeuille. + +### Verdict + +| Bras | Écart de Sharpe contre SPY | IC 95 % | p Holm | 2015-2018 | 2019-2022 | 2023-2026 | Verdict | +|---|---|---|---|---|---|---|---| +| `article` | −0,29 | [−0,94 ; 0,33] | 1,00 | −0,61 | −0,20 | −0,28 | **NO BEATS** | +| `capm` | −0,18 | [−0,91 ; 0,45] | 1,00 | −0,42 | −0,16 | −0,32 | **NO BEATS** | +| `staff` | −0,32 | [−0,76 ; 0,08] | 1,00 | +0,03 | −0,45 | −0,50 | **NO BEATS** | + +Les trois écarts sont négatifs. Aucun bras n'étant significatif, la règle ne demande pas de passage à frais doublés et aucun n'a été lancé. Le CAGR de `capm` dépasse celui de SPY, mais avec un drawdown de −53,5 % : par unité de risque, il fait moins bien que SPY. + +### Exposition effective : le levier 2 n'est pas tenu + +Les journaux des bras `article` et `capm` portent des refus d'ordres pour pouvoir d'achat insuffisant. Le bras `staff`, à 0,5 par ligne, n'en porte aucun. + +| Passage | Rééquilibrages | 2 ordres refusés | 1 ordre refusé | sans refus | +|---|---|---|---|---| +| `article` | 138 | 57 | 41 | 40 | +| `capm` | 138 | 57 | 38 | 43 | +| `staff` | 138 | 0 | 0 | 138 | + +En résolution journalière, un ordre au marché passé en séance s'exécute à la clôture. Le jour du rééquilibrage, la vente des titres qui ne sont plus retenus n'est donc pas encore exécutée quand partent les achats : les anciennes lignes occupent toujours la marge. À levier 2, l'achat d'un nouveau titre est alors refusé, et la vente, elle, s'exécute. Le bras `article` passe ainsi 56 mois sur 138 entièrement en liquidités, et 41 sur une seule ligne. Le 57ᵉ mois à deux refus garde une ligne presque pleine : l'écart à la cible y est de 1,04 ligne au total. + +Les bras A et B mesurent donc le code tel qu'il tourne aujourd'hui sur Lean, et non la règle à levier 2 que décrit l'article. Leur verdict NO BEATS vaut pour ce code. Seul le bras C, sans refus, mesure la règle telle qu'elle est écrite, à levier 1. Il ne bat pas SPY non plus. + +**Bras ajoutés après coup (`timing=intent`).** Ces deux passages ont été inscrits sur l'issue avant d'être lancés ([commentaire d'ajout](https://github.com/jsboige/CoursIA/issues/19680#issuecomment-6034039690)). Ils sont descriptifs, hors verdict et hors Holm. Les ventes partent toujours le jour du rééquilibrage, mais les achats attendent la séance suivante, une fois les ventes exécutées. + +| Passage | Sharpe (sans taux) | CAGR | Drawdown max | Écart contre SPY | IC 95 % | p brut | Mois avec un achat refusé | +|---|---|---|---|---|---|---|---| +| `intent_article` | 0,29 | 2,5 % | −74,6 % | −0,53 | [−1,04 ; −0,08] | 0,99 | 69 sur 138 | +| `intent_capm` | 0,50 | 12,7 % | −62,2 % | −0,32 | [−0,97 ; 0,28] | 0,84 | 71 sur 138 | + +Même une fois les ventes exécutées, le modèle IBKR de Lean refuse une ligne entière un mois sur deux : deux lignes à 1,0 dépassent d'un rien le pouvoir d'achat. Ces bras ne passent plus de mois en liquidités, mais leur exposition alterne entre deux lignes et une. + +Mieux investie, la règle publiée fait moins bien : le Sharpe de `intent_article` tombe à 0,29, son drawdown atteint −74,6 %, et tout son intervalle contre SPY est négatif. Sur cette fenêtre, être davantage investi dans la règle a coûté. Aucun des cinq passages de stratégie ne bat SPY. Le bras C, à exposition constante et sans refus, reste la mesure la plus propre de la règle. + +### Confrontations à l'article + +Ces lectures sont descriptives : elles n'entrent pas dans le verdict. + +1. **Rendement 2015.** La règle nomme deux comparaisons au −15,463 % publié, et elles ne concluent pas pareil : + + | Comparaison | Rendement mesuré | Écart au chiffre publié | Lecture (seuil de 3 points) | + |---|---|---|---| + | `y2015`, année civile | −27,9 % | −12,5 points | ÉCART | + | `article`, du 2015-03-19 au 2015-12-31 | −17,7 % | −2,3 points | dans le seuil | + + Le passage `y2015` porte aussi, sur ses onze premières semaines, une liste qui n'est pas celle du Dow de l'époque (voir « Univers figé »). +2. **Appel de marge.** L'article signale un appel de marge en janvier. Aucun bras préinscrit n'en reçoit, ni en 2015 (`y2015`) ni en 2016 (`article`) : le compteur reste à 0 sur toute la fenêtre. C'est un **ÉCART**. Sous le modèle de marge IBKR de Lean, le levier 2 bute à la place sur les refus d'achat décrits plus haut. Le passage `y2015` en compte 15, sur 10 des 12 rééquilibrages de l'année. Les bras `intent` ne reçoivent pas non plus d'appel de marge : sous ce modèle de courtier, l'ordre qui dépasserait la marge est refusé à l'envoi, et l'appel de marge publié ne se reproduit pas. +3. **Régression inversée.** Sur 138 rééquilibrages, la paire choisie par `article` et celle choisie par `capm` diffèrent 137 fois, et n'ont aucun titre en commun 129 fois. Les deux sens de régression choisissent donc presque toujours des titres différents. L'écart de Sharpe entre les deux bras (`capm` − `article`) vaut +0,11, IC 95 % [−0,73 ; 0,91], p unilatéral 0,39 : le choix du sens ne change pas le résultat de façon mesurable. + +### Limites + +- **Univers figé.** La liste de 2015 ne suit pas l'indice ensuite. Elle garde des titres qui en sont sortis et n'achète jamais ses entrants. +- **Levier nominal 2 sur deux titres.** Le résultat tient à quelques titres par mois. Le drawdown maximal dépasse −47 % dans tous les passages de stratégie, et atteint −74,6 % pour `intent_article`, contre −33,8 % pour SPY. +- **Capacité.** QuantConnect estime la capacité à 71 M$ pour `article`, 850 M$ pour `capm` et 97 M$ pour `staff`. +- **Paramètres de l'article.** Le lookback de 21 séances et la sélection de deux titres sont repris tels quels. Aucun autre jeu de paramètres n'a été essayé : la mesure porte sur la règle publiée, pas sur une variante réglée après coup. + +Les traces de chaque passage sont rangées hors dépôt, dans `G:\Mon Drive\MyIA\IA\QC-traces\19680-capm-alpha-ranking` : plans, identifiants de backtest, graphiques `shadow`, journaux complets et de sélection, et `results.json`. On y trouve aussi les deux backtests en échec, les scripts d'analyse et la version de `main.py` qui a tourné pour les bras préinscrits (empreinte `aac9ea8b90e8`, avant l'ajout de `timing`, sans autre changement). Ces scripts s'appuient sur `shadow_replay`, `strategy_metrics` et `voltarget_strategy_verdict` de `ML-Training-Pipeline/scripts`. diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/CAPMAlphaRankingDow30-QC/config.json b/MyIA.AI.Notebooks/QuantConnect/projects/CAPMAlphaRankingDow30-QC/config.json new file mode 100644 index 0000000000..de428113e8 --- /dev/null +++ b/MyIA.AI.Notebooks/QuantConnect/projects/CAPMAlphaRankingDow30-QC/config.json @@ -0,0 +1,3 @@ +{ + "local-id": 37467233 +} diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/CAPMAlphaRankingDow30-QC/main.py b/MyIA.AI.Notebooks/QuantConnect/projects/CAPMAlphaRankingDow30-QC/main.py new file mode 100644 index 0000000000..9615b54034 --- /dev/null +++ b/MyIA.AI.Notebooks/QuantConnect/projects/CAPMAlphaRankingDow30-QC/main.py @@ -0,0 +1,201 @@ +#region imports +from AlgorithmImports import * +from datetime import datetime +import numpy as np +#endregion +# CAPM Alpha Ranking Strategy On Dow 30 Companies -- Jing Wu, QuantConnect Research 15345 +# https://www.quantconnect.com/research/15345/capm-alpha-ranking-strategy-on-dow-30-companies/ +# Mise a jour STAFF (Derek Melchin) : exposition reduite, initialiseur de titres. +# +# Port du code publie (#19680). La regression est reprise TELLE QUELLE : le code publie +# resout `benchmark ~ pente * action + constante` (np.linalg.lstsq(returns, benchmark)), +# alors que la prose de l'article annonce l'inverse (action sur benchmark). La constante +# retenue comme « alpha » est donc celle du benchmark regresse sur l'action. +# +# Parametres de mesure (defauts = code de l'article) : +# regression = article (sens publie) | capm (action regressee sur le benchmark, sens CAPM) +# exposure = poids de chacune des deux lignes (1.0 dans l'article, levier 2 ; reduit +# dans la mise a jour STAFF) +# mode = strategy | spy (SPY detenu a 100 %, reference sur le meme harnais) +# fee_mult = multiplicateur des frais IBKR (1 = inchanges) +# timing = article (ventes et achats le meme jour, comme le code publie) | intent +# (achats a la seance suivante). En resolution journaliere, la vente part a la +# cloture : le jour du reequilibrage, la marge des anciennes lignes n'est pas +# encore liberee et, a levier 2, les achats de nouveaux titres sont refuses +# (pouvoir d'achat insuffisant). `intent` attend que les ventes soient +# executees pour acheter, et vise le levier que la regle annonce. +# start, end = fenetre ; le code de l'article ne peut pas commencer avant le 2015-03-19 +# (derniere entree au Dow de sa liste figee, AAPL). +# +# La liste des 30 titres n'est pas dans le texte de l'article (seulement dans son backtest +# joint) : c'est la composition du Dow Jones au 2015-03-19, figee. Biais du survivant +# assume : la liste ne suit pas les sorties de l'indice apres cette date. +# Valeur du portefeuille a chaque cloture dans le graphique "shadow" (contrat de rejeu en +# ombre, #18923). + +DOW_2015 = ["MMM", "AXP", "AAPL", "BA", "CAT", "CVX", "CSCO", "KO", "DIS", "DD", + "XOM", "GE", "GS", "HD", "IBM", "INTC", "JNJ", "JPM", "MCD", "MRK", + "MSFT", "NKE", "PFE", "PG", "TRV", "UTX", "UNH", "VZ", "V", "WMT"] + + +class _ScaledFeeModel(FeeModel): + """Frais du courtier (modele par defaut de Lean), mis a l'echelle (identite a 1.0).""" + + def __init__(self, multiplier): + self._multiplier = multiplier + self._base = InteractiveBrokersFeeModel() + + def get_order_fee(self, parameters): + fee = self._base.get_order_fee(parameters) + if fee is None or self._multiplier == 1.0: + return fee + amount = float(fee.value.amount) * self._multiplier + return OrderFee(CashAmount(amount, fee.value.currency)) + + +class CAPMAlphaRankingDow30(QCAlgorithm): + + def initialize(self): + start = datetime.strptime(self.get_parameter("start") or "2015-03-19", "%Y-%m-%d") + end = datetime.strptime(self.get_parameter("end") or "2026-09-30", "%Y-%m-%d") + self.set_start_date(start.year, start.month, start.day) + self.set_end_date(end.year, end.month, end.day) + self.start_value = 1000000.0 + self.set_cash(self.start_value) + self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN) + + self.regression = self.get_parameter("regression") or "article" + self.exposure = float(self.get_parameter("exposure") or 1.0) + self.mode = self.get_parameter("mode") or "strategy" + self.fee_mult = float(self.get_parameter("fee_mult") or 1) + self.timing = self.get_parameter("timing") or "article" + if (self.regression not in ("article", "capm") or self.mode not in ("strategy", "spy") + or self.timing not in ("article", "intent")): + raise ValueError(f"parametres inconnus : {self.regression}/{self.mode}/{self.timing}") + + # Initialiseur de la mise a jour STAFF : prix amorces des l'ajout du titre, pour que + # le premier set_holdings ne bute pas sur un prix nul. Sans effet sur la regle. + seeder = FuncSecuritySeeder(self.get_last_known_prices) + base_init = BrokerageModelSecurityInitializer(self.brokerage_model, seeder) + fee_mult = self.fee_mult + + def init(security): + base_init.initialize(security) + if fee_mult != 1.0: + security.set_fee_model(_ScaledFeeModel(fee_mult)) + + self.set_security_initializer(init) + + self.lookback = 21 + self.symbols = [self.add_equity(t, Resolution.DAILY).symbol for t in DOW_2015] + # `self.benchmark` dans l'article : sous l'API snake_case, le nom est pris par + # QCAlgorithm.benchmark (IBenchmark) et l'initialisation echoue. Seul le nom change. + self._benchmark = self.add_equity("SPY", Resolution.DAILY).symbol + + if self.mode == "strategy": + self.schedule.on(self.date_rules.month_start(self.symbols[0]), + self.time_rules.after_market_open(self.symbols[0]), + self.rebalance) + if self.timing == "intent": + self.schedule.on(self.date_rules.every_day(self.symbols[0]), + self.time_rules.after_market_open(self.symbols[0]), + self.buy_pending) + + self._pending = None + self._rebalance_day = None + self.closes = 0 + self.traded = 0.0 + self.margin_calls = 0 + self.margin_call_days = [] + self.rebalances = 0 + + def select_symbols(self, history): + '''Select symbols with the highest intercept/alpha to the benchmark + ''' + alphas = dict() + + # Get the benchmark returns + benchmark = history[self._benchmark].pct_change().dropna() + + # Conducts linear regression for each symbol and save the intercept/alpha + for symbol in self.symbols: + if symbol not in history.columns: + continue + + # Get the security returns + returns = history[symbol].pct_change().dropna() + if len(returns) != len(benchmark): + continue + + if self.regression == "article": + # Code publie : benchmark regresse sur l'action. + returns = np.vstack([returns, np.ones(len(returns))]).T + result = np.linalg.lstsq(returns, benchmark, rcond=None) + else: + # Sens CAPM de la prose : action regressee sur le benchmark. + design = np.vstack([benchmark, np.ones(len(benchmark))]).T + result = np.linalg.lstsq(design, returns, rcond=None) + alphas[symbol] = result[0][1] + + # Select symbols with the highest intercept/alpha to the benchmark + selected = sorted(alphas.items(), key=lambda x: x[1], reverse=True)[:2] + return [x[0] for x in selected] + + def rebalance(self): + + # Fetch the historical data to perform the linear regression + history = self.history( + self.symbols + [self._benchmark], + self.lookback, + Resolution.DAILY).close.unstack(level=0) + + symbols = self.select_symbols(history) + self.rebalances += 1 + self._rebalance_day = self.time.date() + self.log("SEL " + ",".join(s.value for s in symbols)) + + # Liquidate positions that are not held by selected symbols + for holdings in self.portfolio.values(): + symbol = holdings.symbol + if symbol not in symbols and holdings.invested: + self.liquidate(symbol) + + if self.timing == "intent": + self._pending = symbols + return + + # Invest in each of the selected symbols (100 % in the article) + for symbol in symbols: + self.set_holdings(symbol, self.exposure) + + def buy_pending(self): + # Seance suivant le reequilibrage : les ventes ont ete executees a la cloture d'hier. + if not self._pending or self.time.date() == self._rebalance_day: + return + self.set_holdings([PortfolioTarget(s, self.exposure) for s in self._pending]) + self._pending = None + + def on_margin_call(self, requests): + self.margin_calls += 1 + day = str(self.time.date()) + if day not in self.margin_call_days: + self.margin_call_days.append(day) + return requests + + def on_order_event(self, event): + if event.status in (OrderStatus.FILLED, OrderStatus.PARTIALLY_FILLED): + self.traded += abs(event.fill_quantity * event.fill_price) / self.portfolio.total_portfolio_value + + def on_data(self, data): + if self.mode == "spy" and not self.portfolio.invested and data.bars.contains_key(self._benchmark): + self.set_holdings(self._benchmark, 1.0) + if data.bars.contains_key(self._benchmark): + self.plot("shadow", f"e{self.closes % 5}", self.portfolio.total_portfolio_value) + self.plot("shadow", "fees", self.portfolio.total_fees / self.start_value) + self.plot("shadow", "turnover", self.traded) + self.closes += 1 + + def on_end_of_algorithm(self): + self.set_runtime_statistic("margin_calls", str(self.margin_calls)) + self.set_runtime_statistic("margin_call_days", ",".join(self.margin_call_days[:12]) or "-") + self.set_runtime_statistic("rebalances", str(self.rebalances)) From 3ea8a1fd3ee47598e341056c62b73b49a3e5e64e Mon Sep 17 00:00:00 2001 From: Your Date: Wed, 7 Oct 2026 13:57:40 +0200 Subject: [PATCH 2/2] Fix(qc,#19680): README CAPM -- reformuler l'ecart a la cible du 57e mois (faux positif prose-counts) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../QuantConnect/projects/CAPMAlphaRankingDow30-QC/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/CAPMAlphaRankingDow30-QC/README.md b/MyIA.AI.Notebooks/QuantConnect/projects/CAPMAlphaRankingDow30-QC/README.md index 165655aeaa..89d8d7ee75 100644 --- a/MyIA.AI.Notebooks/QuantConnect/projects/CAPMAlphaRankingDow30-QC/README.md +++ b/MyIA.AI.Notebooks/QuantConnect/projects/CAPMAlphaRankingDow30-QC/README.md @@ -109,7 +109,7 @@ Les journaux des bras `article` et `capm` portent des refus d'ordres pour pouvoi | `capm` | 138 | 57 | 38 | 43 | | `staff` | 138 | 0 | 0 | 138 | -En résolution journalière, un ordre au marché passé en séance s'exécute à la clôture. Le jour du rééquilibrage, la vente des titres qui ne sont plus retenus n'est donc pas encore exécutée quand partent les achats : les anciennes lignes occupent toujours la marge. À levier 2, l'achat d'un nouveau titre est alors refusé, et la vente, elle, s'exécute. Le bras `article` passe ainsi 56 mois sur 138 entièrement en liquidités, et 41 sur une seule ligne. Le 57ᵉ mois à deux refus garde une ligne presque pleine : l'écart à la cible y est de 1,04 ligne au total. +En résolution journalière, un ordre au marché passé en séance s'exécute à la clôture. Le jour du rééquilibrage, la vente des titres qui ne sont plus retenus n'est donc pas encore exécutée quand partent les achats : les anciennes lignes occupent toujours la marge. À levier 2, l'achat d'un nouveau titre est alors refusé, et la vente, elle, s'exécute. Le bras `article` passe ainsi 56 mois sur 138 entièrement en liquidités, et 41 sur une seule ligne. Le 57ᵉ mois à deux refus garde une ligne presque pleine : l'écart à la cible y vaut 1,04 fois le poids d'une ligne. Les bras A et B mesurent donc le code tel qu'il tourne aujourd'hui sur Lean, et non la règle à levier 2 que décrit l'article. Leur verdict NO BEATS vaut pour ce code. Seul le bras C, sans refus, mesure la règle telle qu'elle est écrite, à levier 1. Il ne bat pas SPY non plus.