← L’article
Reproductibilité

Chaque nombre de l’article, et d’où il vient

Assez de méthode pour refaire les calculs à la main, ou pour reconstruire une étude équivalente à partir des données publiques de Binance.

Données

Les barres source viennent de data.binance.vision/data/futures/um/, public et sans identifiants. Six séries sont construites par contrat : les klines OHLCV 30 minutes comme série principale ; les klines 1 minute, réduites à l’arrivée au haut, au bas et à leur minute, conservées uniquement pour résoudre l’ordre dans lequel le haut et le bas de chaque barre 30 minutes se sont imprimés ; les événements de funding à leurs vrais horodatages de règlement, portant funding_interval_hours par événement ; les klines de mark price 30 minutes, qui valorisent le flux de funding ; les klines de premium index 30 minutes, l’entrée de base du bras exogène ; et l’open interest et les ratios long-short en 5 minutes.

L’intervalle de funding est lu événement par événement plutôt que supposé, parce que Binance a fait passer plusieurs contrats de huit à quatre heures en 2025 et que toute constante serait fausse sur une partie de la période. Le mark price publié est utilisé plutôt qu’une ouverture de contrat substituée, et le premium index est utilisé directement plutôt que de dériver la base depuis des clôtures spot, si bien qu’aucun second instrument n’entre dans le panel.

Les prix ne sont jamais reportés en avant à travers une barre manquante, aucune entrée n’est permise tant qu’un intrant requis manque et un signal crée un ordre pour la barre immédiatement suivante seulement. Si cette barre est absente, l’ordre est annulé plutôt que reporté sur une ouverture ultérieure. Une position ouverte à travers un trou de données le reste, et les indicateurs doivent refaire tout leur warm-up avant un autre signal.

Tout intrant non-prix est aligné as-of la clôture de barre, de sorte qu’une valeur à la barre t était connaissable à la clôture de la barre t :

# open interest, long-short ratios: last observation at or before the bar close
panel = panel.join_asof(metrics, on="ts", strategy="backward")

# funding: the last settled rate at or before the close, and separately the rate
# whose settlement falls inside the bar, which is what the cost model charges
panel = panel.join_asof(funding.select("ts", "rate"), on="ts", strategy="backward")
panel = panel.with_columns(
    fund_settle_rate=pl.col("rate").filter(
        pl.col("settle_ts").is_between(bar_open, bar_close)
    )
)

Univers

Le lieu est le marché des perpétuels Binance margés en USDT et la fréquence est de 30 minutes. L’éligibilité se décide sur le seul trimestre de classement : coté en USDT, volume traité positif chaque jour d’octobre à décembre, pas un produit indiciel comme BTCDOMUSDT ou DEFIUSDT et pas une redénomination 1000X d’un contrat déjà inclus. 141 contrats passent. Le classement se fait par notionnel quotidien médian sur le trimestre, et l’univers est le top 30 et le bottom 30.

d = screen.filter(pl.col("rank_days_traded") >= pl.col("rank_days").max())
d = d.sort("median_daily_notional", descending=True)
keep = [s for s in d["symbol"] if s not in excluded]   # index + redenomination rules
H, L = keep[:30], keep[-30:]
ratio = median(notional[H]) / median(notional[L])      # 19.99 against a floor of 10

Les médianes réalisées sont de 138,8 M$ par jour dans la strate H contre 6,9 M$ dans la strate L, et le contrat le plus fin de l’univers gelé traitait encore 5,0 M$ par jour. Les contrats situés entre les deux strates ne sont pas tradés et n’existent que pour définir les rangs. Le plancher de séparation de 10 a été enregistré avant que le screen ne tourne, et une stratification en dessous aurait été abandonnée plutôt que resserrée jusqu’à passer.

L’existence d’un fichier n’est pas une preuve de trading. L’éligibilité comme la fin de l’historique d’un contrat utilisent le volume traité positif, et le remplissage de l’exchange après le dernier vrai trade est écarté. Les contrats qui meurent dans la période restent dans l’univers avec leur mort préservée, c’est pourquoi les données de MATICUSDT s’arrêtent à sa migration vers POL et celles d’EOSUSDT à son renommage en Vaulta plutôt qu’à un coup de balai de délistage. Un univers construit depuis la liste actuelle de l’exchange aurait supprimé les deux.

