← Retour aux articles
July 3, 2026
5 min de lecture

L'espace de parametres a deux axes : pourquoi la majeure partie de votre balayage devrait etre quasi gratuite

L'espace de parametres a deux axes : pourquoi la majeure partie de votre balayage devrait etre quasi gratuite
#algotrading
#backtest
#recherche de parametres
#optimisation
#mise en cache
#fleau de la dimensionnalite
Part 4 of 10 · Collection
High-Performance Backtest Engines

Fait partie de la serie "Des backtests sans illusions".

Le fleau de la dimensionnalite est generalement presente comme une mise en garde : chaque parametre que vous ajoutez multiplie l'espace de recherche, de sorte qu'une strategie a 18 dimensions est desesperee a balayer. Cette formulation suppose discretement que chaque dimension coute le meme prix a evaluer. Ce n'est pas le cas. Dans notre moteur a deux/trois horizons temporels, deux tiers des parametres sont presque gratuits a balayer, et un tiers porte pratiquement l'integralite de la facture de calcul. Une fois que vous voyez cette scission, "l'espace de recherche est trop grand" cesse d'etre la bonne plainte. La bonne question est : quelles dimensions payez-vous ?

La dimension qui n'avait pas besoin d'etre recalculee

Notre benchmark de recherche de parametres execute une strategie de momentum multi-horizons : une moyenne mobile de Hull et sa variante triplement lissee (HMA3) sur deux ou trois horizons temporels, avec une porte de separation directionnelle qui decide quand un croisement est suffisamment "propre" pour agir. L'espace de recherche complet pour la variante a trois horizons compte dix-huit dimensions : la periode et la longueur de HMA par horizon, plus un ensemble de seuils de separation pour l'entree et la sortie sur chaque horizon.

La maniere naive de balayer cela est une boucle unique : choisir un vecteur de parametres, construire chaque indicateur a partir de zero, executer la simulation, la noter, recommencer. Nous avons commence la. C'etait lent, et c'etait lent pour une raison qui s'est averee embarrassante une fois nommee : pour l'ecrasante majorite des essais adjacents, nous recalculions des indicateurs qui n'avaient pas change.

Deplacez le seuil de separation d'entree de 0.03 a 0.035 et relancez. La MA de Hull sur une annee de barres a 1 minute est identique bit pour bit a l'essai precedent : le seuil n'apparait nulle part dans sa definition. Pourtant la boucle naive la reconstruit quand meme, sur plus d'un demi-million de barres, trois horizons de profondeur, a chaque fois. Nous depensions la quasi-totalite de notre budget de calcul a rederiver des quantites invariantes au parametre que nous faisions effectivement varier.

Cette observation, c'est tout l'article. Des l'instant ou vous remarquez que certains parametres modifient les indicateurs et que d'autres ne modifient que la regle de decision appliquee a des indicateurs fixes, l'espace de parametres cesse d'etre un nuage plat de dimensions et devient deux couches imbriquees aux etiquettes de prix radicalement differentes.

Deux axes, pas un

Deux axes d'un espace de parametres a des etiquettes de prix differentes : une grille d'indicateurs couteuse et clairsemee dont chaque cellule debloque une feuille dense et quasi gratuite de configurations de seuils

Separez les parametres selon ce que leur reevaluation vous force a recalculer :

  • L'axe couteux — les parametres d'indicateurs. La periode de l'horizon temporel et la longueur de HMA. Changez l'un ou l'autre et vous devez reconstruire l'indicateur sur toute la serie de prix : reechantillonner l'horizon superieur, executer la ponderation de Hull, recalculer la serie de croisements et la separation a chaque croisement. C'est un balayage en O(n) par barre d'historique, et vous le payez integralement pour chaque combinaison distincte. Dans la strategie a trois horizons, cet axe compte six parametres : (period, hma_length) pour chacun des horizons haut, moyen et bas.

  • L'axe bon marche — les seuils de decision. Les portes de separation directionnelle qui decident, etant donne les series de croisements et de separation deja calculees, s'il faut entrer ou sortir. Changez un seuil et aucun indicateur ne bouge. Vous ne relancez qu'un seul balayage en O(n) sur les signaux precalcules, en verifiant les portes et en enregistrant les executions. Dans la strategie a trois horizons, cet axe compte douze parametres (quatre seuils de separation — achat a l'entree, vente a l'entree, achat a la sortie, vente a la sortie — sur chacun des trois horizons). La variante a deux horizons en compte huit (quatre seuils sur chacun des deux horizons).

