← Terug naar artikelen
July 3, 2026
5 min leestijd

De parameterruimte met twee assen: waarom het grootste deel van je sweep bijna gratis zou moeten zijn

De parameterruimte met twee assen: waarom het grootste deel van je sweep bijna gratis zou moeten zijn
#algotrading
#backtest
#parameterzoektocht
#optimalisatie
#caching
#vloek van de dimensionaliteit
Part 4 of 10 · Collection
High-Performance Backtest Engines

Onderdeel van de serie "Backtests zonder illusies".

De vloek van de dimensionaliteit wordt gewoonlijk verteld als een waarschuwing: elke parameter die je toevoegt vermenigvuldigt de zoekruimte, dus een strategie met 18 dimensies is hopeloos om te sweepen. Dat kader neemt stilzwijgend aan dat elke dimensie evenveel kost om te evalueren. Dat is niet zo. In onze engine met dubbele/drievoudige timeframes zijn twee derde van de parameters bijna gratis om te sweepen, en een derde draagt vrijwel de volledige rekenrekening. Zodra je die splitsing ziet, is "de zoekruimte is te groot" niet langer de juiste klacht. De juiste vraag is: welke dimensies betaal je?

De dimensie die niet herberekend hoefde te worden

Onze benchmark voor parameterzoektocht draait een momentumstrategie over meerdere timeframes: een Hull moving average en zijn drievoudig gladgestreken variant (HMA3) op twee of drie timeframes, met een gate voor directionele scheiding die bepaalt wanneer een crossover "schoon" genoeg is om op te handelen. De volledige zoekruimte voor de drievoudige-TF-variant is achttien dimensies — periode en HMA-lengte per timeframe, plus een set scheidingsdrempels voor entry en exit op elk timeframe.

De naïeve manier om dit te sweepen is een enkele loop: kies een parametervector, bouw elke indicator vanaf nul, draai de simulatie, scoor hem, herhaal. Wij begonnen daar. Het was traag, en het was traag om een reden die gênant bleek te zijn zodra we hem een naam gaven: voor de overweldigende meerderheid van naburige trials herberekenden we indicatoren die niet waren veranderd.

Verschuif de entry-scheidingsdrempel van 0.03 naar 0.035 en draai opnieuw. De Hull MA over een jaar aan 1-minuutbars is bit-voor-bit identiek aan de vorige trial — de drempel komt nergens voor in de definitie ervan. Toch bouwt de naïeve loop hem hoe dan ook opnieuw op, over meer dan een half miljoen bars, drie timeframes diep, elke keer opnieuw. We besteedden vrijwel ons hele rekenbudget aan het opnieuw afleiden van grootheden die invariant waren ten opzichte van de parameter die we daadwerkelijk varieerden.

Die observatie is het hele artikel. Zodra je opmerkt dat sommige parameters de indicatoren veranderen en sommige alleen de beslissingsregel die op vaste indicatoren wordt toegepast, stopt de parameterruimte een platte wolk van dimensies te zijn en wordt hij twee geneste lagen met wild uiteenlopende prijskaartjes.

Twee assen, niet één

Twee assen van een parameterruimte met verschillende prijskaartjes: een schaars, duur indicatorraster waarvan elke cel een dicht, bijna gratis vel drempelconfiguraties ontsluit

Splits de parameters op naar wat het herevalueren ervan je dwingt te herberekenen:

  • De dure as — indicatorparameters. Timeframe-periode en HMA-lengte. Verander een van beide en je moet de indicator over de hele prijsreeks opnieuw opbouwen: het hogere timeframe herbemonsteren, de Hull-weging uitvoeren, de crossover-reeks en de scheiding bij elke cross herberekenen. Dit is een O(n)-sweep per bar aan historie, en je betaalt hem volledig voor elke afzonderlijke combinatie. In de drievoudige-TF-strategie is deze as zes parameters: (period, hma_length) voor elk van de hoge, midden- en lage timeframes.

  • De goedkope as — beslissingsdrempels. De gates voor directionele scheiding die, gegeven de reeds berekende crossover- en scheidingsreeksen, beslissen of er entry of exit plaatsvindt. Verander een drempel en er beweegt geen enkele indicator. Je draait alleen een enkele O(n)-pass over de vooraf berekende signalen opnieuw, controleert de gates en boekt fills. In de drievoudige-TF-strategie is deze as twaalf parameters (vier scheidingsdrempels — entry-buy, entry-sell, exit-buy, exit-sell — op elk van drie timeframes). De dubbele-TF-variant heeft er acht (vier drempels op elk van twee timeframes).

