← La note
Reproductibilité

Chaque chiffre de la note, et sa provenance

De quoi vérifier l'arithmétique à la main, ou reconstruire une étude équivalente à partir des mêmes sources publiques et sous licence, y compris ce que change l'addendum et le hash sous lequel chaque exécution est gelée.

Données et univers

Crypto. Dix contrats perpétuels Binance marginés en USDT, sélectionnés le 31 décembre 2022 par volume notionnel quotidien médian d'octobre à décembre 2022 : BTC, ETH, DOGE, XRP, BNB, SOL, MATIC, LTC, CHZ et ETC. Les klines et l'historique complet des taux de funding proviennent des archives publiques de Binance.

Actions. Dix actions américaines à grande capitalisation sélectionnées de la même manière : TSLA, AAPL, NVDA, AMZN, MSFT, AMD, META, GOOGL, NFLX et XOM. La source est constituée de données de transactions et de cotations à 1 minute éligibles NBBO, indexées par un identifiant de titre stable avec des facteurs d'ajustement chaînés à travers les changements d'identifiant, afin que les splits et la réutilisation de tickers ne puissent pas corrompre les rendements.

Portée. Deux fréquences de barre, 15 minutes et 1 heure, donnant quatre cellules : crypto:15m, crypto:1h, equity:15m et equity:1h. L'historique d'entrée commence le 1er janvier 2021 et l'évaluation hors échantillon court du 1er janvier 2023 au 31 juillet 2026.

Chaque fichier d'entrée est haché dans un manifeste de données que l'exécution vérifie avant de toucher la moindre barre. Le funding, les spreads, les frais, le slippage, les horodatages des plus hauts et des plus bas, les champs d'ordre de parcours, les rendements futurs et les résultats de trades sont des métadonnées d'exécution, et ils sont interdits dans les définitions de signaux de base comme dans les entrées d'état de la courbe d'équité.

La bibliothèque de stratégies, et sa calibration

Quatre-vingts stratégies mécaniques réparties sur huit familles, toutes symétriques en signe. La bibliothèque a été calibrée une seule fois, sur janvier 2021 à décembre 2022 uniquement, pour que les durées de détention ressemblent à celles d'un trader discrétionnaire intraday et swing : la plupart des positions se ferment en quelques heures, avec une queue atteignant environ deux semaines. La calibration lit le nombre de candidats, les durées de détention et la couverture des instruments, et ne lit aucun rendement, Sharpe, taux de réussite ni drawdown d'aucune sorte. Elle a convergé en trois tours avec cinq remplacements, tous dans la famille retournement RSI, chacun pour couverture insuffisante.

Un candidat ne se déclenche que lorsque la vue directionnelle d'une stratégie s'inverse réellement, de long à short ou de short à long. Une barre neutre n'est pas un signal et ne réinitialise pas la vue :

# The candidate grid: genuine inversions only, never a re-entry from neutral.
view = raw_view.replace(0, method="ffill")        # neutral holds the previous view
signal = view != view.shift(1)                    # a transition, not a level

Cela compte plus qu'il n'y paraît. Traiter chaque retour depuis le neutre comme un nouveau signal fait qu'une règle dont la condition oscille autour d'une bande émet un candidat à chaque fois, et ne compter que les véritables inversions a divisé le nombre de candidats par environ trois.

Ces transitions de signal constituent aussi la grille de candidats commune sur laquelle chaque politique est comparée. À chaque transition, chaque politique ferme ce qu'elle détient et ouvre une position dans la direction nouvellement signalée, si bien que les bornes et le nombre de trades sont identiques d'une politique à l'autre par construction, et que la seule chose qui diffère est la façon dont chaque trade s'est terminé et à quel prix.

Les barrières, et la référence que les traitements doivent battre

La largeur des barrières et le ratio gain/risque sont des paramètres de stratégie, pas des constantes universelles, donc ils sont calibrés en échantillon pour chaque couple stratégie-fenêtre sur l'objectif enregistré, et comptés dans le même budget de recherche que les overlays.

stop in {3, 6} ATR  x  reward-to-risk in {1.5, 3}   =  4 base configurations