Donc la forme reelle de l'espace est : 6 couteux + 12 bon marche = 18 pour trois horizons, et 4 couteux + 8 bon marche = 12 pour deux horizons. Deux tiers de la dimensionnalite vivent sur l'axe bon marche. Et les deux axes ne different pas seulement en nombre : ils different en cout unitaire de plus de trois ordres de grandeur, ce qui est le chiffre dont parle le reste de cet article.

La structure est strictement imbriquee. Chaque point sur l'axe couteux — un (period, hma_length) concret par horizon — definit un ensemble de tableaux de signaux fixes. Au-dessus de cette fondation fixe repose une feuille entiere de l'axe bon marche : des milliers de vecteurs de seuils, chacun etant un balayage rapide sur les memes tableaux. Vous payez la fondation une fois et l'amortissez sur toute la feuille.

Pourquoi les indicateurs sont invariants aux seuils

La mise en cache ne fonctionne qu'en raison d'un fait mathematique, et non d'un heureux detail d'implementation, et il vaut la peine de l'enoncer precisement parce qu'il constitue l'hypothese porteuse. Les tableaux d'indicateurs sont une fonction de (period, hma_length) uniquement. Les seuils de separation n'apparaissent nulle part dans leur definition.

Concretement, pour chaque horizon temporel, le moteur precalcule quatre tableaux alignes sur l'index de base : la MA de Hull hma, sa version triplement lissee hma3, la serie de croisements cross (+1 achat / −1 vente / 0), et le pourcentage de separation a chaque croisement. Chacun d'eux est derive des prix et des deux parametres d'indicateur. Le seuil — le nombre auquel vous comparez separation — est applique plus tard, au moment de la decision. Il ne touche jamais aux tableaux.

Il ne s'agit pas d'un rearrangement trivialement sur. Il n'est sur que parce que nous avons ete attentifs au moment ou l'information devient disponible, qui est la preoccupation du travail sur le biais de look-ahead ailleurs dans cette serie. L'indicateur d'horizon superieur sur la barre de base i est calcule a partir de bougies d'horizon superieur closes plus une bougie en formation dont la cloture courante est egale a la cloture de base actuelle close[i] — une valeur connue a la barre i. Rien du futur ne s'infiltre. Ainsi, le signal precalcule a la barre i est exactement ce qu'un bot en direct aurait vu a la barre i, et il reste valide quel que soit le seuil que vous testez ensuite contre lui. Mettre en cache un signal qui fuit ne ferait que mettre la fuite en cache ; mettre en cache un signal causal met en cache quelque chose que vous pouvez reellement trader.

L'invariance nous donne une factorisation. Ecrivez l'evaluation d'un vecteur de parametres complet θ = (indicator_params, threshold_params) ainsi :

signals   = build_indicators(indicator_params)      # EXPENSIVE, depends only on indicator_params
score     = simulate(signals, threshold_params)     # CHEAP, reuses signals across all thresholds

build_indicators ne lit pas threshold_params. Ce seul fait est ce qui autorise le cache : maintenez indicator_params fixe, faites varier threshold_params librement, et signals est une constante que vous calculez une seule fois.

L'architecture : calculer une fois, balayer plusieurs fois

Un cache de signaux indexe par parametres d'indicateurs alimentant un balayage par lots rapide : une seule construction d'indicateurs se deploie en une large rangee d'evaluations de seuils partageant les memes tableaux mis en cache

