Signal
Quant & research
Des systèmes de recherche qui ne se mentent pas à eux-mêmes : ingestion read-only, classification de régime, Market Memory et calibration. La colonne vertébrale de XCAP.
SG-01 Ingestion read-only & edge sans clés
leçon Construire un edge d'ingestion en lecture seule qui rapatrie sur disque des données publiques du monde sans jamais exposer la moindre credential, et qui est structurellement incapable d'opérer ou de déplacer de l'argent.
Ingestion read-only & edge sans clés
leçonConstruire un edge d'ingestion en lecture seule qui rapatrie sur disque des données publiques du monde sans jamais exposer la moindre credential, et qui est structurellement incapable d'opérer ou de déplacer de l'argent.
Un système de research qui touche le réseau de partout est un système qui peut faire fuir des clés, exécuter des trades par accident et contaminer ses résultats avec des données non reproductibles. L'edge keyless est la frontière où le monde entre sans pouvoir faire de dégâts.
LA LEÇON
L'intuition centrale de XCAP est brutale et peu intuitive pour qui vient du SaaS : dans un système de recherche en trading, le réseau n'est pas une commodité, c'est la plus grande surface d'attaque que tu possèdes. Chaque endpoint avec clé est une credential qui peut fuir ; chaque appel en écriture est un trade qui peut se déclencher à cause d'un bug. C'est pourquoi XCAP réduit toute son exposition réseau à un seul point : un edge d'ingestion en lecture seule, keyless et GET-only. Le reste du système est déterministe depuis le disque. Si tu comprends ça, tu comprends pourquoi `binance_public.py` ne demande que des klines (bougies OHLCV historiques) par HTTP GET, sans API key, sans rien signer, sans jamais toucher un endpoint de compte ou d'ordre.
Concrètement, le pattern est le suivant : une source abstraite (chez XCAP, une hiérarchie `OHLCSource`) avec deux implémentations. `LocalCSVSource` lit des CSV déjà présents sur disque — zéro réseau, le cas par défaut, celui qu'utilisent TOUTES les boucles automatiques. Et `HttpOHLCSource` (adossé à `binance_public.py`) qui ne s'active que lorsqu'un appelant passe explicitement `allow_network=True` ou invoque la commande `data-fetch`. L'asymétrie est délibérée : le chemin sûr est le défaut et le chemin réseau exige une option explicite à chaque appel. Jamais un flag global ambiant, jamais une variable d'environnement qui reste allumée.
L'endpoint Binance que tu utilises est le public de marché : `GET https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1d&limit=1000`. Il renvoie un tableau de tableaux : `[openTime, open, high, low, close, volume, closeTime, ...]`. Regarde ce qu'il n'y a PAS ici : pas de header `X-MBX-APIKEY`, pas de query param de signature HMAC, pas de timestamp signé. Cet endpoint ne peut physiquement pas lire ton solde ni placer un ordre, parce que l'API de compte de Binance vit sur une autre route et exige une signature. La sécurité n'est pas une promesse de bien te comporter : c'est une propriété de l'endpoint que tu as choisi.
Le deuxième pilier, c'est la normalisation au bord. Les données du monde réel arrivent sales : timestamps en millisecondes, nombres sous forme de strings, bougies dupliquées, trous. Ton edge doit normaliser vers UN format canonique avant que quoi que ce soit en aval n'y touche. Chez XCAP les CSV de `data/real` sont en minuscules (`open,high,low,close,volume`), les timestamps parsés en dates, et `LocalCSVSource` les lit toujours de la même façon. Cette discipline est ce qui rend le système reproductible : le classificateur de régime (SG-02) et la Market Memory (SG-03) lisent depuis le disque un format stable, pas une réponse HTTP volatile qui change d'une exécution à l'autre.
La vérification, c'est le verrou final. XCAP a une commande `check` (qui tourne avec `PYTHONPATH=src python3 -m xcap.control check`) qui affirme comme invariant que le connecteur est keyless, en lecture seule, ne peut pas opérer et ne peut pas déplacer d'argent. Ce n'est pas un test qui passe une fois : c'est une assertion qui tourne à chaque `check`, de sorte que si quelqu'un ajoute une clé ou une méthode d'écriture, le `check` passe au rouge. C'est comme ça que tu transformes une intention de design en une propriété que le système défend tout seul. La règle d'or : la sécurité qui dépend du fait que tu te souviennes de quelque chose n'est pas de la sécurité ; la sécurité qu'un test rouge t'oblige à respecter, oui.
Une nuance opérationnelle importante : le dashboard de XCAP se bind uniquement sur `127.0.0.1`, jamais sur `0.0.0.0`. L'edge d'ingestion est la seule surface de sortie ; celle d'entrée (le dashboard) n'écoute pas sur le réseau. Et FileVault (AES-256) chiffre le disque au repos. La chaîne complète — disque chiffré, ingest keyless en lecture seule, dashboard local-only, Capital Gate fermé — c'est ce qui permet d'avoir un système de trading-research sur un portable sans que ce soit une bombe à retardement.
EXERCICE
Implémente une source de données avec deux backends. Crée `ohlc_source.py` avec une classe de base `OHLCSource` (méthode `get(symbol, interval, limit) -> list[Bar]`). Implémente `LocalCSVSource` (lit depuis `./data/{symbol}.csv`, sans réseau) et `HttpOHLCSource` (GET vers `api.binance.com/api/v3/klines`, sans aucune API key). Le constructeur de `HttpOHLCSource` doit recevoir `allow_network: bool = False` et lever `RuntimeError('network not allowed')` si `get()` est appelé avec `allow_network=False`. Normalise les deux vers la même structure `Bar(time, open, high, low, close, volume)` avec des floats et une date parsée. Écris `check_connector()` qui vérifie par introspection que la classe HTTP n'a aucune méthode ni attribut dont le nom contient 'order', 'trade', 'sign', 'key', 'secret' ou 'withdraw', et qui échoue s'il en trouve un.
LIVRABLE
`ohlc_source.py` avec `LocalCSVSource` + `HttpOHLCSource(allow_network=False)` par défaut, normalisation vers un `Bar` canonique, et une fonction `check_connector()` qui affirme keyless/lecture-seule par introspection et renvoie un rapport PASS/FAIL.
CLÉ ESSENTIELLE
Le chemin sûr doit être le défaut et le chemin dangereux doit exiger une option explicite à chaque appel — jamais un flag global. Un `allow_network=True` passé en argument à chaque usage est sûr ; une variable d'environnement `ALLOW_NETWORK=1` qui reste allumée est une fuite qui n'attend qu'à se produire.
ERREURS À ÉVITER
- ×Utiliser un endpoint qui exige une API key 'parce qu'il donne plus de données' — au moment où tu signes une requête, ton edge n'est plus structurellement incapable d'opérer, et toute la garantie s'effondre.
- ×Mettre le contrôle du réseau dans une variable d'environnement globale au lieu d'un argument explicite par appel ; elle reste allumée d'une exécution à l'autre et les boucles automatiques héritent du réseau sans le vouloir.
- ×Ne pas normaliser au bord : laisser des timestamps en ms et des nombres sous forme de string se propager en aval, cassant la reproductibilité entre la source locale et la source HTTP.
- ×Binder le dashboard sur `0.0.0.0` pour 'le voir depuis le mobile' — tu exposes au réseau toute ta recherche ; utilise `127.0.0.1` et un tunnel SSH si tu en as vraiment besoin.
- ×Vérifier la propriété keyless une seule fois à la main au lieu de la coder comme une assertion dans un `check` qui tourne toujours ; la garantie s'érode au premier refactor que personne ne relit.
SG-02 Classificateur de régime de prix
leçon Construire un classificateur de régime de prix pur et déterministe qui étiquette l'état du marché (tendance, volatilité, comportement, stress) sans jamais dimensionner ni opérer une position.
Classificateur de régime de prix
leçonConstruire un classificateur de régime de prix pur et déterministe qui étiquette l'état du marché (tendance, volatilité, comportement, stress) sans jamais dimensionner ni opérer une position.
Décider sans savoir dans quel régime tu te trouves, c'est opérer à l'aveugle : une stratégie qui gagne en tendance se vide de son sang en range. L'étiquette de régime est la condition préalable honnête de toute décision, et la séparer de l'exécution évite que le biais du 'vouloir opérer' contamine le diagnostic.
LA LEÇON
L'erreur conceptuelle que XCAP a corrigée lors de son audit de juin 2026 est subtile et vaut de l'or : confondre santé opérationnelle et régime de prix. `market_state.py` mesure la santé du système (est-ce que j'ai des données fraîches ? est-ce que les flags sont corrects ?). `regime.py` mesure quelque chose de totalement différent : dans quel ÉTAT se trouve le marché lui-même ? Ce sont des axes orthogonaux et les mélanger produit un classificateur qui ne sait pas ce qu'il dit. La première discipline est : un classificateur de régime ne parle que du prix, jamais de l'état de ton infrastructure.
Le design de XCAP classifie le long de quatre axes indépendants. Trend (ça monte, ça descend, c'est latéral ? — via la pente des moyennes ou le rendement cumulé sur une fenêtre). Volatility (calme ou agité ? — via l'écart-type des rendements, normalisé). Behavior (persistant/tendanciel ou mean-reverting ? — via l'autocorrélation des rendements). Et Stress (y a-t-il un drawdown en cours de développement ?). Chaque axe se calcule de façon pure : même entrée, même sortie, sans état caché, sans aléatoire. Ensuite une cascade de priorité documentée dérive un `Regime` titulaire unique à partir des quatre axes.
L'insight le plus tranchant de XCAP réside dans la façon dont il définit Panic, et c'est là que presque tout le monde se trompe. La tentation est de définir la panique comme 'volatilité élevée' (ratio d'écart-type élevé). C'est faux. Un crash monotone — le prix tombant en ligne quasi droite — a une volatilité INTERNE basse, parce que chaque bougie ressemble à la précédente. Si tu définis la panique par l'écart-type, tu rates exactement le crash le plus dangereux. La définition correcte de XCAP : la panique est un drawdown important qui se développe VITE, mesuré par un rendement récent fortement négatif et aigu, pas par la dispersion. C'est ce qui distingue un classificateur pensé par quelqu'un qui a regardé de vrais crashes de celui qui a copié une formule dans un livre.
La pureté n'est pas un caprice esthétique, c'est le socle de la confiance. `regime.py` est une fonction pure : `classify(bars) -> Regime`. Elle ne lit pas depuis le disque en interne, n'appelle pas le réseau, ne mute rien de global, n'a aucune branche aléatoire. Ça signifie que tu peux l'exécuter mille fois sur la même fenêtre et obtenir la même étiquette, que tu peux la tester avec des fixtures déterministes, et que tu peux auditer exactement pourquoi elle a dit 'Panic' à une date précise. Un classificateur avec état caché est un classificateur auquel tu ne peux pas faire confiance pour construire la Market Memory par-dessus.
La règle la plus importante, et celle qui relie toute la constellation : le classificateur est label-only. Il ne dimensionne JAMAIS et n'opère JAMAIS. Il renvoie une étiquette et rien d'autre. La raison relève de l'architecture de l'honnêteté : si le même module qui diagnostique le régime décide aussi de la taille de la position, tu as une incitation à ce que le diagnostic justifie l'opération que tu voulais déjà faire. Séparer le diagnostic de l'action, c'est ce qui permet, en SG-03, de mesurer honnêtement si le régime a une valeur prédictive — parce que l'étiquette a été générée sans savoir ce que tu allais en faire.
Pour bien l'implémenter, documente la cascade de priorité sous forme de code lisible, pas comme un enchevêtrement de ifs. Par exemple : si Stress déclenche Panic, Panic l'emporte sur tout le reste ; sinon, et si Volatility est élevée avec une Trend forte, c'est Trending-Volatile ; si Behavior est mean-reverting et Trend latérale, c'est Range ; etc. Chaque branche doit avoir un commentaire qui explique le pourquoi du marché, pas seulement le seuil. Ce commentaire, c'est ce dont un relecteur — ou toi-même dans six mois — a besoin pour faire confiance à l'étiquette.
EXERCICE
Écris `regime.py` avec une fonction pure `classify(bars: list[Bar]) -> Regime`. Calcule quatre sous-signaux : `trend` (signe et magnitude du rendement sur la fenêtre), `vol` (écart-type des rendements journaliers), `behavior` (autocorrélation lag-1 des rendements), `stress` (drawdown maximum récent ET rapidité = rendement des N derniers jours). Implémente la cascade de priorité où Panic = gros drawdown + rendement récent fortement négatif (PAS écart-type élevé). Renvoie un `Regime` avec le label titulaire et les quatre axes exposés. Teste avec trois fixtures synthétiques : une tendance haussière propre, un crash monotone (qui doit donner Panic malgré une vol interne basse), et un range bruité. Vérifie que la fonction est pure en exécutant `classify` deux fois sur la même entrée et en affirmant l'égalité.
LIVRABLE
`regime.py` avec `classify(bars) -> Regime` pur et déterministe, quatre axes (trend/vol/behavior/stress), cascade de priorité documentée, et trois tests de fixtures où le crash monotone est correctement classé comme Panic.
CLÉ ESSENTIELLE
La panique n'est pas une volatilité élevée — un crash monotone a une volatilité interne basse parce que chaque bougie imite la précédente. Définis la panique par la vitesse du drawdown (rendement récent aigu), pas par la dispersion, sinon tu rateras précisément le régime le plus dangereux.
ERREURS À ÉVITER
- ×Mélanger régime de prix et santé opérationnelle dans le même module ; ce sont des axes orthogonaux et les réunir produit des étiquettes qui ne signifient rien de concret.
- ×Définir Panic par un ratio d'écart-type élevé : tu rates les crashes monotones, qui sont justement ceux qui comptent.
- ×Glisser une branche aléatoire ou une lecture de disque dans le classificateur, cassant sa pureté et rendant impossible l'audit de pourquoi il a étiqueté une date.
- ×Laisser le classificateur dimensionner ou suggérer des opérations ; au moment où diagnostic et action partagent un module, le diagnostic se met à justifier l'opération que tu voulais déjà.
- ×Cacher la cascade de priorité dans des ifs sans commenter le pourquoi de marché ; personne ne pourra faire confiance au seuil ni le relire dans six mois.
SG-03 Market Memory : forecast + calibration
leçon Construire la Market Memory : un ledger qui verrouille une prédiction falsifiable AVANT le dénouement, la résout contre le rendement réalisé, et note sa propre calibration (hit-rate, Brier, calibration par confiance et par régime).
Market Memory : forecast + calibration
leçonConstruire la Market Memory : un ledger qui verrouille une prédiction falsifiable AVANT le dénouement, la résout contre le rendement réalisé, et note sa propre calibration (hit-rate, Brier, calibration par confiance et par régime).
Sans registre de prédictions verrouillées avant le résultat, un système de research n'a aucune façon honnête de savoir s'il sait quelque chose. C'est la différence entre 'je crois que ça marche' et 'voici mon historique out-of-sample noté'. C'est l'unique vraie source de preuve d'edge.
LA LEÇON
La découverte centrale de l'audit de XCAP fut dévastatrice : chaque module précédent était soit du calcul stateless, soit un accumulateur passif ; RIEN n'enregistrait une prédiction falsifiable avant le dénouement pour ensuite noter sa propre calibration. Cette boucle absente est exactement ce qui rend un système de research difficile à tromper, et l'unique source honnête de preuve d'edge. La Market Memory (`market_memory.py`) est cette boucle. Si tu ne construis pas ce module, tout le reste n'est que du théâtre : de jolis calculs jamais confrontés à la réalité.
Le cœur, c'est `advance_market_memory(universe, state_dir, horizon_days=5, ...)` et sa séquence est sacrée dans cet ordre : observer les dates nouvelles → classifier le régime (en utilisant le `regime.py` de SG-02) → enregistrer un forecast VERROUILLÉ (un baseline dérivé du régime, avec sa probabilité et son horizon) → résoudre les forecasts déjà arrivés à maturité contre le rendement forward réalisé → noter. Le verrouillage est le mot-clé : une fois que tu enregistres 'à la date T je prédis prob_up=0.62 à 5 jours', cet enregistrement est immuable. Tu ne peux pas y retoucher quand tu vois le résultat. Cette immuabilité est ce qui transforme le ledger en preuve et non en rationalisation a posteriori.
Les métriques ne sont pas une seule ; c'est un tableau de bord honnête. Hit-rate (quelle fraction des prédictions directionnelles a vu juste ?). Brier score (à quel point les probabilités étaient-elles bien calibrées ? — pénalise autant la surconfiance que la sous-confiance). Calibration par bucket de confiance (quand tu as dit 70 %, as-tu eu raison 70 % du temps ?). Et le joyau : hit-rate PAR RÉGIME. Cette dernière est celle qui informe vraiment : elle te dit si ta prédiction a de la valeur en tendance mais est nulle en range, ou si tu ne vois juste qu'en période calme. Un hit-rate global de 55 % peut cacher 70 % en tendance et 40 % en panique — et c'est cette décomposition qui dit où, le cas échéant, il y a de l'edge.
La propriété anti-fabrication est par construction (on l'approfondit en SG-04, mais ici elle est structurelle) : on ne résout que les forecasts dont l'horizon est arrivé à maturité avec des données qui existaient APRÈS la date du forecast. C'est out-of-sample par design. Tu ne peux jamais 'prédire' une date dont tu connais déjà le résultat, parce que le forecast s'est verrouillé en T et ne se résout que lorsque arrive T+horizon avec de vraies données. Le state dir est gitignored — on ne versionne pas l'historique pour qu'il n'y ait aucune tentation de l'éditer à la main et de maquiller les résultats.
Les trois propriétés d'intégrité que XCAP impose, et que tu dois répliquer : accumule (le ledger grandit d'une exécution à l'autre, ne se réinitialise jamais), idempotent (lancer le tick deux fois sur les mêmes dates ne duplique pas les forecasts et ne les résout pas deux fois), et Gate-closed (n'opère jamais). C'est la même famille d'invariants que le capital invariant de SG-05 : l'état du système est la somme d'événements réels, jamais un replay ni un baseline frais. Le CLI `market-memory-tick` orchestre tout ça et lit les CSV au format `data/real` (minuscules) via `LocalCSVSource` — zéro réseau, tout déterministe depuis le disque.
Un point qui sépare l'ingénieur de l'amateur : le baseline que tu enregistres doit être honnête et modeste. XCAP enregistre un 'regime_baseline' — une probabilité dérivée du régime, pas un modèle élaboré que tu as déjà optimisé en regardant le passé. Pourquoi commencer humble ? Parce que le baseline est ton étalon de mesure. Si un forecaster sophistiqué ne bat pas le baseline de régime en out-of-sample, tu n'as pas d'edge, tu as de l'overfitting. La Market Memory existe pour réfuter tes idées, pas pour les confirmer. Et un bon registre de forecasts est une preuve, jamais une démonstration — c'est pourquoi le Capital Gate reste fermé même si le hit-rate a l'air bon.
EXERCICE
Construis `market_memory.py` avec un ledger en JSON-lines append-only. Implémente `advance(universe, state_dir, horizon_days=5)` qui : (1) lit les dates nouvelles, (2) pour chaque date classifie le régime avec ton `regime.py`, (3) enregistre un forecast verrouillé `{date, symbol, regime, prob_up, horizon, resolved: false}`, (4) résout les forecasts mûrs (où la barre à date+horizon existe) en calculant le rendement forward réel et en marquant `hit`, (5) écrit les métriques : hit-rate global, Brier, et une décomposition du hit-rate PAR régime. Rends le tick idempotent : s'il existe déjà un forecast pour (date, symbol), ne le duplique pas ; s'il est déjà résolu, ne le re-résous pas. Démontre l'accumulation en lançant le tick sur deux plages de dates consécutives et en montrant que le ledger grandit sans se réinitialiser.
LIVRABLE
`market_memory.py` + ledger append-only dans `state/` (gitignored) avec des forecasts verrouillés avant le dénouement, une résolution out-of-sample, des métriques hit-rate/Brier/calibration-par-régime, et un CLI `market-memory-tick` idempotent et cumulatif.
CLÉ ESSENTIELLE
La métrique qui informe vraiment, c'est le hit-rate PAR RÉGIME, pas le global. Un 55 % global peut cacher 70 % en tendance et 40 % en panique — et c'est l'unique décomposition qui te dit où, le cas échéant, il y a un vrai edge à exploiter.
ERREURS À ÉVITER
- ×Enregistrer la prédiction et la résoudre dans la même exécution en regardant déjà le résultat ; ce n'est pas un forecast, c'est un ajustement a posteriori, et ça détruit toute preuve d'edge.
- ×Permettre que le ledger se réinitialise d'une exécution à l'autre ; tu perds l'historique qui est justement l'actif, et tu casses la propriété d'accumulation.
- ×Rendre le tick non idempotent : le lancer deux fois duplique les forecasts ou les re-résout, gonflant ou corrompant les métriques.
- ×Ne reporter que le hit-rate global et cacher la décomposition par régime, qui est là où vit l'information actionnable.
- ×Versionner le state dir dans git, ouvrant la porte à l'édition manuelle de l'historique et au maquillage de la calibration ; garde-le gitignored.
SG-04 Anti-fabrication par construction
leçon Concevoir le système pour qu'il soit structurellement incapable d'inventer des données ou de l'edge : null controls, significativité avec correction des comparaisons multiples, évaluation out-of-sample et un edge_demonstrated qui est toujours False par défaut.
Anti-fabrication par construction
leçonConcevoir le système pour qu'il soit structurellement incapable d'inventer des données ou de l'edge : null controls, significativité avec correction des comparaisons multiples, évaluation out-of-sample et un edge_demonstrated qui est toujours False par défaut.
La façon la plus coûteuse de se mentir en research, c'est de produire des nombres qui ressemblent à de l'edge mais qui sont du bruit, de la dérive ou du data-snooping. L'anti-fabrication n'est pas une revue finale : c'est une propriété architecturale qui rend impossible pour un forecaster de crier victoire sans avoir battu le hasard ET la dérive, avec significativité.
LA LEÇON
La question qui définit ce module : comment construis-tu un système qui ne PEUT PAS te mentir sur le fait qu'il a de l'edge ? La réponse de XCAP n'est pas 'être prudent'. C'est d'introduire des adversaires structurels — null controls — contre lesquels toute affirmation d'edge doit lutter. Le Hypothesis Lab (`hypothesis_lab.py`) a un registre de forecasters réels (regime_baseline, trend_follow, momentum_follow, mean_revert) ET deux contrôles nuls délibérés : `always_up` (le null de DÉRIVE — le marché monte en moyenne, est-ce que ton forecaster ne capture que ça ?) et `coin_flip` (le null de HASARD — seedé et déterministe, est-ce que tu bats un tirage à pile ou face ?). Un forecaster qui ne bat pas les DEUX nulls n'a pas d'edge, il a une illusion.
Le deuxième pilier, c'est la significativité avec correction des comparaisons multiples. Si tu testes vingt forecasters, l'un d'eux semblera bon par pur hasard — c'est le problème du data-snooping. XCAP le neutralise avec un seuil z corrigé par Bonferroni qui ÉCHELONNE avec K (le nombre de forecasters évalués) : plus tu testes d'hypothèses, plus la barre que chacune doit franchir est haute. `evaluate_forecasters(universe, names, horizon_days, min_samples, alpha)` compare chaque forecaster réel aux deux nulls (Δhasard et Δdérive) et ne lui accorde `candidate_edge` que s'il bat le hasard ET la dérive ET dépasse le seuil corrigé ET a au moins `min_samples` observations. Les quatre conditions, conjointes. En retirer une seule ouvre une porte à la fabrication.
Le détail qui démontre que le système est honnête et non truqué : `edge_demonstrated` est TOUJOURS False. Le lab peut accorder `candidate_edge` — 'ceci mérite plus d'investigation' — mais ne déclare jamais d'edge démontré, parce qu'aucun backtest, aussi bon soit-il, ne prouve un edge futur. C'est une décision de design qui te protège de ton propre optimisme : le système n'a littéralement aucun chemin de code qui dit 'oui, tu as de l'edge, opère'. Le Capital Gate reste fermé par construction, pas par ta discipline.
La vérification que le scoreboard n'est pas truqué est élégante et tu dois la répliquer : XCAP a vérifié que sur des tendances synthétiques, `trend_follow` gagne largement (il détecte le vrai signal), `mean_revert` part significativement NÉGATIF (il détecte un anti-edge — parier contre la tendance perd), et `coin_flip` atterrit à ~50 % (le hasard n'est pas récompensé). Si ton coin_flip sortait à 60 % de hit-rate, ton pipeline est cassé ou tu as du leakage. Les nulls ne sont pas seulement des adversaires : ce sont des sondes de calibration de ton propre système de mesure. Un coin_flip qui ne donne pas ~50 % est un système qui se ment, et tu dois tout arrêter jusqu'à comprendre pourquoi.
Le fondement partagé évite l'astuce de déplacer les buts : `scoring.py` est l'unique source de vérité pour les primitives (prob_up, is_hit, brier, conf_bucket, hit_rate_z). La Market Memory (SG-03) comme le Hypothesis Lab notent avec LE MÊME code. Si tu avais deux implémentations de 'hit', tu pourrais — consciemment ou non — utiliser la plus généreuse pour ton forecaster favori. Une seule primitive partagée ferme cette porte. C'est de l'anti-fabrication par construction : tu ne peux pas choisir la règle qui t'arrange parce qu'il n'y a qu'une seule règle.
Toute évaluation est out-of-sample par la même mécanique que SG-03 : chaque barre se classifie une seule fois, le forecast se génère sans voir le futur, et se résout contre le rendement forward réel. Le doc `docs/HYPOTHESIS_LAB.md` et les 4 invariants du `check` codifient ces garanties. La leçon de fond, qui est la thèse de toute la constellation Signal : la contrainte qui décide de l'opération de capital réel est l'edge DÉMONTRÉ, et ces outils mesurent l'edge honnêtement (null controls + significativité) au lieu de l'affirmer. Un système qui ne peut émettre que `candidate_edge` et jamais `edge_demonstrated` est un système qui respecte ce qu'il ne sait pas.
EXERCICE
Étends ton Hypothesis Lab avec `forecasters.py` (registre avec au moins `trend_follow`, `mean_revert` et deux nulls : `always_up` pour la dérive et `coin_flip` seedé pour le hasard) et `evaluate_forecasters(names, horizon_days, min_samples, alpha)`. Pour chaque forecaster réel, calcule son hit-rate out-of-sample et le delta contre les DEUX nulls. Implémente un seuil z avec correction Bonferroni qui échelonne avec K = nombre de forecasters. Accorde `candidate_edge=True` uniquement s'il bat le hasard ET la dérive ET dépasse le seuil ET a >= min_samples. Fixe `edge_demonstrated=False` toujours. Valide ton pipeline sur une série synthétique avec tendance : `trend_follow` doit sortir significativement positif, `mean_revert` significativement négatif, et `coin_flip` doit atterrir à ~50 % (sinon, ta mesure a du leakage).
LIVRABLE
`forecasters.py` + `hypothesis_lab.py` avec deux null controls (hasard et dérive), seuil z Bonferroni qui échelonne avec K, gate à quatre conditions pour `candidate_edge`, `edge_demonstrated` toujours False, et une validation où coin_flip sort à ~50 % prouvant que le scoreboard n'est pas truqué.
CLÉ ESSENTIELLE
Les null controls ne sont pas seulement des adversaires contre lesquels tu luttes — ce sont des sondes de calibration de ton propre système de mesure. Si ton coin_flip ne sort pas à ~50 %, tu as du leakage ou un bug, et tu dois TOUT arrêter jusqu'à comprendre pourquoi avant de croire le moindre autre chiffre.
ERREURS À ÉVITER
- ×Comparer le forecaster uniquement au hasard et oublier la dérive ; beaucoup d''edges' ne sont que le biais haussier de fond du marché déguisé.
- ×Ne pas corriger les comparaisons multiples : teste 20 forecasters et l'un semblera significatif par pur hasard (data-snooping).
- ×Permettre un chemin de code qui déclare `edge_demonstrated=True` ; aucun backtest ne prouve un edge futur, et ce flag invite à ouvrir le Capital Gate prématurément.
- ×Avoir deux implémentations de 'hit' ou de 'brier' au lieu d'une seule primitive partagée ; ça te laisse choisir la règle la plus généreuse pour ton forecaster favori.
- ×Ignorer que le coin_flip est sorti à 60 % : ce n'est pas de la chance, c'est du leakage dans ton pipeline, et tout autre résultat du lab est du déchet tant que ce n'est pas réglé.
SG-05 Capital invariant
leçon Implémenter le capital invariant : un ledger où le capital s'accumule entre les sessions, ne bouge que sur des clôtures RÉELLES (P&L réalisé), et ne se réinitialise jamais — de sorte que le nombre à l'écran reflète toujours des résultats clos, pas des replays ni des gains sur papier.
Capital invariant
leçonImplémenter le capital invariant : un ledger où le capital s'accumule entre les sessions, ne bouge que sur des clôtures RÉELLES (P&L réalisé), et ne se réinitialise jamais — de sorte que le nombre à l'écran reflète toujours des résultats clos, pas des replays ni des gains sur papier.
Un solde qui se gonfle de P&L non réalisé ou qui se réinitialise d'une exécution à l'autre est un solde qui ment. L'intégrité du capital est la condition sans laquelle aucun autre nombre du système ne mérite confiance : si la comptabilité peut mentir, tout le reste aussi.
LA LEÇON
Ce module naît d'une faille réelle et douloureuse chez XCAP, et c'est pour ça qu'il enseigne si bien. Un autorun programmé RÉINITIALISAIT silencieusement le ledger au solde initial de 500 et rejouait le même fixture identique toutes les 5 minutes. Le système montrait de l'activité, des nombres qui changeaient, il avait l'air vivant — et c'était complètement faux. Trois règles étaient violées à la fois, et ce sont les trois qui définissent le capital invariant. Mémorise ces règles parce qu'elles sont le noyau : accumule, realized-only, et la re-entry continue les sessions précédentes.
Règle 1 — Accumule. Le capital ne se réinitialise PAS d'une exécution ou d'une session à l'autre. Si tu as terminé la session d'hier avec 547,30, aujourd'hui tu démarres avec 547,30. Un baseline frais à chaque démarrage n'est pas de la comptabilité, c'est une démo déguisée. Le ledger se charge depuis le disque au démarrage et se persiste à la fin ; l'état vit entre les exécutions.
Règle 2 — Realized-only. Le solde ne bouge que lorsqu'une position est CLÔTURÉE/vendue. Le P&L non réalisé (mark-to-market, la valeur flottante d'une position ouverte) ne gonfle jamais le solde. L'invariant exact, que tu dois pouvoir affirmer à tout moment : `current_balance == starting_balance + realized_pnl`. Une position ouverte avec +200 de gain flottant NE change PAS le solde ; il ne bouge que lorsque tu la clôtures et que ces +200 deviennent réalisés. Ça évite le piège psychologique le plus vieux du trading : compter les gains avant de les avoir empochés.
Règle 3 — La re-entry continue. Au redémarrage, le capital est le résultat cumulé des opérations des sessions précédentes, pas une nouvelle ligne de base. Le système doit pouvoir s'éteindre et se rallumer mille fois et le capital toujours être la somme honnête de toutes les clôtures réelles qui ont eu lieu. C'est la même chose que la propriété d'accumulation de la Market Memory (SG-03) : l'état est la somme d'événements réels, une seule direction, sans replay.
La leçon d'architecture, c'est la distinction entre deux commandes que XCAP maintient délibérément séparées. L'autorun programmé exécute `sim-autopilot-tick` : il charge le ledger existant, fait tourner des régimes synthétiques, et ACCUMULE sur ce qui précède. Il existe aussi `sim-run-agent`, qui réinitialise et rejoue un fixture — mais celui-là est conservé UNIQUEMENT pour les démos déterministes, et ne doit JAMAIS être ce que lance l'autorun. Le bug fut précisément de pointer le scheduler sur la mauvaise commande. L'enseignement : une commande qui réinitialise et une commande qui accumule ne doivent jamais être confondables ; nomme-les de façon à ce que la dangereuse crie ce qu'elle fait.
Comment on blinde ça pour que ça ne recasse pas : l'invariant est LOCKED par `tests/test_sim_autopilot_tick.py`. Si tu touches au ledger ou à l'autorun, ces tests doivent rester verts. Ce n'est pas de la documentation, c'est un verrou exécutable. La règle opérationnelle que l'utilisateur a imposée avec insistance : si tu touches à la comptabilité, tu lances ce test, et s'il passe au rouge, tu as cassé l'intégrité du capital et tu t'arrêtes. C'est la même philosophie que le Capital Gate et le `check` de SG-01 : les invariants qui comptent vraiment se codent comme des tests qui refusent de te laisser les casser. La comptabilité de XCAP n'est pas fiable parce que l'auteur est prudent ; elle est fiable parce qu'un test rouge l'empêche d'être négligent.
EXERCICE
Implémente `ledger.py` avec un solde persisté dans `state/ledger.json`. Fonctions : `load()` (lit le solde cumulé, ou le starting_balance uniquement au tout premier démarrage), `open_position(symbol, entry, size)`, `mark(symbol, price)` (calcule le P&L NON réalisé mais NE touche PAS au solde), et `close_position(symbol, exit)` (réalise le P&L et SEULEMENT à ce moment-là met à jour le solde, puis persiste). Maintiens l'invariant `current_balance == starting_balance + sum(realized_pnl)` et affirme-le après chaque opération. Écris `test_capital_invariant.py` qui : (1) ouvre une position gagnante, appelle `mark` avec un prix supérieur, et vérifie que le solde N'A PAS changé ; (2) la clôture et vérifie que maintenant il a bien monté d'exactement le P&L réalisé ; (3) simule un redémarrage (load après persistance) et vérifie que le solde continue, ne se réinitialise pas.
LIVRABLE
`ledger.py` avec solde cumulatif persisté + `test_capital_invariant.py` qui teste les trois règles : mark ne bouge pas le solde, close le bouge bien du P&L réalisé exact, et un redémarrage continue le cumul au lieu de réinitialiser.
CLÉ ESSENTIELLE
L'invariant `current_balance == starting_balance + realized_pnl` est une seule ligne qui, affirmée après chaque opération, rend structurellement impossible qu'un P&L sur papier ou un replay gonfle ton solde. Quand la comptabilité est une équation vérifiable et non une croyance, elle ne peut plus te mentir.
ERREURS À ÉVITER
- ×Réinitialiser le ledger au starting_balance à chaque démarrage de l'autorun — le bug original de XCAP ; ça montre une fausse activité et efface tout l'historique réel.
- ×Laisser le P&L non réalisé (mark-to-market) toucher au solde ; tu comptes des gains que tu n'as pas encore empochés et le nombre à l'écran ment.
- ×Confondre la commande qui accumule (`sim-autopilot-tick`) avec celle qui réinitialise-et-rejoue (`sim-run-agent`) ; nomme la dangereuse pour qu'il soit impossible de lui pointer le scheduler par erreur.
- ×Toucher au ledger ou à l'autorun sans lancer `test_capital_invariant.py` ensuite ; l'invariant s'érode au premier refactor que personne n'a vérifié.
- ×Traiter l'invariant comme un commentaire dans le code au lieu d'une assertion exécutée après chaque opération ; un commentaire ne t'empêche pas de le casser, un assert si.
SG-06 Autopilot ticks & boucles honnêtes
leçon Construire des boucles d'autopilot qui accumulent un véritable apprentissage entre les ticks — déterministes depuis le disque, idempotentes, Gate-fermé — au lieu de générer du bruit ou de la fausse activité.
Autopilot ticks & boucles honnêtes
leçonConstruire des boucles d'autopilot qui accumulent un véritable apprentissage entre les ticks — déterministes depuis le disque, idempotentes, Gate-fermé — au lieu de générer du bruit ou de la fausse activité.
Une boucle autonome mal conçue est dangereuse : elle tourne seule, sans personne pour regarder, et peut réinitialiser l'état, dupliquer des enregistrements ou déclencher des actions réelles. La différence entre une boucle qui apprend et une qui se contente de bouger, c'est toute la différence entre du research et du théâtre automatisé.
LA LEÇON
Un tick d'autopilot est du code qui tourne sans qu'un humain regarde, et c'est exactement pour ça qu'il doit être le code le PLUS discipliné du système. L'erreur que XCAP a subie dans sa propre chair enseigne tout : un tick programmé qui, toutes les 5 minutes, réinitialisait le ledger et répliquait un fixture identique. Vu de l'extérieur, le système semblait vivant — des nombres qui bougeaient, une activité constante. À l'intérieur c'était un mensonge en boucle. La question qui définit un bon tick : est-ce que cette boucle, lancée mille fois, ACCUMULE quelque chose de vrai, ou ne génère-t-elle que l'apparence d'activité ?
Propriété 1 — Déterministe depuis le disque. Le tick lit son entrée depuis le disque (CSV `data/real` via `LocalCSVSource`), pas depuis le réseau. Les boucles automatiques de XCAP n'activent JAMAIS le réseau — `allow_network` reste à False, toujours. C'est la continuité directe de SG-01 : le réseau est en opt-in explicite par appel humain (la commande `data-fetch`), jamais quelque chose qu'une boucle autonome active seule. Un tick qui touche au réseau sans supervision est un tick qui peut faire fuir, échouer de façon non reproductible ou rester bloqué en attente d'un socket. Déterministe depuis le disque signifie que le même état sur disque produit le même résultat, hardi et reproductible.
Propriété 2 — Idempotent. Lancer le tick deux fois sur les mêmes dates ne doit pas dupliquer les forecasts, ni re-résoudre ce qui est déjà résolu, ni compter deux fois une clôture. Ça compte énormément dans les boucles parce que les schedulers échouent, retentent, se chevauchent. Si ton tick n'est pas idempotent, un retry corrompt l'état en silence. La façon d'y arriver : chaque événement (un forecast, une clôture) a une clé naturelle (date+symbole) et le tick vérifie l'existence avant d'écrire. `advance_market_memory` et `sim-autopilot-tick` sont idempotents pour cette raison.
Propriété 3 — Accumule, ne réinitialise pas. Le tick charge l'état précédent et construit DESSUS. `sim-autopilot-tick` charge le ledger, fait tourner des régimes synthétiques, et accumule — exactement la règle du capital invariant de SG-05 et de la Market Memory de SG-03. C'est le même invariant qui apparaît pour la troisième fois dans la constellation, et ce n'est pas un hasard : l'honnêteté d'un système autonome EST son refus de se réinitialiser. Une boucle qui repart de zéro à chaque fois n'apprend rien ; une boucle qui accumule est la seule chose capable de construire un historique de calibration qui vaille.
Propriété 4 — Gate-fermé par construction. Le tick n'opère JAMAIS de capital réel. Le Capital Gate (`capital/broker/live_trading/automation` tous à False) est vérifié par `python3 -m xcap.control check`. Un autopilot est exactement l'endroit où un système de trading tue son propriétaire : il tourne seul, et s'il avait la capacité d'opérer en réel, un bug à 3h du matin vide le compte. C'est pourquoi chez XCAP l'autopilot est sim-only, sans exception, et `check` l'affirme à chaque exécution. L'automatisation et la capacité d'opérer en réel sont deux choses qui ne doivent JAMAIS coexister sans une décision humaine ultra-délibérée.
Comment on opère et on blinde tout ça : le tick tourne via le scheduler (chez XCAP, une tâche programmée qui invoque `sim-autopilot-tick`, la commande qui accumule — JAMAIS `sim-run-agent`, celle qui réinitialise pour les démos). Le comportement est LOCKED par `tests/test_sim_autopilot_tick.py` : si tu touches à l'autorun, ces tests restent verts ou tu as cassé l'intégrité de la boucle. Et chaque exécution se termine par un `check` qui réaffirme les invariants (keyless, Gate-fermé, accumulation). Le principe final, qui scelle toute la constellation : une boucle honnête est une boucle dont les garanties ne dépendent pas de quelqu'un qui la surveille, parce qu'elles sont codées comme des tests et des invariants que la boucle elle-même vérifie à chaque tour. Construis des boucles qui refusent de mentir même quand personne ne regarde — c'est la seule automatisation à laquelle un système de research peut faire confiance.
EXERCICE
Construis `autopilot_tick.py` avec une fonction `tick(state_dir)` qui, à chaque appel : (1) charge l'état précédent depuis le disque (ledger + ledger de forecasts), (2) lit les barres nouvelles via `LocalCSVSource` avec `allow_network=False`, (3) fait avancer la Market Memory (verrouille les nouveaux forecasts, résout les mûrs) en accumulant, (4) lance `check()` en affirmant Gate-fermé et keyless, (5) persiste. Rends-le idempotent : lance `tick` deux fois de suite sur le même état et vérifie que le second n'ajoute ni ne change rien. Écris `test_autopilot_tick.py` qui teste : idempotence (double tick = état identique), accumulation (deux ticks sur des plages distinctes font grandir l'état), et que `check` échoue si quelqu'un met `allow_network=True` ou ouvre le Gate. Programme le tick avec une tâche récurrente qui n'invoque QUE la commande qui accumule.
LIVRABLE
`autopilot_tick.py` (déterministe depuis le disque, idempotent, cumulatif, Gate-fermé) + `test_autopilot_tick.py` qui locke l'idempotence et l'accumulation, et un scheduler pointant sur la commande qui accumule — jamais sur celle qui réinitialise.
CLÉ ESSENTIELLE
L'invariant d'accumulation (ne pas réinitialiser) apparaît trois fois dans Signal — capital, Market Memory et autopilot — et ce n'est pas un hasard : l'honnêteté d'un système autonome EST son refus de se réinitialiser. Une boucle qui repart de zéro à chaque tour n'apprend pas ; elle simule juste d'être vivante.
ERREURS À ÉVITER
- ×Pointer le scheduler sur la commande qui réinitialise-et-rejoue (`sim-run-agent`) au lieu de celle qui accumule (`sim-autopilot-tick`) ; le bug exact qui falsifiait l'activité de XCAP.
- ×Laisser la boucle activer le réseau (`allow_network=True`) ; un tick autonome qui touche au réseau peut faire fuir, se bloquer ou échouer de façon non reproductible sans personne pour regarder.
- ×Rendre le tick non idempotent : quand le scheduler retente ou se chevauche, il duplique les forecasts et corrompt l'état en silence.
- ×Donner à l'autopilot la moindre capacité d'opérer en réel sans une décision humaine délibérée ; automatisation + capacité de trade réel, c'est comme ça qu'une boucle vide un compte à 3h du matin.
- ×Se fier à une surveillance humaine au lieu de coder les garanties comme des tests et des invariants que la boucle elle-même vérifie avec `check` à chaque tour.
Constellation suivante
Brand & Surface
Marque et surface