Cela donne des couples objectif et stop de (4.5, 3), (9, 3), (9, 6) et (18, 6) ATR. L'ATR est l'ATR de Wilder(14) calculé jusqu'à la barre de signal complétée puis figé pour toute la durée du trade, et une unité de risque vaut un mouvement d'un ATR à l'entrée, à la taille de référence. Calibrer la référence de cette façon relève la barre que les traitements doivent franchir : la comparaison honnête se fait contre une gestion statique calibrée, pas contre une référence universelle à deux pour un, facile à améliorer.

Variables d'état de la courbe d'équité

Huit variables orientées, chacune définie pour qu'une valeur plus grande signifie toujours une meilleure performance récente : le résultat net cumulé sur les 10 trades précédents ; le même sur 50 ; l'équité moins sa moyenne mobile sur 20 trades, divisée par l'écart-type des 20 incréments précédents ; le drawdown maximal sur les 50 derniers trades clôturés, stocké comme valeur non positive ; la longueur signée de la série en cours ; le taux de réussite sur 20 trades ; le taux de réussite sur 50 ; et le résultat net moyen sur 20 trades.

state = {
    "cum_r_10":     net_r.rolling(10).sum(),
    "cum_r_50":     net_r.rolling(50).sum(),
    "equity_vs_ma": (equity - equity.rolling(20).mean()) / net_r.rolling(20).std(),
    "max_dd_50":    (equity - equity.rolling(50).max()).rolling(50).min(),   # <= 0
    "streak":       signed_streak(net_r > 0),
    "win_rate_20":  (net_r > 0).rolling(20).mean(),
    "win_rate_50":  (net_r > 0).rolling(50).mean(),
    "mean_r_20":    net_r.rolling(20).mean(),
}

Une neuvième candidate, le drawdown courant face à un plus haut glissant de tous les temps, a été retirée pour des raisons structurelles avant l'exécution. Pour une stratégie à espérance négative, elle n'est pas bornée et dérive de façon monotone vers le bas, si bien que les seuils ajustés en échantillon sont dépassés en permanence hors échantillon : dans un exemple mesuré, toute la plage hors échantillon est tombée sous le plus bas point de coupure en échantillon et le filtre n'a exécuté aucun trade. C'est un défaut de stationnarité, observable sans référence à la rentabilité, et c'est pourquoi le drawdown fenêtré sur 50 trades est utilisé à la place.

Tant que le lookback d'une variable n'est pas complet, la gestion reste à la référence sélectionnée et la trace de décision enregistre insufficient_history. Il existe une courbe d'équité par stratégie de base, marché, timeframe et politique, et les dix instruments d'une cellule la partagent.

La bibliothèque de gestion et le budget de recherche

Les actions d'overlay sont des multiplicateurs de la base sélectionnée, si bien qu'une famille garde son sens pendant que la base flotte. La réponse mean-reversion inverse la correspondance entre l'état et l'action.

ÉtatTP seulSL seulTP + SLTaille de positionFiltre de trade
10.5x TP0.5x SL0.5x les deux0.5Rsauter
20.75x TP0.75x SL0.75x les deux1Rsauter
31x1x1x1Rprendre
41.5x TP1.5x SL1.5x les deux1.5Rprendre
52x TP2x SL2x les deux2Rprendre

Huit variables d'état multipliées par deux sens donnent 16 overlays par famille, et 16 overlays multipliés par 4 configurations de base donnent 64 essais par famille et par fenêtre. Chaque famille reçoit exactement le même budget, et c'est ce que recherche égale veut dire ici : un même nombre de configurations tentées, pas un même nombre de réglages.

Les sources d'état sont choisies pour qu'aucune famille ne puisse fabriquer son propre état. Les familles barrières lisent leur propre résultat net réalisé ; le dimensionnement de position lit son propre résultat réalisé à taille unitaire, de sorte que le levier ne peut pas gonfler la variable qui le pilote ; le filtrage de trades lit un book fantôme de référence mis à jour en continu, de sorte que l'état off ne peut jamais être absorbant.

Walk-forward