Familles

Le bras C compte huit familles d’indicateurs classiques, toutes uniquement prix : ATR, EMA, MACD, PPO, RSI, RSI_LEVEL, SMA et STOCHK. Le bras M-price compte neuf familles à mécanisme, également uniquement prix : volcomp_break, tsmom, session_mom, weekend_rev, volclimax, btc_relstr, gap_fade, range_rev et keltner_trend. Le bras M-exo compte onze familles qui lisent en plus le funding, l’open interest, la base, le flux d’ordres ou le positionnement : funding_capit, liq_snap, basis_fade, oi_breakout, flow_absorb, smart_follow, lev_stress, funding_settle, cvd_exhaust, retail_contra et oi_elastic.

Chaque famille du bras M porte un énoncé de mécanisme d’un paragraphe nommant la contrepartie, la raison pour laquelle elle traite sous contrainte, et l’observable qui sert de proxy à cette raison. Les énoncés sont sérialisés, hachés en SHA-256 et gelés avec le reste de la configuration avant qu’une seule barre ne soit lue. Ce sont des reconstructions écrites depuis le dossier de conception d’origine plutôt qu’un document scellé à l’époque, ce qui constitue la première limite de l’article.

btc_relstr lit le prix d’un second instrument : il est donc uniquement prix mais cross-asset. Il reste dans M-price et est signalé. Les statistiques de bras sont des moyennes équipondérées sur les familles d’un bras, donc le déséquilibre huit contre neuf n’oblige à supprimer aucune famille.

Une stratégie est le tuple (arm, family, variant, threshold, max_hold, stop). Le lookback et le reward-to-risk sont des boutons ajustables en échantillon plutôt que des stratégies distinctes, et ils sont resélectionnés à chaque fenêtre. Chaque famille des deux bras puise dans une seule bibliothèque partagée, copiée textuellement depuis le générateur qui a produit les deux corpus d’origine puis hachée.

TRANSFORMS = ["base", "slope", "normalized_price", "roc", "bias", "volZ", "accel",
              "disFromMedian", "quant_stretch", "rank_resid", "fold_dev"]          # 11
CONFL      = ["RSIge40", "RSIge50", "Pge0.7", "Pge0.8", "BW_filter", "pi", "vr",
              "kurtosis", "kurtosis10", "skew", "skew0.75", "atr_pct", "atr_pct0.8",
              "burstfreq", "TinyBody", "NoNewLowGreen", "RangeSpike", "YesterdayPeak",
              "DeadFlat10", "InsideBar", "SameDirection", "TopOfRange",
              "VolContraction", "EMAHug"]                                          # 24

VARIANTS = TRANSFORMS + CONFL      # 35
THR      = [1.0, 1.5, 2.0]         # entry threshold, in z units
MAXH     = [24, 96, 288]           # 12 hours, 2 days, 6 days
SLS      = [1.0, 2.0, 4.0]         # stop, in entry-ATR units
N_CONFIGS = 35 * 3 * 3 * 3         # 945 attempted configurations

945 tentatives par famille, par contrat, par fenêtre, dans les deux bras. C’est cela le budget de recherche égal : des configurations tentées égales plutôt qu’un nombre égal de boutons, et il diffère délibérément des corpus d’origine, où les familles de catalogue allaient de 12 à 8 932 configurations. Chaque tentative est persistée avec son motif, y compris celles qui ne tradent jamais, pour qu’une famille qui échoue souvent ne paraisse pas moins cherchée qu’une famille qui trade sans arrêt. Un événement de signal se produit quand l’état désiré courant est non nul et que l’état précédent était neutre ou opposé, de sorte qu’une condition persistante ne peut pas se redéclencher à chaque barre.

La table de scores réalisée compte 28 576 800 lignes, sur 60 contrats, 28 familles et 18 fenêtres, soit 1 680 unités contrat-famille portant chacune 945 configurations.

Exécution

L’ensemble d’information et la séquence de trade, dans l’ordre : features de la barre complétée t, état désiré après la barre t, entrée à l’ouverture de la barre t+1, barrières actives à partir de t+1, sortie au premier des trois : stop, cible, ou l’ouverture après H barres complètes détenues.

