De GPU-precisievalstrik: Hoe een fp32-backtest op Apple Metal stilletjes onzin teruggeeft
Onderdeel van de serie "Backtests zonder illusies".
We hebben onze parameter-sweep-backtest overgezet naar de GPU en hij werd 2,796× sneller. De getallen die hij teruggaf waren plausibel. Ze waren, in de eerste werkende versie, ook complete onzin — een factor tweehonderd ernaast — en er crashte niets, er waarschuwde niets, niets zag er fout uit.
Dit is de valstrik die GPU-backtesting op Apple Silicon gevaarlijk maakt op een manier die CPU-code niet is: Metal heeft geen float64. Elk getal dat je backtest op een Apple-GPU aanraakt is een 32-bits float, of je daar nu om vroeg of niet. En de allerverleidelijkste manier om een voortschrijdend gemiddelde te vectoriseren — de O(n)-prefixsom-truc waar elke prestatiegerichte quant naar grijpt — is precies de formulering die fp32 op prijsschaal niet overleeft. Het geeft geen foutmelding. Het draait op volle snelheid en overhandigt je een plausibel ogende equitycurve, gebouwd op een voortschrijdend gemiddelde dat een factor 211 verkeerd is.
De oplossing is het interessante deel, want het is niet "gebruik meer bits" (dat kan niet) en het is niet "wees voorzichtiger" (de naïeve code is al voorzichtig). De oplossing is om dezelfde wiskundige grootheid via een andere som te berekenen — een directe gevensterde convolutie — die elke tussenwaarde klein genoeg houdt zodat fp32 die exact kan representeren. Hetzelfde WMA, hetzelfde resultaat tot op zeven significante cijfers, en 55.9× sneller dan single-thread numba op de CPU. Dit artikel is de lijkschouwing: waarom Metal fp32 aan je opdringt, precies waar de voor de hand liggende formulering overloopt, waarom de correcte dat niet doet, en hoe we bewezen — door trades te tellen, niet door curves te bekijken — dat de snelle versie en de eerlijke fp64-versie overeenkomen.
Alle getallen hier zijn gemeten op een Apple M2 Max, 150,000 bars × 80 parametercombinaties, best-of-3, uit de scripts/bench_param_sweep.py van de repo (de M5-GPU-methode, commit 97eadaf), scripts/engine_multitf_gpu.py (04d71e8), en de ontwerpnotities in scripts/GPU_NOTES.md.
Metal heeft geen float64