Quatorze fenêtres trimestrielles hors échantillon complètes, de 2023Q1 à 2026Q2, plus une fenêtre partielle enregistrée couvrant juillet 2026 seul, qui est notée, signalée et déclarée partout où elle est incluse. L'échantillon d'entraînement est constitué des 24 mois calendaires précédents dans tous les cas, et les bornes de calendrier sont identiques d'un marché à l'autre.

for window in quarterly_windows(oos_start="2023-01-01", oos_end="2026-08-01"):
    grid = candidate_grid(strategy, window)                    # no equity state, no OOS values
    is_candidates = [c for c in grid if c.entry_ts < window.oos_start
                                     and c.exit_ts < window.oos_start]

    thresholds = state_thresholds(baseline_replay(is_candidates))
    attempts   = [simulate(cfg, is_candidates, thresholds) for cfg in family_grid]  # all 64
    persist(attempts)                                          # including degenerate and inactive

    winner = max(attempts, key=lambda a: (a.net_r_per_common_candidate,
                                          a.executed_trades, -a.canonical_id))
    winner.freeze()                                            # config and thresholds fixed
    score(winner, window.oos_slice)                            # OOS starts flat, state from IS replay

La sélection est chronologique, chaque tentative est persistée, y compris celles qui n'ont rien fait, et l'état hors échantillon de l'overlay sélectionné est initialisé à partir de son rejeu en échantillon complété.

Exécution intrabarre

Les panels préparés portent l'horodatage du plus haut et du plus bas définitifs de chaque barre parente, si bien que le parcours intrabarre est reconstruit comme ouverture, premier extrême enregistré, second extrême enregistré, clôture, les franchissements de barrière étant évalués le long de ces segments monotones.

Un gap qui traverse un stop sort à la première ouverture disponible, tandis qu'un gap qui traverse un objectif est exécuté à l'objectif, ce que fait un ordre limite en attente. Si une seule barrière est atteignable, elle s'exécute ; si les deux le sont, l'ordre des extrêmes enregistrés tranche ; et si cet ordre est inconnu, le stop s'exécute en premier, les trades qui dépendaient de cette convention étant comptés et leur contribution économique déclarée.

Modèle de coûts

ExécutionFraisSlippageSpread
Entrée, quelle qu'en soit la raisontaker 5 bps2 bpsmoitié du spread mesuré
Sortie : take-profit, ou gap traversant l'objectifmaker 2 bpsaucunaucun
Sortie : stop-loss, ou gap traversant le stoptaker 5 bps2 bpsmoitié du spread mesuré
Sortie : retournement sur signal inversetaker 5 bps2 bpsmoitié du spread mesuré

Les actions paient une commission et provision de frais forfaitaire de 0.5 bps par exécution, sans distinction maker ou taker. Le funding crypto est facturé à chaque settlement traversé par une position ouverte, à son signe observé. De 2021 à 2026, le funding a été positif 74.5% du temps et s'est établi en moyenne à +0.87 bp par tranche de huit heures, soit environ 9.5% par an contre un book en permanence long, ce qui explique pourquoi il est rapporté décomposé par sens de trade plutôt que fondu dans un chiffre net unique.

Chaque ledger conserve le résultat brut de prix, le spread d'entrée et de sortie, les frais d'entrée et de sortie, le slippage d'entrée et de sortie, le funding signé et le résultat net comme colonnes séparées, et l'identité doit se réconcilier sous 1e-9 unité de risque :

net_r = (gross_price_r
         - entry_spread_r - exit_spread_r
         - entry_fee_r    - exit_fee_r
         - entry_slip_r   - exit_slip_r
         + funding_r)                       # signed: debits longs when funding is positive
assert abs(net_r - ledger.net_r).max() < 1e-9

Inférence

Le bootstrap apparié rééchantillonne des semaines calendaires entières du lundi au dimanche, en prenant ensemble tous les instruments et toutes les stratégies d'une même semaine, sur 10,000 rééchantillonnages avec une seed fixe. Le rééchantillonnage ligne par ligne, la moyenne des Sharpe par fenêtre et la multiplication du nombre de fenêtres par le nombre de stratégies sont tous interdits.