Chaque stratégie ne détient qu’une position au plus, un signal de même sens pendant qu’elle est occupée est ignoré, et un signal opposé sort et inverse à la prochaine ouverture observée, en payant deux exécutions. Les barrières sont mises à l’échelle par l’ATR plutôt qu’en pourcentages fixes, parce que sur un univers stratifié par liquidité un stop fixe à 1 % est une distance de risque différente sur une major et sur un alt fin, ce qui confondrait l’axe de liquidité avec un axe de volatilité.

Les barres qui touchent les deux barrières sont résolues en parcourant le chemin 1 minute et en retenant la barrière franchie en premier. Résoudre plutôt par l’ordre d’impression des extrêmes est une autre question, et cela a été mesuré comme biaisant les résultats jusqu’à 12,5 bp par trade sur une martingale sans drift, plus que tout le modèle de coûts. Quand une seule minute couvre les deux barrières, l’ordre est réellement inconnu à cette résolution, et le stop est pris en premier.

Coûts et funding

gross = direction * (exit_price / entry_price - 1.0)
fee   = 5e-4 * fills                                   # 5 bp per fill, both legs
slip  = (0.5 * spread_month + 0.5 * 0.5 * spread_month) * fills
fund  = sum(direction * rate_k * qty * mark_k for k in settlements_held)
net   = gross - fee - slip - fund                      # reconciles to 1e-9 per row

Les frais sont fixés à 5 basis points par exécution, le taux taker VIP0 des perpétuels Binance margés en USDT, et ils ne varient pas selon le contrat. Le spread est construit depuis daily/aggTrades comme la médiane du rebond entre transactions, la variation relative absolue de prix entre transactions consécutives qui changent de côté agresseur, prise le 8 et le 22 de chaque mois avec jusqu’à trois replis en avant lorsqu’un jour manque. Chaque exécution paie la moitié du spread mesuré plus encore la moitié de celui-ci en provision de slippage, si bien qu’un aller-retour paie un spread mesuré complet plus une provision d’impact de 50 %.

bookTicker, le flux de cotation direct, est inutilisable ici car l’archive ne le porte qu’à partir du 2023-05-16, ce qui laisserait le premier tiers de la période sans coût. Les estimateurs haut-bas sont proscrits : mesuré sur ce desk, Corwin-Schultz surestime le spread du BTC d’un facteur 568 à ces tailles de barre, ce qui veut dire qu’il lit la volatilité et non le spread.

L’estimateur a été validé avant gel contre une mesure bookTicker indépendante de BTCUSDT le 15 juillet 2023 et la reproduit exactement à 0.0330 basis points, sur 440 267 transactions et 172 496 changements de côté agresseur. Cet accord est un gate bloquant, pas une note.

Un contrat-mois avec des barres tradées et aucun spread mesurable est une défaillance de données et fait tomber le contrat. Un contrat-mois sans barre tradée est une absence structurelle et ne porte pas de spread, puisqu’il ne porte pas de position. Sur l’univers gelé, la répartition réalisée est de 3 607 contrat-mois mesurés, 593 structurellement absents et zéro défaillance.

Composante de coûtTraitement
Frais5 bp par exécution, à plat, sur les deux jambes
SpreadMesuré par contrat-mois, moitié facturée par exécution
SlippageEncore la moitié du spread mesuré par exécution
FundingÉvénement observé et signé à sa barre de règlement, valorisé au mark price publié

Un funding positif débite les longs et crédite les shorts, et le funding est calculé sur la position détenue immédiatement avant l’horodatage de règlement. Une entrée nouvelle à cet horodatage ne paie ni ne reçoit ; une position portée jusque-là et sortie à ce moment paie. La quantité de position est fixée au capital de la stratégie divisé par le prix d’entrée brut pour toute la vie du trade. Le funding n’est jamais un intrant de stratégie dans l’un ou l’autre bras, et dans le bras M-exo le chemin de coût et le chemin de feature sont calculés depuis des colonnes séparées, avec un gate explicite affirmant qu’aucune colonne de coût n’atteint la matrice de features.

Walk-forward