Op een CPU draait een Python/numpy-backtest standaard in dubbele precisie. float64 geeft je een 52-bits mantisse: gehele getallen worden exact gerepresenteerd tot 2⁵³ ≈ 9×10¹⁵, en de relatieve precisie is ongeveer 1.1×10⁻¹⁶. Je denkt er bijna nooit over na, want op prijsschaal — een BTC-slotkoers rond de 30,000, een cumulatieve som die 10¹⁴ bereikt — heeft fp64 marge te over.
Apple's GPU biedt die marge niet, want hij biedt float64 helemaal niet. De Metal Shading Language heeft geen double-type; MLX, PyTorch-MPS en elk ander framework dat zich op Apple Silicon richt erft dit. Er is geen vlag om het aan te zetten en geen langzame-maar-correcte terugvaloptie. Als je berekening de GPU raakt, gebeurt het in fp32:
- Mantisse: 23 bits (24 met de impliciete voorloopbit). Gehele getallen zijn slechts exact tot 2²⁴ = 16,777,216 ≈ 1.6×10⁷.
- Relatieve precisie: ~1.2×10⁻⁷. Ruwweg zeven significante decimale cijfers, en niet meer.
Dat plafond van 1.6×10⁷ is het hele verhaal. Het klinkt royaal — zestien miljoen — totdat je merkt dat een backtest routinematig tussengrootheden opbouwt die veel groter zijn dan dat, en op het moment dat een tussenwaarde 1.6×10⁷ overschrijdt, kan fp32 niet eens meer opeenvolgende gehele getallen representeren, laat staan de fractionele structuur die je nodig hebt. De precisie neemt niet geleidelijk af; ze valt van een klif, en die klif ligt bij een getal waar je data zonder er twee keer over na te denken langs zeilt.
De valstrik is dat fp32 bijna altijd prima is. Het grootste deel van een backtest — prijzen, rendementen, PnL, Sharpe — leeft comfortabel in het bereik waar zeven cijfers ruim voldoende zijn. Dus de naïeve port werkt, doorstaat een smoke test, produceert gezond ogende output. Het falen is beperkt tot precies één operatie, de ene plek waar een tussenwaarde ver voorbij 10⁷ schiet, en die operatie is degene die iedereen als eerste vectoriseert.
De verleidelijke formulering: WMA in één prefixsom-sweep
Onze strategie leunt op Hull moving averages. Een HMA bestaat uit drie gewogen voortschrijdende gemiddelden die zijn samengesteld; de HMA3-variant is er vier. Een gewogen voortschrijdend gemiddelde met een lineaire kernel over een venster van lengte p is
Bij het doorlopen van duizenden parametercombinaties over 150k bars zijn de WMA-convoluties de kostenpost. Dus het instinct — het juiste instinct, op een CPU — is om elke WMA O(n) te maken in plaats van O(n·p) met behulp van prefixsommen. Je berekent twee cumulatieve sommen één keer vooraf,
en dan valt de lineair gewogen som van elk venster uiteen in een handvol verschillen en index-verschuivingen van S1 en S2. Geen lus per venster, geen reductie per venster — twee cumsum-passes en de hele matrix van WMA's rolt uit array-rekenkunde. Het vectoriseert prachtig, het mapt perfect op de parallel-scan-primitief van een GPU, en in fp64 klopt het exact.
Het is ook het allerslechtste wat je in fp32 kunt doen, en de reden verschuilt zich in S2.
S2 = mx.cumsum(j * price) # j is the global bar index: 0, 1, 2, ... , n-1
De term j · price is het probleem. Met j die oploopt tot 150,000 en price rond de 30,000 is de laatste term alleen al 4.5×10⁹, en S2 is de lopende som van 150,000 van zulke termen. Hij blijft niet op prijsschaal. Hij klimt het gebied in waar fp32 al is opgehouden met kunnen tellen.
Waar het overloopt: de rekenkunde van de valstrik

Laten we de ordes van grootte naast elkaar zetten, want daar zit het hele falen.
S2 = cumsum(j · price) bereikt ruwweg price · n²/2 ≈ 30,000 · (150,000)²/2 ≈ 3×10¹⁴. Noem het ~10¹⁴. Herinner je nu het exacte-gehele-getal-plafond van fp32: ~1.6×10⁷. De lopende som schiet het laatste gehele getal dat fp32 exact kan representeren met zeven ordes van grootte voorbij.
Wat betekent dat concreet? In de buurt van 10¹⁴ is het gat tussen twee representeerbare fp32-getallen — één eenheid op de laatste plaats, de ULP — ongeveer 2²³ ≈ 8×10⁶. Dus zodra S2 in het 10¹⁴-bereik zit, is hij slechts bekend tot op ±8 miljoen. Elke waarde die hij opslaat is afgerond naar het dichtstbijzijnde veelvoud van ~8×10⁶.
Kijk nu wat de WMA-reconstructie daarmee doet. Om de gewogen som van één venster te extraheren, trek je twee naburige S2-waarden van elkaar af (plus S1-correcties). Die twee S2-waarden zijn elk ~10¹⁴, elk met ±8×10⁶ aan afrondingsruis. Hun werkelijke verschil — de venster-grootheid die je eigenlijk wilt — komt, na normalisatie, overeen met een WMA in de orde van de prijs zelf, ~3×10⁴. Dus de rekenkunde is:
Dit is catastrofale uitdoving in zijn puurste vorm: de afrondingsfout van elke operand (±8×10⁶) is groter dan het antwoord dat je probeert terug te winnen. Het signaal is kleiner dan de ruisvloer van de getallen waaruit het wordt geëxtraheerd. Het is niet dat je een paar cijfers verliest — je verliest ze allemaal, en wat terugkomt wordt gedomineerd door de opgestapelde afronding van de cumsum.
De gemeten consequentie, uit GPU_NOTES.md: voor een WMA die op deze manier is berekend op 150k bars bij prijs ~30,000 bereikt de maximale relatieve fout ten opzichte van fp64 ~211. Geen 211 procent — 211×. Het berekende voortschrijdend gemiddelde kan twee ordes van grootte van het echte af liggen. En hier is het deel dat er een valstrik van maakt in plaats van een bug: het draait tot het einde en geeft eindige, plausibele getallen terug. Geen overflow naar oneindig, geen NaN, geen exception. Een voortschrijdend gemiddelde dat 211× verkeerd is ziet er nog steeds uit als een voortschrijdend gemiddelde — het is glad, het is eindig, het zit ruwweg in de goede orde van grootte op de bars waar de uitdoving toevallig mild is — dus het glipt door elke sanity check heen die geen directe vergelijking met een vertrouwde referentie is. Je krijgt een volledige backtest, een volledige equitycurve, een volledige set "optimale" parameters, allemaal gebouwd op een indicator die fictie is.
De oplossing is niet meer precisie — het is een andere som