weeks  = pd.Grouper(key="ts", freq="W-MON")
blocks = [df for _, df in paired_deltas.groupby(weeks)]      # whole weeks move together

draws = [np.concatenate(rng.choice(blocks, len(blocks))).mean() for _ in range(10_000)]
p = 2 * min((np.array(draws) <= 0).mean(), (np.array(draws) >= 0).mean())
q = benjamini_hochberg(p_values, alpha=0.05)                 # 20 contrasts in the declared family

La multiplicité est corrigée à l'intérieur de la famille déclarée de 2 marchés par 2 timeframes par 5 familles de gestion, soit 20 contrastes, échecs compris.

Le placebo par rotation d'actions reprend la même politique et fait tourner ses actions sur les mêmes candidats, sur 1,000 tirages à seed fixée, avec une correspondance d'exposition comme contrôle bloquant : le null permuté doit reproduire exactement le turnover, le nombre d'exécutions et l'exposition nominale d'action de l'overlay réel, de sorte que la seule chose détruite soit l'alignement entre l'état de la courbe d'équité et le trade.

La suite de perturbation exécute 20 tirages de suppression de candidats à 1% et 20 chocs d'exécution avec des coûts mis à l'échelle entre 0.75x et 1.25x, et exige qu'au moins 80% des tirages conservent le signe de l'effet, que l'accord médian des décisions reste au-dessus de 80%, et que la corrélation médiane des courbes d'équité reste au-dessus de 0.9.

Les huit conditions d'attribution

  1. le résultat net agrégé par candidat commun dépasse la référence
  2. l'espérance sur les trades exécutés s'améliore, une espérance non définie comptant comme un échec
  3. le résultat net par unité de risque brut nominal moyen ne se dégrade pas
  4. la différence appariée groupée par semaine a un q ajusté BH inférieur à 0.05
  5. taux de réussite sur fenêtres complètes supérieur à 50% et ratio gain/risque supérieur à 1
  6. la différence reste positive après suppression de la meilleure fenêtre hors échantillon
  7. le résultat bat le 95e centile du placebo apparié par rotation d'actions
  8. le contrôle de stabilité à la dépendance de parcours est franchi

La condition 4 a échoué dans les 20 comparaisons de l'étude et dans les 24 de l'addendum. La condition 2 est rédigée pour qu'une politique à turnover nul y échoue, parce qu'une telle politique peut battre une référence perdante sur la condition 1 en ne faisant rien du tout, et la suite d'analyse teste exactement ce cas.

Ce que change l'addendum

L'univers, les données, les timeframes, le calendrier walk-forward, la grille de barrières calibrable en échantillon, les huit variables d'état de la courbe d'équité, le budget de 64 essais, la règle de sélection, l'éligibilité, le bootstrap, le placebo, la suite de perturbation et les huit conditions d'attribution sont tous hérités sans changement. Quatre choses diffèrent.

Sorties

Une position ne se ferme que lorsque son take-profit ou son stop-loss s'exécute. Si aucun des deux ne s'est exécuté à la fin de l'historique de l'instrument, le trade est tronqué à la dernière barre observée et signalé, et le taux de troncature est une statistique à rapporter obligatoirement, car un taux élevé signifierait que les barrières ne se résolvent pas à l'intérieur de l'échantillon et que le protocole n'a pas livré le test équitable qu'il promet.

Largeurs de barrière

Supprimer la sortie sur retournement allonge les détentions, donc la grille de stops a été recalibrée par cellule avant l'exécution, en lisant uniquement la durée de détention et la résolution :

CelluleStops (ATR)Détention médiane sur les quatre basesDétentions de deux semaines ou plus
crypto:15m3.0, 6.06.2h à 38.5h0.0% à 4.4%
equity:15m3.0, 6.06.0h à 28.9h0.6% à 24.3%
crypto:1h1.5, 3.06.5h à 38.0h0.0% à 3.8%
equity:1h1.5, 2.257.0h à 21.0h0.3% à 13.7%

Quinze des seize combinaisons cellule et base se situent dans la bande convenue. L'exception est equity:15m à sa base la plus large, rapportée dans les limites de la note plutôt que retirée.