Le moteur (scripts/engine_multitf.py, commit bfc8aaa dans notre backtester) implemente la factorisation avec deux pieces dont les noms disent exactement ce qu'elles font.

SignalCache — l'axe couteux, memoise. C'est un dictionnaire indexe par (period_bars, hma_length). Demandez-lui les signaux d'un horizon temporel et il renvoie le TFSignals mis en cache si cette combinaison d'indicateurs a deja ete construite, sinon il les construit une fois et les stocke. Comme la cle n'est constituee que des parametres d'indicateurs, chaque configuration de seuils qui partage une combinaison d'indicateurs — et dans un balayage dense de seuils, il y en a des milliers — touche la meme entree de cache. Les horizons superieurs recompensent particulierement cela : une grille grossiere de, disons, quatre periodes candidates fois une poignee de longueurs de HMA represente un petit nombre de constructions couteuses distinctes, et chacune est reutilisee sur toute la feuille de seuils empilee au-dessus.

sweep_separations — l'axe bon marche, par lots. Il prend les tableaux de signaux mis en cache et une matrice de vecteurs de seuils (sps, de forme [m, 12]) et les fait tous passer par un unique noyau compile. Chaque ligne est un balayage en O(n) : parcourir les barres, appliquer les portes, enregistrer des executions sans fuite a open[i+1], cumuler le PnL et le temps en position. Aucun indicateur n'est reconstruit a l'interieur de cette boucle — elle lit cross et separation directement depuis le cache. La simulation interne est compilee JIT (Numba), de sorte qu'une fois rechauffee, le cout par configuration est domine par l'unique balayage lineaire sur les barres, et non par la surcharge de Python.

Les deux pieces se composent en la recherche imbriquee naturelle : la boucle externe parcourt l'axe couteux (chaque iteration construit et met en cache une combinaison d'indicateurs), et la boucle interne deploie un large lot sur l'axe bon marche sur ces tableaux mis en cache. Dans le code, la forme est exactement cela — une petite boucle couteuse enveloppant un large lot bon marche :

cache = SignalCache(base_close, base_ts)           # keyed by (period, hma_length)

for htf_p, htf_h, mtf_p, mtf_h, ltf_p, ltf_h in indicator_grid:   # EXPENSIVE axis (coarse)
    htf = cache.get(htf_p, htf_h)                  # built once, then a cache hit forever
    mtf = cache.get(mtf_p, mtf_h)
    ltf = cache.get(ltf_p, ltf_h)

    sps = sample_thresholds(m=4000)                # CHEAP axis: [m, 12] threshold vectors
    pnl, n_trades, bars_in_pos = sweep_separations(  # one compiled batch, no indicator work
        base_close, base_open, htf, ltf, sps, mtf=mtf)

Vous touchez au constructeur couteux aussi peu de fois que la grille d'indicateurs le permet, et vous laissez le balayage bon marche faire le volume — des milliers de lignes sps contre des tableaux qui ne bougent jamais. C'est toute l'optimisation — pas d'approximation, pas de fidelite perdue, juste le refus de recalculer ce qui n'a pas change.

Ce que "gratuit" coute reellement : les chiffres

Une comparaison en barres du cout par configuration : une haute colonne pour le recalcul par configuration a cote d'un mince trait pour le balayage de seuils mis en cache, annotee de l'ecart d'environ 1 600x

Nous avons mesure les deux axes sur la charge de travail de demonstration : une annee complete de barres ETHUSDT a 1 minute (~527k barres), trois horizons, indicateurs rechauffes et compilation JIT exclue du chronometrage.

Sur l'axe bon marche, sweep_separations soutient ~5 600 configurations de seuils par seconde. C'est une simulation complete — portes, executions, PnL, exposition — pour chaque configuration, sur plus d'un demi-million de barres, a environ 180 microsecondes par configuration. La raison pour laquelle elle peut etre aussi rapide est qu'elle ne fait aucun travail d'indicateur : chaque configuration lit les memes tableaux cross et separation mis en cache.