Dus de werkelijke vorm van de ruimte is: 6 duur + 12 goedkoop = 18 voor drievoudige-TF, en 4 duur + 8 goedkoop = 12 voor dubbele-TF. Twee derde van de dimensionaliteit leeft op de goedkope as. En de twee assen verschillen niet alleen in aantal — ze verschillen in kosten per eenheid met meer dan drie ordes van grootte, en dat is het getal waar de rest van dit artikel over gaat.

De structuur is strikt genest. Elk punt op de dure as — één concrete (period, hma_length) per timeframe — definieert een set vaste signaalarrays. Bovenop dat vaste fundament ligt een heel vel van de goedkope as: duizenden drempelvectoren, elk een snelle pass over dezelfde arrays. Je betaalt eenmalig voor het fundament en amortiseert het over het hele vel.

Waarom indicatoren invariant zijn ten opzichte van drempels

De caching werkt alleen dankzij een wiskundig feit, geen gelukkig implementatiedetail, en het is de moeite waard om het precies te formuleren omdat het de dragende aanname is. De indicatorarrays zijn een functie van (period, hma_length) alleen. De scheidingsdrempels komen nergens voor in hun definitie.

Concreet berekent de engine voor elk timeframe vier uitgelijnde arrays over de basisindex vooraf: de Hull MA hma, zijn drievoudig gladgestreken hma3, de crossover-reeks cross (+1 buy / −1 sell / 0), en het separation-percentage bij elke cross. Elk hiervan is afgeleid uit prijzen en de twee indicatorparameters. De drempel — het getal waar je separation tegen vergelijkt — wordt later toegepast, op het moment van beslissen. Hij raakt de arrays nooit aan.

Dit is geen triviaal veilige herschikking. Het is alleen veilig omdat we zorgvuldig zijn geweest over wanneer informatie beschikbaar wordt, wat de zorg is van het werk rond look-ahead bias elders in deze serie. De indicator van het hogere timeframe op basisbar i wordt berekend uit gesloten hogere-TF-candles plus een vormende candle waarvan de lopende close gelijk is aan de huidige basisclose close[i] — een waarde die bekend is op bar i. Niets uit de toekomst lekt binnen. Het vooraf berekende signaal op bar i is dus precies wat een live bot op bar i zou hebben gezien, en het blijft geldig ongeacht welke drempel je er later tegen test. Een lekkend signaal cachen zou gewoon het lek cachen; een causaal signaal cachen cachet iets waar je daadwerkelijk mee kunt handelen.

De invariantie geeft ons een factorisatie. Schrijf de evaluatie van één volledige parametervector θ = (indicator_params, threshold_params) als:

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

build_indicators leest threshold_params niet. Dat ene feit is wat de cache mogelijk maakt: houd indicator_params vast, varieer threshold_params vrijelijk, en signals is een constante die je eenmalig berekent.

De architectuur: bereken eenmalig, sweep veelvuldig

Een signaalcache met indicatorparameters als sleutel die een snelle batch-sweep voedt: één indicatorbouw waaiert uit in een brede rij drempelevaluaties die dezelfde gecachte arrays delen

De engine (scripts/engine_multitf.py, commit bfc8aaa in onze backtester) implementeert de factorisatie met twee onderdelen waarvan de namen precies zeggen wat ze doen.

SignalCache — de dure as, gememoïseerd. Het is een dictionary met (period_bars, hma_length) als sleutel. Vraag hem om de signalen van een timeframe en hij retourneert de gecachte TFSignals als die indicatorcombinatie eerder is gebouwd, anders bouwt hij ze eenmalig en slaat ze op. Omdat de sleutel alleen de indicatorparameters is, treft elke drempelconfiguratie die een indicatorcombinatie deelt — en in een dichte drempel-sweep zijn dat er duizenden — dezelfde cache-entry. De hogere timeframes belonen dit vooral: een grofmazig raster van, zeg, vier kandidaat-periodes maal een handjevol HMA-lengtes is een klein aantal afzonderlijke dure bouwsels, en elk ervan wordt hergebruikt over het hele drempelvel dat erbovenop is gestapeld.

sweep_separations — de goedkope as, in batches. Het neemt de gecachte signaalarrays en een matrix van drempelvectoren (sps, vorm [m, 12]) en draait ze allemaal door één gecompileerde kernel. Elke rij is één O(n)-pass: loop door de bars, pas de gates toe, boek lekvrije fills op open[i+1], tel PnL en tijd-in-positie op. Binnen deze loop wordt geen enkele indicator opnieuw opgebouwd — hij leest cross en separation rechtstreeks uit de cache. De binnenste simulatie is JIT-gecompileerd (Numba), dus zodra hij is opgewarmd worden de kosten per configuratie gedomineerd door de enkele lineaire scan over de bars, niet door Python-overhead.