Les fenêtres sont ancrées au calendrier : un bloc de 12 mois calendaires en échantillon suivi d’un bloc de 3 mois calendaires hors échantillon, avançant de trois mois, dix-huit blocs couvrant du 1er janvier 2022 au 30 juin 2026.

def quarters():
    out, d = [], date(2022, 1, 1)
    while d < date(2026, 7, 1):
        nxt = next_quarter(d)
        out.append((date(d.year - 1, d.month, 1), d, nxt))   # IS start, OOS start, OOS end
        d = nxt
    return out                                               # 18 windows

Tous les contrats partagent les frontières de fenêtres, si bien qu’un contrat listé plus tard contribue à moins de fenêtres et qu’un contrat qui meurt en contribue encore moins. Les fenêtres ancrées aux barres, qu’ont utilisées les deux corpus d’origine, donnent à chaque contrat un calendrier différent et rendent les clusters de semaines incohérents à l’échelle du panel.

Dans chaque bloc en échantillon, les 945 tentatives sont scorées, le lookback et le reward-to-risk sont sélectionnés par le Sharpe en échantillon, et les boutons sélectionnés sont gelés puis appliqués une seule fois au bloc hors échantillon. Un trade dont l’intervalle de détention traverse la frontière est exclu entièrement du score en échantillon plutôt que tronqué. Le warm-up d’indicateur peut lire des barres antérieures au début d’un bloc, puisque ces barres étaient déjà connues, mais les lignes de warm-up ne contribuent jamais au résultat du bloc scoré.

Appariement

Une stratégie-fenêtre éligible compte au moins 20 trades clôturés en échantillon sur au moins 4 semaines calendaires distinctes et un Sharpe en échantillon fini. L’appariement se fait à l’intérieur d’une cellule (contract, window), jamais à travers une strate de liquidité, de façon gloutonne par distance absolue croissante de Sharpe en échantillon et sans remise des deux côtés, les égalités étant départagées par les deux identifiants de stratégie afin que l’appariement soit une fonction pure de la table d’entrée.

order_vals = np.sort(m_sharpe)                  # candidates within the caliper by
lo = np.searchsorted(order_vals, c_sharpe - 0.05, side="left")   # binary search, not a
hi = np.searchsorted(order_vals, c_sharpe + 0.05, side="right")  # |A| x |B| matrix
# pairs taken in ascending |distance|, without replacement, ties broken by (c_sid, m_sid)

Le caliper vaut 0,05 unité de Sharpe annualisé, avec un unique élargissement enregistré à 0,10 disponible si le gate de recouvrement échouait. Il n’a pas servi : 6 812 171 paires appariées à 0,05, et le gate de recouvrement enregistré exige au moins 200 paires appariées dans chaque fenêtre et au moins 60 % du bras le plus petit apparié au total, évalué par strate.

L’apparieur ne lit que des colonnes en échantillon. Le faire tourner contre une table dont les colonnes hors échantillon ont été supprimées produit un hash d’appariement identique, ce qui est un gate bloquant et non une affirmation. L’équilibre après appariement est rapporté comme différence moyenne standardisée du Sharpe en échantillon, et tout ce qui dépasse 0,10 serait une défaillance de l’implémentation d’appariement, pas un résultat.

Les identifiants de stratégie sont rendus uniques entre familles avant toute jointure. sid repart de zéro pour chaque contrat-famille, donc la famille EMA du bras C et la famille momentum du bras M portent toutes deux des identifiants de 0 à 944. Joindre sur le seul sid est ambigu, et sur un corpus à deux contrats cela a transformé 119 178 paires en 8 580 816 lignes.

Inférence

Des séries quotidiennes appariées de rendement net sont construites pour les deux bras depuis l’ensemble apparié. Dans chaque fenêtre hors échantillon fixe, le bootstrap rééchantillonne des clusters de semaines calendaires UTC du lundi au dimanche, et une frontière de trimestre coupe une semaine calendaire, ses jours de part et d’autre formant des fragments fenêtre-semaine distincts. Chaque contrat, famille, stratégie et bras à l’intérieur d’un fragment tiré voyage ensemble, ce qui préserve la dépendance transversale sans changer le nombre de clusters d’une fenêtre. Les données réalisées portent 250 fragments sur 1 642 jours de bourse.