Chiffrez maintenant l'alternative. Construire l'ensemble d'indicateurs a trois horizons une seule fois prend de l'ordre de quelques centaines de millisecondes (~0.3 s) — reechantillonnage de trois horizons, ponderation de Hull, extraction des croisements et de la separation sur toute l'annee. Si vous recalculiez les indicateurs a l'interieur de la boucle de configuration — la conception naive a boucle unique — chacune de ces 5 600 configurations par seconde paierait a la place la construction complete des indicateurs. Le cout par configuration explose de ~180 microsecondes a ~0.3 seconde :

Cout par configuration Configurations/s
Axe bon marche (signaux en cache) ~180 µs ~5 600
Recalcul des indicateurs par configuration ~0.3 s ~3.4

Le rapport est de ~1 600x. Balayer l'axe bon marche sur des signaux mis en cache est environ trois ordres de grandeur moins cher que la conception naive qui reconstruit les indicateurs pour chaque vecteur de seuils. Concretement : un lot de quelques milliers de configurations de seuils qui se termine en moins d'une seconde sur le chemin mis en cache prendrait la majeure partie d'une heure si chaque configuration reconstruisait ses indicateurs. Memes resultats, meme fidelite, aucun raccourci dans les calculs — la seule difference est que l'une recalcule un invariant et l'autre non.

Ce n'est pas une micro-optimisation que l'on saupoudre a la fin. Elle change ce que les recherches sont faisables. A 5 600 cfg/s, l'axe des seuils est assez dense pour etre explore correctement — vous pouvez vous offrir une grille fine ou un long echantillon aleatoire/QMC par combinaison d'indicateurs — tandis que l'axe couteux reste une poignee deliberement grossiere de constructions. Le budget de calcul se dirige la ou les parametres coutent effectivement quelque chose.

Le fleau de la dimensionnalite, remis a son juste prix

Une grille uniforme et plate de parametres reetiquetee en une carte de prix a deux niveaux : quelques cellules couteuses fortement encadrees, le reste ombre comme quasi gratuit

Revenez au cadrage sur lequel cet article s'est ouvert. Le fleau de la dimensionnalite dit que l'espace de recherche croit exponentiellement avec le nombre de parametres, de sorte que davantage de dimensions est categoriquement pire. C'est vrai a propos de la taille de la grille. C'est trompeur a propos du cout de sa couverture, parce qu'il chiffre chaque dimension a l'identique.

Une fois que vous separez les axes, le compte se lit differemment. La strategie a trois horizons est a 18 dimensions, mais seulement 6 de ces dimensions sont couteuses. Les 12 autres sont des dimensions de l'axe bon marche qui elargissent la grille sans elargir la facture de calcul de maniere significative — vous pouvez leur lancer des milliers de configurations pour quelques centimes. La strategie a deux horizons est a 12 dimensions avec seulement 4 dimensions couteuses et 8 bon marche. Dans les deux cas, la majorite du "fleau" est concentree sur l'axe qui ne coute presque rien a balayer.

Ainsi, la maniere honnete de raisonner sur le cout de recherche n'est pas "combien de parametres" mais "combien de parametres couteux, et jusqu'a quel point leur grille peut-elle etre grossiere". L'axe couteux est la ou l'exponentielle fait vraiment mal, et c'est la ou vous voulez une grille petite et bien choisie — quelques periodes candidates, une plage modeste de longueurs de HMA — possiblement raffinee du grossier au fin, dans l'esprit du zoom a resolution adaptative que nous utilisons ailleurs. L'axe bon marche est la ou vous pouvez etre genereux, parce que chaque configuration de seuils supplementaire coute 180 microsecondes.