De twee onderdelen componeren tot de natuurlijke geneste zoektocht: de buitenste loop loopt langs de dure as (elke iteratie bouwt en cachet één indicatorcombinatie), en de binnenste loop waaiert een brede batch uit over de goedkope as op die gecachte arrays. In code is de vorm precies dat — een kleine dure loop die een brede goedkope batch omvat:

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)

Je raakt de dure builder zo weinig aan als het indicatorraster toestaat, en je laat de goedkope sweep het volume doen — duizenden sps-rijen tegen arrays die nooit bewegen. Dat is de hele optimalisatie — geen benadering, geen verloren getrouwheid, gewoon weigeren om te herberekenen wat niet veranderd is.

Wat "gratis" werkelijk kost: de cijfers

Een balkvergelijking van kosten per configuratie: een hoge kolom voor herberekenen-per-configuratie naast een sliver voor de gecachte drempel-pass, geannoteerd met de kloof van ruwweg 1.600x

We hebben de twee assen gemeten op de demo-werklast: een vol jaar aan 1-minuut ETHUSDT-bars (~527k bars), drievoudige-TF, met indicatoren opgewarmd en de JIT-compilatie buiten de timing gehouden.

Op de goedkope as houdt sweep_separations ~5.600 drempelconfiguraties per seconde vol. Dat is een volledige simulatie — gates, fills, PnL, blootstelling — voor elke configuratie, over een half miljoen bars, met ruwweg 180 microseconden per configuratie. De reden dat het zo snel kan zijn, is dat het geen enkel indicatorwerk doet: elke configuratie leest dezelfde gecachte cross- en separation-arrays.

Prijs nu het alternatief. Het eenmalig bouwen van de drievoudige-TF-indicatorset kost in de orde van een paar honderd milliseconden (~0.3 s) — drie timeframes herbemonsteren, Hull-weging, crossover- en scheidingsextractie over het hele jaar. Als je indicatoren binnen de configuratie-loop herberekende — het naïeve enkele-loop-ontwerp — zou elk van die 5.600 configuraties per seconde in plaats daarvan de volledige indicatorbouw betalen. De kosten per configuratie zwellen op van ~180 microseconden naar ~0.3 seconden:

Kosten per configuratie Configuraties/sec
Goedkope as (gecachte signalen) ~180 µs ~5.600
Indicatoren per configuratie herberekenen ~0.3 s ~3,4

De verhouding is ~1.600x. De goedkope as sweepen op gecachte signalen is ruwweg drie ordes van grootte goedkoper dan het naïeve ontwerp dat indicatoren opnieuw opbouwt voor elke drempelvector. Concreet gesteld: een batch van een paar duizend drempelconfiguraties die op het gecachte pad binnen een seconde klaar is, zou het beste deel van een uur kosten als elke configuratie zijn indicatoren opnieuw opbouwde. Dezelfde resultaten, dezelfde getrouwheid, geen kortere weggetjes in de wiskunde — het enige verschil is dat de een een invariant herberekent en de ander niet.

Dit is geen micro-optimalisatie die je op het einde erbij strooit. Het verandert welke zoektochten haalbaar zijn. Bij 5.600 cfg/s is de drempelas dicht genoeg om goed te verkennen — je kunt je een fijn raster of een lange random/QMC-steekproef per indicatorcombinatie veroorloven — terwijl de dure as een bewust grofmazig handjevol bouwsels blijft. Het rekenbudget stroomt naar waar de parameters daadwerkelijk iets kosten.

De vloek van de dimensionaliteit, herprijsd

Een plat, uniform raster van parameters omgelabeld tot een tweelaags prijskaart: een paar kostbare cellen zwaar omlijnd, de rest gearceerd als bijna gratis

Ga terug naar het kader waar dit artikel mee opende. De vloek van de dimensionaliteit zegt dat de zoekruimte exponentieel groeit met het aantal parameters, dus meer dimensies is categorisch slechter. Dat klopt wat betreft de omvang van het raster. Het is misleidend wat betreft de kosten om het te dekken, omdat het elke dimensie identiek prijst.