rng = np.random.default_rng(20260810)
stat = []
for _ in range(10_000):
    draw = rng.integers(0, n_clusters, n_clusters)   # week fragments, not rows
    stat.append(diff[rows_in(draw)].mean())
point, (lo, hi) = observed_diff, np.percentile(stat, [2.5, 97.5])

10 000 réplications, seed 20260810, intervalle par percentiles. Le rééchantillonnage ligne à ligne est proscrit, puisque des fenêtres de prévision chevauchantes ne créent pas d’observations indépendantes. Les p-values bilatérales valent deux fois la plus petite queue du bootstrap avec la correction de réplications finies (count + 1) / (B + 1), plafonnées à un, avant d’entrer dans Benjamini-Hochberg. Aucun ajustement de multiplicité ne s’applique à l’unique contraste primaire enregistré, et les quatre secondaires forment une famille BH à 5 %.

La pente de décroissance est la seule statistique qui ne peut pas clusteriser sur les semaines. Son observation est une paire-fenêtre, un Sharpe en échantillon contre un rendement hors échantillon, elle clusterise donc sur la fenêtre elle-même et obtient 18 clusters au lieu de 250. Les intervalles de l’article sont larges en conséquence, et c’est une propriété de l’estimande plutôt qu’un choix.

Gates

Vingt gates bloquants tournent avant l’analyse, et chacun porte un défaut planté prouvant qu’il peut échouer. Un gate qui passe à la fois sur son entrée propre et sur son entrée cassée ne peut pas échouer et est déclaré cassé. Les vingt passent et les vingt défauts plantés sont attrapés.

Ils couvrent le hash de configuration gelée, l’univers et son ratio de séparation, la vivacité des familles dans les deux sens, des gates de régime filtrant mesurablement l’ensemble de candidats, la troncature du futur laissant les signaux antérieurs inchangés, les fuites de features, la résolution par premier contact sur le chemin 1 minute, le signe du funding, l’identité de coûts, le compte de 945 tentatives, l’ordre et la disjonction des fenêtres, l’aveuglement de l’apparieur aux colonnes hors échantillon, le recouvrement, l’équilibre après appariement, la symétrie à l’échange des étiquettes de bras, un résultat futur planté, une rotation d’actions à l’intérieur d’une fenêtre, les contrôles de clones et le corpus nul ci-dessous.

Deux contrôles supplémentaires méritent mention. Le moteur gagne zéro sur un chemin sans drift et colle à une référence de premier contact à 0,08 basis point par trade près. L’identité du ledger gross - fee - slippage - funding = net tient ligne par ligne et en agrégat, et après le défaut de funding non fini décrit dans l’article elle rejette désormais explicitement les valeurs non finies avant toute comparaison de magnitude, parce qu’un résidu non fini passe une comparaison de magnitude et certifie un ledger corrompu comme propre.

Le corpus nul

Le vocabulaire de règles partagé porte un artefact d’auto-référence : un signal dérivé de la série tradée, combiné à des barrières mises à l’échelle par la volatilité retardée de cette même série, ne rapporte pas zéro sur un corpus sans drift. Les deux bras y perdent environ 550 basis points du seul fait de l’artefact. C’est une propriété du vocabulaire et non un défaut du moteur, donc tester que le niveau vaut zéro ferait échouer un design qui est en fait correct.

Le test enregistré porte donc sur la différence, qui est l’estimande :

# same engine, same vocabulary, same matching, driftless GBM paths
D_null = matched_difference(null_corpus)
# 72.92 bp, 95% CI [-15.11, 166.60], 51,124 matched pairs, covers zero

Cet intervalle contient zéro, ce qu’exige le gate. Il représente aussi 33 % de l’effet observé au point estimé, avec une borne haute au-dessus de la borne basse de l’effet observé, et c’est pourquoi l’article rapporte le résultat principal et l’artefact ensemble plutôt que le résultat principal seul.

Chaque graphique de l’article est dessiné depuis un seul fichier JSON publié, exporté de ces artefacts. Le pipeline complet, le bundle de gel et le ledger trade par trade ne sont pas distribués depuis cette page. Si vous les voulez, écrivez-moi.

Retour à l’article.

← Les stratégies de trading ont-elles besoin d’une raison économique ?