Occupation

Chaque branche tranche le cas d'un signal qui arrive alors qu'une position est ouverte, et les deux sont notées sur le même dénominateur, à savoir chaque transition de signal de la fenêtre, laquelle est fixée par la règle et par la série de prix :

if branch == "single" and book.holds(instrument):
    decision = "declined_as_occupied"      # earns nothing, still counts in the denominator
else:
    book.open(instrument, side, size, tp, sl)

Une politique occupée qui saute un signal n'en tire simplement rien. Le nombre de trades diffère alors d'une politique à l'autre, ce qui est un résultat à rapporter plutôt qu'une comparaison cassée, et le turnover est rapporté à côté.

Multiplicité

L'addendum est corrigé à l'intérieur de sa propre famille déclarée de 3 familles par 4 cellules par 2 branches, soit 24 contrastes. Les deux branches sont corrigées ensemble parce que les deux ont été exécutées et que les deux sont rapportées, si bien que les noter séparément sous-estimerait l'ampleur de la recherche. Les contrastes de l'addendum ne sont jamais agrégés avec ceux de l'étude, et aucun des deux ne réévalue l'autre.

Vérification, et les hashes gelés

Le moteur d'exécution en Rust a été vérifié ligne par ligne contre une implémentation de référence en Python écrite indépendamment. Sur le moteur de l'addendum, les deux concordent exactement, à une tolérance de 1e-9, sur chaque barre de sortie, chaque prix de sortie et chaque résultat net, sur 362,100 lignes. La parité est la suite qui compte le plus ici, parce que supprimer la sortie sur retournement a modifié le cœur d'exécution lui-même, et le gel refuse un verdict enregistré avant le code qu'il certifie, donc aucun verdict n'est hérité du moteur de l'étude.

SuiteÉtudeAddendum
Parité d'exécution, moteur Rust contre la référence Pythonréussiréussi
Calibration avant gel, durée de détention uniquementréussiréussi
Primitives d'inférenceréussiréussi
Contrôles d'analyseréussiréussi
État, sélection, robustesse et parité des artefactsnon rejouéenon rejouée
Détection de défauts plantés, causalité et contrôles de fuitenon rejouéenon rejouée

Les deux ont été spécifiés par écrit et gelés avant qu'aucun chiffre hors échantillon ne soit produit ou ouvert, et chaque chiffre rapporté est généré à partir d'artefacts stockés plutôt que saisi à la main. L'addendum porte ses propres hashes d'overlay et d'implémentation, et c'est ce qui en fait un ajout plutôt qu'une réévaluation :

ArtefactSHA-256
strategy_configs.json, partagé par les deuxd5c1fabbd22fcf20907e24025c6868f739afb11a2a952fc71241630f318cf68e
overlay_configs.json, étudec0eaaaedd74800e2e973dda23a23053ee7966a07762803e7b2592cabfed5320f
analysis_config.json, étude1afcb713478d37d234268038280d9a83f38dfc535c8b1e3fbb9b6acc649e4fef
data_manifest.json, étude93aefeaa5ad78d920fdaf48d36cc61794921eaaaa92cbf52f3b7f5119c855a59
strategy_selection.json, étudea37718869d539141b64c492f1d221a7a9ba8395f7e9ca323d2c536028a21df4c
Implémentation d'analyse, étudee46041ab30b8f44ad1029a798e1d0bb58aa97959ba88bfb0558bf4468393600f
overlay_configs.json, addendum7d575275961bee72f5f6557ae5c253545f69cc352d5b9e35a216643714b7331e
Implémentation d'analyse, addendum667f6ab6e65c2421c670179c6a5de8405209e2d8db35347e40fd71acb5fb4105

Demander le pipeline complet

Cette page suffit pour vérifier à la main chaque chiffre de la note, ou pour reconstruire une étude équivalente à partir des mêmes sources de données publiques et sous licence. Les scripts de production, c'est-à-dire la collecte des données, la construction des panels, le moteur Rust, le lanceur d'expériences et l'analyse, ne sont pas publiés ici. Envoyez un e-mail à daniel@daru.finance et je vous les enverrai.