Zodra je de assen scheidt, leest het aantal anders. De drievoudige-TF-strategie heeft 18 dimensies, maar slechts 6 van die dimensies zijn duur. De andere 12 zijn goedkope-as-dimensies die het raster uitbreiden zonder de rekenrekening op enige betekenisvolle manier uit te breiden — je kunt er voor een habbekrats duizenden configuraties tegenaan gooien. De dubbele-TF-strategie heeft 12 dimensies met slechts 4 dure dimensies en 8 goedkope. In beide gevallen is het merendeel van de "vloek" geconcentreerd op de as die vrijwel niets kost om te sweepen.

Dus de eerlijke manier om over zoekkosten na te denken is niet "hoeveel parameters" maar "hoeveel dure parameters, en hoe grofmazig mag hun raster zijn." De dure as is waar het exponentiële daadwerkelijk pijn doet, en het is waar je een klein, weloverwogen raster wilt — een paar kandidaat-periodes, een bescheiden bereik aan HMA-lengtes — mogelijk grof-naar-fijn verfijnd, in de geest van de adaptieve-resolutie drill-down die we elders gebruiken. De goedkope as is waar je overvloedig mag zijn, omdat elke extra drempelconfiguratie 180 microseconden is.

Deze herprijzing generaliseert ver voorbij onze HMA-strategie. Het patroon — sommige parameters veranderen de features, de meeste parameters veranderen de regel die op vaste features wordt toegepast — komt overal terug in systematisch handelen. Indicatorlengtes, herbemonsteringsfrequenties en lookback-vensters zijn duur; entry/exit-drempels, stopafstanden, position-sizing-vermenigvuldigers en bevestigingsgates zijn goedkoop. Telkens wanneer een parameter alleen de beslissingsgrens over reeds berekende signalen hervormt, hoort hij thuis op de goedkope as, en zou hij daar gesweept moeten worden. Verwante caching-winsten uit dezelfde familie duiken op in multi-timeframe parquet-caching en in de bredere snelheidsladder van de backtest-engine.

Waar de gratis lunch een rekening heeft

De goedkope as is goedkoop in rekenkracht. Hij is niet goedkoop in statistiek, en die twee door elkaar halen is hoe je een prestatiewinst in een overfitting-machine verandert.

Elke drempelconfiguratie die je evalueert is een trial, en wanneer je duizenden trials op één dataset draait, ziet de beste ervan er deels door geluk goed uit. Die trials 1.600x goedkoper maken laat het geluk niet verdwijnen — het maakt het makkelijker om het op te stapelen. Een dichte drempel-sweep is precies de situatie waarin multiple-testing-inflatie het hardst toeslaat: veel kandidaten, één historie, en een selectieregel die het maximum rapporteert. De snelste engine ter wereld overhandigt je met plezier een drempelvector die de ruis van je testvenster prachtig fit en out-of-sample faalt.

Dus de discipline moet meeschalen met de snelheid. Op het moment dat de rekenkosten van een trial naar nul dalen, wordt de statistische boekhouding de bindende beperking, en die moet je expliciet betalen:

  • Deflateer voor het aantal trials. Scoor de winnaar tegen hoeveel configuraties je hebt geprobeerd, niet tegen nul. De Deflated Sharpe Ratio en de waarschijnlijkheid van backtest-overfitting bestaan precies hiervoor — ze veranderen "we hebben 4.000 drempelvectoren geprobeerd" in een korting op de gerapporteerde edge.
  • Valideer out-of-sample, per fold. Een goedkope sweep moet nog steeds binnen een eerlijke walk-forward-splitsing worden gedraaid; een drempel die alleen in-sample wint is waardeloos, hoe snel je hem ook hebt gevonden. Onze engine houdt de fills lekvrij (open[i+1]) juist zodat de goedkope as geen prestatie kan kopen door te gluren.
  • Verkies plateaus boven pieken. Omdat de drempelas dicht en snel is, kun je zijn responsoppervlak in kaart brengen, niet alleen zijn argmax. Een breed gebied van drempels die allemaal werken is een echte edge; een enkel puntig maximum is een gefit artefact — het plateau-versus-piek-onderscheid, betaalbaar gemaakt door precies de snelheid die we beschrijven.

Lees de twee-assen-structuur op de juiste manier: hij koopt je geen meer vertrouwen, hij koopt je meer trials tegen dezelfde vertrouwenskosten per trial. Dat is oprecht waardevol — dichte dekking van de goedkope as is wat je in staat stelt plateaus te vinden en het oppervlak te karakteriseren — maar alleen als je het statistische grootboek eerlijk houdt. Snelheid neemt het reken-excuus weg om niet grondig te zoeken; het neemt de verplichting niet weg om te disconteren wat grondig zoeken vindt.