De reflex, zodra je de fout ziet, is om naar meer precisie te grijpen — accumuleren in fp64, of gecompenseerde (Kahan-)sommatie gebruiken. Op Metal is het eerste simpelweg niet beschikbaar. Maar je hebt geen van beide nodig, want het probleem was nooit het aantal bits. Het probleem was de formulering. De prefixsom-truc fabriceert tussenwaarden op 10¹⁴-schaal en trekt ze dan weer naar beneden af; de groottes die hij creëert zijn een artefact van het algoritme, niet van het antwoord. Kies een formulering die ze nooit creëert en fp32 is prima.
Die formulering is de definitie zelf: een directe gevensterde convolutie. In plaats van twee globale cumulatieve sommen schuif je de lineaire kernel van lengte p over de reeks en sommeer je ter plekke. Elke output is een som van hooguit p ≈ 200 termen, en elke term is weight × price waarbij de gewichten zijn genormaliseerd om op te tellen tot 1 — dus elke term is in de orde van price / p, elke deelsom blijft rond prijsschaal (~3×10⁴), en geen enkele tussenwaarde komt ooit binnen zes ordes van grootte van het fp32-plafond. Er is niets om uit te doven, want niets is ooit opgeblazen.
In MLX is dit één primitief — mx.conv1d — precies wat GPU's zijn gebouwd om snel te doen:
def _mx_wma_valid(x, period):
w = mx.arange(1, period + 1, dtype=mx.float32) / (period * (period + 1) / 2.0)
return mx.conv1d(x.reshape(1, -1, 1), w.reshape(1, period, 1), padding=0).reshape(-1)
Hetzelfde WMA, wiskundig identiek aan de prefixsom-versie en aan de fp64 vec_wma/nb_wma van de CPU. Maar nu is de gemeten maximale relatieve fout ten opzichte van fp64 8.2×10⁻⁷ — precies op de fp32-ruisvloer van ~1.2×10⁻⁷, zeven significante cijfers overeenstemming. De formulering die op papier langzamer lijkt (O(n·p) in plaats van O(n)) is de enige die correct is, en — omdat het een dichte convolutie is die de GPU tegelijkertijd over zowel bars als vensters parallelliseert — is ze ook razendsnel. We gingen van een relatieve fout van 211 naar 8×10⁻⁷ door te veranderen hoe we sommeren, niet met hoeveel bits we sommeren.
Twee praktische opmerkingen die voortvloeien uit deze aanpak. Ten eerste propageert MLX geen NaN's door een conv1d zoals numpy dat doet, dus de warm-up-regio (de eerste p−1 bars, waar een venster-gemiddelde niet gedefinieerd is) kan op de GPU niet met NaN worden gemarkeerd. Dat hoeven we ook niet: het geldige begin van elke reeks is analytisch bekend, ongeldige prefixen worden gevuld met nullen die nooit worden gelezen, en de NaN-padding wordt achteraf op de CPU hersteld — bit-voor-bit hetzelfde geldigheidsmasker als de gevectoriseerde en numba-versies. Ten tweede deelt de hele sweep één cand_close-reeks en hergebruikt vensters intensief over combinaties heen, dus één enkele batched conv1d met veel outputkanalen berekent elke unieke WMA die de sweep nodig heeft in één GPU-aanroep, gematerialiseerd met één mx.eval().
Bewijzen dat je er niet in getrapt bent: pariteit via trade-aantal
Hier is de ongemakkelijke vraag die de vorige sectie zou moeten oproepen: als een WMA die 211× verkeerd is er nog steeds uitziet als een WMA, hoe weet je dan dat de 8×10⁻⁷-versie werkelijk correct is en niet gewoon subtieler verkeerd? Je kunt het niet op het oog beoordelen. Je hebt een invariant nodig die een stroomafwaarts, discreet deel van de pijplijn blootlegt — en een backtest geeft je een perfecte: de trades.
De andere GPU-methoden in de ladder (M0–M4) draaien volledig in fp64, dus we houden ze aan een strikte equivalentie-assert — identieke trade-aantallen, PnL die matcht tot op atol=1e-6. De fp32-GPU-methode (M5) kan dat per definitie niet halen, en de assert stilletjes voor iedereen versoepelen om haar tegemoet te komen zou precies het soort oneerlijkheid zijn waartegen deze serie bestaat. Dus M5 krijgt zijn eigen kwantitatieve pariteitsrapport, report_equiv_fp32, dat zijn geëxtraheerde trades vergelijkt met de fp64-referentie.
Het mechanisme van elke resterende afwijking is het waard om precies te benoemen, want het is niet de uitdovingscatastrofe — het is de gewone, minuscule fp32-afronding die je zou verwachten. De strategie vuurt op de crossover van twee Hull-gemiddelden, h versus h3. Een relatieve fout van ~1×10⁻⁶ op een indicator bij prijs ~30,000 is een absolute schommeling van ~0.03. Op de overgrote meerderheid van de bars liggen de twee curves verder uit elkaar dan dat en is de crossover ondubbelzinnig. Maar op een grensgeval-bar — waar h − h3 zelf binnen 0.03 van nul ligt — kan die schommeling het teken van de vergelijking omdraaien, waardoor één crossover een enkele bar verschuift en er één trade wordt toegevoegd of verwijderd.
Dit is waarom "fractie van combinaties die verschillen" een waardeloze gezondheidsmaat is, en onze eerste pariteitscheck maakte zichzelf te schande door hem te gebruiken. Bij 150k bars heeft elke combinatie duizenden crossovers, dus verschijnt er op vrijwel elke combinatie ten minste één grensgeval-bar — 37 van de 80 combinaties "verschilden," wat alarmerend klinkt en niets betekent. De maat die ertoe doet is met hoeveel:
- PnL-delta over alle 80 combinaties: max |Δ| = 1.843 procentpunt, max relatief = 1.25×10⁻²; crashdrempel 5 p.p.
- Trade-aantal-drift per combinatie: max |Δn| = 4 trades op duizenden, max relatief = 2.5×10⁻³; crashdrempel 1%.
- In totaal: 90 verschoven trades op 479,016 — 0.019%.
Negentig trades op bijna een half miljoen, elk daarvan een grensgeval-crossover die is aangeduwd door een afrondingsschommeling kleiner dan een price tick, en niets in de buurt van de crashdrempels. Dat is de signatuur van een fp32-methode die correct is — kleine, begrensde, verklaarbare afwijking — en het is een compleet ander beest dan een relatieve fout van 211. De drempels bestaan om een kapotte formulering te betrappen die zich voordoet als "ach, het is nu eenmaal fp32"; de werkelijke delta's komen er een orde van grootte onder. Het trade-aantal is het orakel dat de equitycurve weigerde te zijn.
De opbrengst, en waar de GPU stopt met helpen
Nu de correctheid is vastgesteld, is de snelheid het vermelden waard — en vervolgens eerlijk te nuanceren, want het voordeel van de GPU is niet uniform over de pijplijn.
Op de pure WMA-convoluties op zichzelf — de operatie waarvoor de hele methode bestaat om te versnellen — draait de fp32 conv1d-batch 55.9× sneller dan single-thread numba, bij die relatieve fout van 8.2×10⁻⁷. Dat is het schone, appels-met-appels GPU-versus-CPU-getal: dezelfde wiskunde, één thread gecompileerde CPU-code tegen de Metal-GPU.
Maar een sweep is niet alleen convoluties. Zodra de HMA/HMA3-matrices op de GPU zijn berekend, moeten de trades nog steeds worden geëxtraheerd — een O(n)-wandeling over de crossovers van elke combinatie — en dat doen we op de CPU in fp64, waarbij we de exacte trade-semantiek van de andere methoden hergebruiken in plaats van ze op de GPU opnieuw te implementeren. Het end-to-end timed()-cijfer omvat alles: kernel warm-up uitgesloten (symmetrisch met het uitsluiten van numba's compilatie), maar GPU→CPU-overdracht en CPU-trade-extractie inbegrepen. Op 150k bars × 80 combinaties, best-of-3, M2 Max:
| Methode | Wall | Versnelling vs baseline | combos/s |
|---|---|---|---|
| M0 pandas + Python loop* | 287.08s | 1.0× | 0.3 |
| M1 vectorized numpy | 3.14s | 91.5× | 25.5 |
| M2 numba (serial) | 2.02s | 142.3× | 39.7 |
| M3 multiprocess + vectorized | 0.50s | 570.2× | 158.9 |
| M4 multiprocess + numba (12 cores) | 0.33s | 882.5× | 245.9 |
| M5 MLX GPU (fp32) | 0.10s | 2796.0× | 779.2 |
*M0 geëxtrapoleerd uit een uniforme steekproef van 5 combinaties.
De volledige-engine M5 doet 779 combos/s — 2,796× ten opzichte van de pandas-baseline, 19.6× ten opzichte van seriële numba, en 3.2× ten opzichte van de volledige 12-core CPU-pool die numba draait (M4). Eén GPU verslaat elke CPU-core die de machine heeft, drievoudig.
Nu de eerlijke nuance: merk op dat het end-to-end GPU-voordeel (19.6× ten opzichte van M2) kleiner is dan het alleen-convolutie-voordeel (55.9× ten opzichte van numba). Dat gat is de wet van Amdahl die precies op tijd arriveert. De GPU vernietigt de convoluties zo volledig dat ze ophouden de bottleneck te zijn; wat overblijft — de O(n) CPU-trade-extractie die de GPU helemaal niet versnelde — domineert nu de wall time van M5. Dit is dezelfde les waar de snelheidsladder en de IPC-belasting uit deze serie steeds op uitkomen: voorbij een bepaald punt is de winst niet "maak het snelle deel sneller", maar orkestratie — waar de data leeft, welke fase nu serieel is, wat je betaalt om tussen device en host te verplaatsen. Een hypothetische M6 najagen die trade-extractie in een custom Metal-kernel duwt zou alleen de krimpende CPU-plak terugwinnen, en daarom hebben we hem niet gebouwd.
De algemene les: stille numerieke onzin is de standaard
Neem afstand van Hull-gemiddelden en MLX, want de valstrik generaliseert veel verder dan deze ene indicator.
De verleidelijke pitch van GPU-backtesting is "één grote matrix": stapel elke parametercombinatie in een tensor, draai de hele sweep als een handvol dichte array-operaties, laat de hardware het opeten. Die pitch is echt — de versnellingen hierboven zijn echt. Maar het verandert stilletjes het numerieke regime onder je, en de verandering is onzichtbaar in de code. Op de CPU beschermden je defaults je: fp64, NaN-propagatie, een cumsum die tot 10¹⁴ kon oplopen zonder het te merken. Verplaats dezelfde array-expressie naar Metal en je zit in fp32 met een hard geheel-getal-plafond van 1.6×10⁷, en de identieke regel code — cumsum(j * price) — gaat van exact naar onzin. Niets in de syntaxis waarschuwt je. De compiler is tevreden. De output is eindig en plausibel. fp32 faalt niet luidruchtig; het faalt beleefd, met getallen.
De drie gewoonten die je werkelijk beschermen zijn goedkoop:
- Weet waar je tussenwaarden leven, niet alleen je inputs en outputs. De inputs (prijzen ~10⁴) en outputs (een WMA ~10⁴) zaten beide comfortabel binnen het exacte bereik van fp32. De ramp zat volledig in een verborgen tussenwaarde (
S2~10¹⁴) die noch de API noch de types zichtbaar maakten. Voordat je een fp32-reductie vertrouwt, vraag je af hoe hoog de grootste deelsom komt — en als die ~10⁷ overschrijdt, verander de formulering. - Geef de voorkeur aan formuleringen die groottes begrensd houden. Directe convolutie boven prefixsommen; lokale vensters boven globale scans; centreren/differentiëren voor het sommeren in plaats van erna. Groot-en-dan-uitdoven is het antipatroon. Het correcte algoritme is vaak degene die er op papier asymptotisch slechter uitziet maar nooit een grootheid fabriceert die het weer moet wegdoven.
- Valideer tegen een fp64-orakel via een discrete invariant. Vergelijk geen curves; vergelijk iets gekwantiseerds en stroomafwaarts — trade-aantal, aantal crossovers, positiewijziging-gebeurtenissen. Een discrete invariant verandert een stille 211×-fout in een schreeuwende assertion failure, en verandert een acceptabele 8×10⁻⁷-fout in een kleine, begrensde, verklaarbare delta. Dit is dezelfde discipline als de one-bar-shift-test voor look-ahead bias: een goedkope diagnose die een onzichtbaar falen omzet in een zichtbaar falen.
Niets hiervan is exotische numerieke analyse. Het is de gewone hygiëne van een snelle backtest niet vertrouwen totdat een langzame, vertrouwde er zijn zegen aan heeft gegeven — uitgebreid naar de ene plek waar de taal ophoudt je te waarschuwen dat de precisie stilletjes met twaalf ordes van grootte is gedaald.
Kernpunten

- Apple's GPU heeft geen float64 — elk GPU-getal in je backtest is fp32. Gehele getallen zijn slechts exact tot ~1.6×10⁷ en de precisie is ~1.2×10⁻⁷. Er is geen vlag, geen terugvaloptie. Het grootste deel van een backtest overleeft dit; precies één operatie meestal niet.
- De prefixsom-WMA is de valstrik.
cumsum(j · price)klimt tot ~10¹⁴, zeven ordes voorbij het exacte plafond van fp32, en om een venster terug te winnen moet je twee van zulke getallen van elkaar aftrekken waarvan de afrondingsfout (±8×10⁶) het antwoord al in het niet doet vallen. Gemeten maximale relatieve fout: 211×. Het crasht nooit — het geeft plausibele onzin terug. - De oplossing is een andere som, niet meer bits. Een directe gevensterde convolutie (
mx.conv1d) houdt elke deelsom in de buurt van prijsschaal, dus fp32 behoudt zeven eerlijke cijfers: 8.2×10⁻⁷ relatieve fout, en 55.9× sneller dan single-thread numba. Je kunt op Metal geen fp64 kopen, en dat hoeft ook niet. - Verifieer met een discrete invariant, nooit met de curve. Trade-aantal-pariteit betrapte het: fp32 conv1d verschilde van fp64 op 90 van de 479,016 trades (0.019%), allemaal grensgeval-crossovers, allemaal ver onder de crashdrempels — de signatuur van een correcte methode, onmiskenbaar anders dan een 211×-fout. "Fractie van combinaties die verschillen" is een misleidende maat; meet met hoeveel.
- De volledige sweep doet 779 combos/s — 2,796× ten opzichte van de pandas-baseline, 3.2× ten opzichte van de volledige 12-core CPU-pool — maar de end-to-end-winst (19.6× ten opzichte van seriële numba) is kleiner dan de alleen-convolutie-winst (55.9×) omdat de CPU-trade-extractie nu de bottleneck is. Voorbij een bepaald punt is snelheid orkestratie, geen rekenkunde.
De GPU-port was 2,796× sneller en, in zijn eerste werkende versie, volledig verkeerd — en de twee feiten hadden niets met elkaar te maken. De snelheid was echt. De onzin was een verborgen tussenwaarde van 10¹⁴ die fp32 niet kon vasthouden en die geen enkele foutmelding zou noemen. Als een backtest dramatisch sneller wordt en de getallen er nog steeds goed uitzien, is dat geen bevestiging. Op Metal is "ziet er goed uit" precies hoe een relatieve fout van 211 eruitziet.
Dit is de GPU-sport van de ladder die deze serie beklimt: de snelheidsladder van de backtest-engine, de IPC-belasting van multiprocessing, de look-ahead-taxonomie van lekken, en het ontwerp van de objectieffunctie dat bepaalt wat "goed" überhaupt betekent. Snelheid is waardeloos als ze snel het verkeerde getal berekent.
Auteurs
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.