Cette remise a plat des prix se generalise bien au-dela de notre strategie HMA. Le motif — certains parametres modifient les features, la plupart des parametres modifient la regle appliquee a des features fixes — se retrouve partout dans le trading systematique. Les longueurs d'indicateurs, les frequences de reechantillonnage et les fenetres de lookback sont couteuses ; les seuils d'entree/sortie, les distances de stop, les multiplicateurs de dimensionnement de position et les portes de confirmation sont bon marche. Chaque fois qu'un parametre ne fait que remodeler la frontiere de decision sur des signaux deja calcules, il appartient a l'axe bon marche, et c'est la qu'il doit etre balaye. Des gains de mise en cache apparentes de la meme famille apparaissent dans la mise en cache parquet multi-horizons et dans l'echelle de vitesse du moteur plus large.

La ou le repas gratuit a une facture

L'axe bon marche est bon marche en calcul. Il n'est pas bon marche en statistiques, et confondre les deux est la maniere de transformer un gain de performance en machine a surapprentissage.

Chaque configuration de seuils que vous evaluez est un essai, et quand vous executez des milliers d'essais sur un seul jeu de donnees, le meilleur d'entre eux parait bon en partie par chance. Rendre ces essais 1 600x moins chers ne fait pas disparaitre la chance — cela la rend plus facile a accumuler. Un balayage dense de seuils est precisement la situation ou l'inflation des tests multiples mord le plus fort : de nombreux candidats, un seul historique, et une regle de selection qui rapporte le maximum. Le moteur le plus rapide au monde vous remettra volontiers un vecteur de seuils qui epouse magnifiquement le bruit de votre fenetre de test et echoue hors echantillon.

Ainsi, la discipline doit croitre avec la vitesse. Des l'instant ou le cout de calcul d'un essai tend vers zero, la comptabilite statistique devient la contrainte contraignante, et vous devez la payer explicitement :

  • Deflatez pour le nombre d'essais. Notez le gagnant par rapport au nombre de configurations que vous avez essayees, et non par rapport a zero. Le ratio de Sharpe deflate et la probabilite de surapprentissage du backtest existent exactement pour cela — ils transforment "nous avons essaye 4 000 vecteurs de seuils" en une decote sur l'avantage rapporte.
  • Validez hors echantillon, par pli. Un balayage bon marche doit tout de meme etre execute a l'interieur d'une decoupe walk-forward honnete ; un seuil qui ne gagne qu'en echantillon ne vaut rien, quelle que soit la vitesse a laquelle vous l'avez trouve. Notre moteur garde les executions sans fuite (open[i+1]) precisement pour que l'axe bon marche ne puisse pas acheter de la performance en trichant.
  • Preferez les plateaux aux pics. Comme l'axe des seuils est dense et rapide, vous pouvez cartographier sa surface de reponse, et pas seulement son argmax. Une large region de seuils qui fonctionnent tous est un avantage reel ; un unique maximum en pointe est un artefact ajuste — la distinction plateau contre pic, rendue abordable par la vitesse meme que nous decrivons.

Lisez la structure a deux axes de la bonne maniere : elle ne vous achete pas plus de confiance, elle vous achete plus d'essais au meme cout de confiance par essai. C'est reellement precieux — une couverture dense de l'axe bon marche est ce qui vous permet de trouver des plateaux et de caracteriser la surface — mais seulement si vous gardez le grand livre statistique honnete. La vitesse supprime l'excuse de calcul pour ne pas chercher a fond ; elle ne supprime pas l'obligation d'escompter ce que la recherche approfondie trouve.

Comment structurer votre propre recherche