Hoe je je eigen zoektocht structureert

Om dit op je eigen strategie toe te passen is het werk voornamelijk classificatie — je parameters op de juiste as sorteren — gevolgd door een geneste loop:

  1. Label elke parameter naar wat hij je dwingt te herberekenen. Als het veranderen ervan een indicator/feature-array verandert, is hij duur. Als het alleen een vergelijking, drempel of sizing-regel verandert die op vaste arrays wordt toegepast, is hij goedkoop. Bij twijfel, vraag: komt deze parameter ergens voor binnen de definitie van de indicator? Zo niet, dan is hij goedkoop.
  2. Gebruik alleen de dure as als sleutel voor een cache. Memoïseer feature-constructie op zijn indicatorparameters (zoals SignalCache doet met (period, hma_length)). Naburige trials die een indicatorcombinatie delen hergebruiken dan gratis dezelfde arrays.
  3. Batch de goedkope as over gecachte features. Draai drempelconfiguraties als een strakke gecompileerde loop over de vooraf berekende signalen, niet als volledige herevaluaties. Dit is waar je doorvoer — en in ons geval de ~5.600 cfg/s — vandaan komt.
  4. Nest de loops: duur buiten, goedkoop binnen. Houd het dure raster klein en weloverwogen (grof, of grof-naar-fijn); laat de goedkope sweep dicht zijn. Besteed het budget waar de parameters iets kosten.
  5. Budgetteer trials tegen overfitting, niet tegen de klok. Nu de klok niet meer de limiet is, laat de deflated Sharpe / PBO dat zijn. Beslis hoeveel goedkope-as-trials je je statistisch kunt veroorloven, en valideer de winnaar out-of-sample.

De technische opbrengst en het statistische vangrail zijn twee kanten van één idee: het scheiden van de assen stelt je in staat de goedkope dimensies uitputtend te doorzoeken, wat precies de reden is waarom je vervolgens moet disconteren hoe uitputtend je hebt gezocht.

Kernpunten

  1. Niet alle dimensies kosten evenveel. De parameters van een strategie splitsen zich in een dure as (indicatoren — herberekend over de hele reeks) en een goedkope as (drempels — een O(n)-pass over vooraf berekende signalen). In onze engine is dat 6 duur + 12 goedkoop voor drievoudige-TF, 4 + 8 voor dubbele-TF.
  2. Indicatoren zijn invariant ten opzichte van drempels, en die invariantie is de hele optimalisatie. Feature-constructie hangt alleen af van indicatorparameters, dus je bouwt eenmalig, cachet op (period, hma_length), en hergebruikt over elke drempelconfiguratie die de combinatie deelt.
  3. De goedkope as draait ~1.600x goedkoper. ~5.600 drempelconfiguraties/sec (~180 µs elk) op gecachte signalen, tegenover ~0.3 s per configuratie als je elke keer indicatoren opnieuw opbouwt. Dezelfde getrouwheid — het enige verschil is weigeren om een invariant te herberekenen.
  4. De vloek van de dimensionaliteit is eigenlijk een vloek van dure dimensionaliteit. Het merendeel van het parameteraantal leeft op de as die bijna gratis is om te sweepen. Houd het dure raster grof; wees overvloedig op het goedkope.
  5. Snelheid verschuift de bindende beperking van rekenkracht naar statistiek. Een dichte, snelle sweep is een multiple-testing-machine. Deflateer voor het aantal trials, valideer per fold, en verkies plateaus boven pieken — de gratis lunch is echt, maar de statistische rekening is niet optioneel.

De volledige engine — SignalCache, sweep_separations, de lekvrije multi-TF-simulatie, en de pariteitstest die hem vastpint op de live running-candle-semantiek — leeft in scripts/engine_multitf.py (commit bfc8aaa) in onze backtester. De volgende keer dat iemand je vertelt dat een strategie met 18 parameters te groot is om te doorzoeken, vraag hem dan hoeveel van die parameters de indicatoren daadwerkelijk bewegen. Meestal is het een derde ervan, en de rest is bijna gratis.

Disclaimer: De informatie in dit artikel is uitsluitend bedoeld voor educatieve en informatieve doeleinden en vormt geen financieel, beleggings- of handelsadvies. Het handelen in cryptovaluta brengt een aanzienlijk risico op verlies met zich mee.

Auteurs

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

Blijf de markt voor

Abonneer je op onze nieuwsbrief voor exclusieve AI-handelsinzichten, marktanalyses en platformupdates.

We respecteren je privacy. Je kunt je op elk moment afmelden.