Pour appliquer ceci a votre propre strategie, le travail est surtout de la classification — trier vos parametres sur le bon axe — suivi d'une boucle imbriquee :

  1. Etiquetez chaque parametre selon ce qu'il vous force a recalculer. Si le changer change un tableau d'indicateur/feature, il est couteux. S'il ne change qu'une comparaison, un seuil ou une regle de dimensionnement appliquee a des tableaux fixes, il est bon marche. En cas de doute, demandez : ce parametre apparait-il quelque part dans la definition de l'indicateur ? Si non, il est bon marche.
  2. Indexez un cache sur le seul axe couteux. Memoisez la construction des features par leurs parametres d'indicateurs (comme SignalCache le fait par (period, hma_length)). Les essais adjacents qui partagent une combinaison d'indicateurs reutilisent alors les memes tableaux gratuitement.
  3. Traitez l'axe bon marche par lots sur les features mises en cache. Executez les configurations de seuils comme une boucle compilee serree sur les signaux precalcules, et non comme des reevaluations completes. C'est de la que provient votre debit — et dans notre cas les ~5 600 cfg/s.
  4. Imbriquez les boucles : couteux a l'exterieur, bon marche a l'interieur. Gardez la grille couteuse petite et deliberee (grossiere, ou du grossier au fin) ; laissez le balayage bon marche etre dense. Depensez le budget la ou les parametres coutent quelque chose.
  5. Budgetez les essais contre le surapprentissage, et non contre l'horloge. Maintenant que l'horloge n'est plus la limite, laissez le Sharpe deflate / PBO l'etre. Decidez combien d'essais sur l'axe bon marche vous pouvez vous offrir statistiquement, et validez le gagnant hors echantillon.

Le gain d'ingenierie et le garde-fou statistique sont deux faces d'une meme idee : separer les axes vous permet de chercher les dimensions bon marche de maniere exhaustive, ce qui est exactement pourquoi vous devez ensuite escompter a quel point vous avez cherche exhaustivement.

A retenir

  1. Toutes les dimensions ne coutent pas le meme prix. Les parametres d'une strategie se scindent en un axe couteux (les indicateurs — recalcules sur toute la serie) et un axe bon marche (les seuils — un balayage en O(n) sur des signaux precalcules). Dans notre moteur, c'est 6 couteux + 12 bon marche pour trois horizons, 4 + 8 pour deux horizons.
  2. Les indicateurs sont invariants aux seuils, et cette invariance est toute l'optimisation. La construction des features ne depend que des parametres d'indicateurs, donc vous construisez une fois, mettez en cache par (period, hma_length), et reutilisez sur chaque configuration de seuils qui partage la combinaison.
  3. L'axe bon marche s'execute ~1 600x moins cher. ~5 600 configurations de seuils/s (~180 µs chacune) sur des signaux mis en cache, contre ~0.3 s par configuration si vous reconstruisez les indicateurs a chaque fois. Meme fidelite — la seule difference est le refus de recalculer un invariant.
  4. Le fleau de la dimensionnalite est en realite un fleau de la dimensionnalite couteuse. La majorite du nombre de parametres vit sur l'axe qui est quasi gratuit a balayer. Gardez la grille couteuse grossiere ; soyez genereux sur celle qui est bon marche.
  5. La vitesse deplace la contrainte contraignante du calcul vers les statistiques. Un balayage dense et rapide est une machine a tests multiples. Deflatez pour le nombre d'essais, validez par pli, et preferez les plateaux aux pics — le repas gratuit est reel, mais la facture statistique n'est pas optionnelle.

Le moteur complet — SignalCache, sweep_separations, la simulation multi-horizons sans fuite, et le test de parite qui l'ancre a la semantique de la bougie en formation en direct — reside dans scripts/engine_multitf.py (commit bfc8aaa) dans notre backtester. La prochaine fois que quelqu'un vous dira qu'une strategie a 18 parametres est trop grande a explorer, demandez-lui combien de ces parametres bougent effectivement les indicateurs. Habituellement c'est un tiers d'entre eux, et le reste est quasi gratuit.

blog.disclaimer

Authors

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

Gardez une longueur d'avance sur le marché

Abonnez-vous à notre newsletter pour des insights exclusifs sur le trading IA, des analyses de marché et des mises à jour de la plateforme.

Nous respectons votre vie privée. Désabonnement possible à tout moment.