Circuit breakers voor tradingbots instellen
Circuit breakers stoppen een tradingbot voordat een slechte sessie zich opstapelt. Lees hoe vaak een daily loss limit ingrijpt en hoe volatility scaling de positiegrootte bepaalt.
Circuit breakers voor trading bots zijn regels die een geautomatiseerde strategie ervan weerhouden orders te versturen zodra een vastgestelde limiet is bereikt. Zij bevinden zich in een risicolaag tussen de strategie en de broker en worden op elke order toegepast, ongeacht of de strategie daarmee instemt. De strategie bepaalt wat er wordt verhandeld. De risicolaag bepaalt of er überhaupt wordt gehandeld.
Die scheiding vormt de kern van het ontwerp. Een strategie die zichzelf controleert, heeft geen onafhankelijke controle op het moment dat haar eigen aannames niet meer opgaan. Juist dan is zo’n controle het meest waardevol.
Wat een circuit breaker doet in een tradingbot
Een risicolaag bestaat uit vier onderdelen. Elk onderdeel voorkomt een specifiek en meestal weinig spectaculair probleem.
- Harde limieten voor de positiegrootte, de nominale exposure per symbool en de orderfrequentie. Zij begrenzen de schade van een bug die anders onbeperkte schade zou kunnen veroorzaken.
- Een drawdown-circuit breaker die nieuwe orders stillegt zodra de rekening tijdens de sessie met een vastgesteld bedrag is gedaald, of met een vastgesteld bedrag vanaf de equity-piek.
- Geschaalde ordergrootte, gebaseerd op recente volatiliteit of op een fractie van de Kelly-bet in plaats van op een vast aantal aandelen. Zo blijft het risico per trade ongeveer constant terwijl de trading range van de markt verandert.
- Een append-only auditlog van elke beslissing, inclusief de orders die de risicolaag heeft afgewezen. Dit is het enige artefact dat onderscheid maakt tussen “de strategie was onjuist” en “de controle is nooit uitgevoerd”.
Alles hieronder is een concrete uitwerking van een van deze vier onderdelen.
Wat is een redelijke dagelijkse verlieslimiet voor een tradingbot?
Een dagelijkse verlieslimiet blokkeert nieuwe orders zodra het verlies in de sessie een bepaalde drempel overschrijdt. Het vaststellen van die limiet is een kwestie van kalibratie, niet van smaak: ligt de limiet binnen de normale marktvolatiliteit, dan blijft de bot de meeste weken stilgelegd; ligt zij daar ver buiten, dan wordt zij nooit geactiveerd. Het uitgangspunt is hoe vaak de markt zelf een dalingsdag van een bepaalde omvang laat zien.
De exacte SQL achter elk getal
WITH daily AS (
SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS d,
argMax(toFloat64(close), window_start) AS close_px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2017-12-01')
AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-07-31')
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
GROUP BY d
),
with_prev AS (
SELECT d,
close_px,
any(close_px) OVER (ORDER BY d ASC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prev_close
FROM daily
)
SELECT toYear(d) AS year,
countIf(close_px / prev_close - 1 <= -0.01) AS down_1pct_days,
countIf(close_px / prev_close - 1 <= -0.02) AS down_2pct_days,
countIf(close_px / prev_close - 1 <= -0.03) AS down_3pct_days
FROM with_prev
WHERE prev_close > 0
AND d >= toDate('2018-01-01')
GROUP BY year
ORDER BY yearHet aantal sessies met een daling van 1% of meer bedroeg 15 in 2019 en 45 in 2020, op ongeveer 250 handelsdagen per jaar. Het aantal sessies met een daling van 3% of meer is van een andere orde: 0 in 2019 tegenover 16 in 2020. De laatste rij van de 9 omvat alleen sessies tot en met 31 juli 2026.
De reeks is grillig in plaats van gelijkmatig, en juist daarin ligt het ontwerpprincipe. Zware dagen komen in clusters. Een bot die op de eerste dag van zo’n cluster wordt stilgelegd en op de tweede dag weer wordt geactiveerd, is feitelijk niet stilgelegd.
Twee drempels hebben verschillende functies. Een dagelijkse verlieslimiet, doorgaans 2% van het accountvermogen, beëindigt de sessie. Een trailing drawdown-limiet vanaf de hoogste vermogensstand, doorgaans ongeveer 10%, beëindigt de strategie in afwachting van menselijke beoordeling. De eerste limiet is routinematig, de tweede is bedoeld om zelden te worden bereikt. Een bot met alleen de eerste limiet kan een account in stappen van 2% uithollen zonder ooit een limiet te activeren.
Hoe verandert volatility scaling de positiegrootte?
Volatility targeting bepaalt de positiegrootte omgekeerd evenredig aan de recente realized volatility. Wanneer de daily range verdubbelt, wordt de positie ongeveer gehalveerd. Zo blijft het dollar risk per trade ongeveer constant. Realized volatility is hier de geannualiseerde standaarddeviatie van daily returns. Deze beweegt sterker dan de meeste mensen verwachten.
De exacte SQL achter elk getal
WITH daily AS (
SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS d,
argMax(toFloat64(close), window_start) AS close_px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2023-12-01')
AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-07-31')
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
GROUP BY d
),
rets AS (
SELECT d,
close_px / any(close_px) OVER (ORDER BY d ASC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) - 1 AS ret
FROM daily
)
SELECT formatDateTime(toStartOfMonth(d), '%Y-%m') AS month,
round(stddevSamp(ret) * sqrt(252) * 100, 1) AS realized_vol_pct,
round(least(100.0, 1200.0 / (stddevSamp(ret) * sqrt(252) * 100)), 1) AS vol_target_size_pct
FROM rets
WHERE d >= toDate('2024-01-01')
AND isFinite(ret)
GROUP BY toStartOfMonth(d)
HAVING count() >= 15
ORDER BY toStartOfMonth(d)Realized volatility gemeten op 11.1% annualized in 2024-01 en 12% in 2026-07, over 31 maanden. De tweede kolom vertaalt elke meting naar de positie die past bij een volatility target van 12%, met een maximum gelijk aan de volledige regel: 100% in 2024-01 tegenover 99.7% in 2026-07. Dezelfde strategie en dezelfde overtuiging kunnen dus leiden tot een zeer verschillend aantal aandelen.
Het Kelly-criterium benadert hetzelfde probleem vanuit de andere kant. De positiegrootte wordt bepaald op basis van de geschatte edge en variantie, in plaats van alleen op basis van volatility. De meeste systematic operators gebruiken een fractie ervan, bijvoorbeeld half Kelly of quarter Kelly, omdat beide inputvariabelen schattingen zijn op basis van een beperkte steekproef. positiegrootte volgens het Kelly-criterium werkt die berekening uit.
Waarom blijft een tradingbot na een stop opnieuw instappen?
Een stop wordt uitgevoerd. De positie wordt gesloten. Negentig seconden later is opnieuw aan de instapvoorwaarde voldaan. De bot stapt opnieuw in, neemt hetzelfde verlies en herhaalt dit proces. Geen enkel onderdeel is defect. De strategie doet wat daarvoor is geprogrammeerd, de stop doet wat daarvoor is geprogrammeerd en de rekening loopt toch leeg, één round trip per keer.
De frequentie volgt rechtstreeks uit het koersverloop. Dit panel telt per sessie hoe vaak SPY vanaf meer dan 0,1% boven de openingskoers naar meer dan 0,1% eronder bewoog, of omgekeerd.
De exacte SQL achter elk getal
WITH mins AS (
SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS d,
window_start AS ts,
toFloat64(close) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2025-08-01')
AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-07-31')
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
),
opens AS (
SELECT d, argMin(px, ts) AS open_px
FROM mins
GROUP BY d
),
zoned AS (
SELECT m.d AS d,
m.ts AS ts,
multiIf(m.px >= o.open_px * 1.001, 1,
m.px <= o.open_px * 0.999, -1,
0) AS zone
FROM mins AS m
INNER JOIN opens AS o ON m.d = o.d
),
flips AS (
SELECT d,
zone,
any(zone) OVER (PARTITION BY d ORDER BY ts ASC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prev_zone
FROM zoned
WHERE zone != 0
),
per_day AS (
SELECT d, countIf(prev_zone != 0 AND zone != prev_zone) AS crossings
FROM flips
GROUP BY d
)
SELECT formatDateTime(toStartOfMonth(d), '%Y-%m') AS month,
round(avg(crossings), 1) AS avg_crossings_per_session,
max(crossings) AS max_crossings_in_a_session
FROM per_day
GROUP BY toStartOfMonth(d)
ORDER BY toStartOfMonth(d)SPY doorkruiste die band gemiddeld 0.7 keer per sessie in 2025-08 en 1.5 keer in 2026-07. In één sessie werden 5 crossings geregistreerd in 2026-07. Elke regel die aan de ene kant van een niveau opent en aan de andere kant een stop activeert, heeft op één dag dus evenveel kansen om opnieuw te worden uitgevoerd.
Vier mechanismen beperken dit.
- Een cooldown na elke stop, uitgedrukt in minuten of bars. Tijdens die periode passeert geen nieuwe order voor dat symbool de risk layer.
- Een dagelijkse limiet op het aantal trades per symbool. Daarmee wordt een onbegrensde lus begrensd.
- Een halt-vlag die wordt vergrendeld. Zodra de dagelijkse loss limit wordt geraakt, blijft de vlag actief totdat iemand deze wist.
- Persistentie van die vlag buiten het procesgeheugen. Een supervisor die een gecrashte bot opnieuw start, geeft de bot een schone lei. Precies dat moet de vlag voorkomen.
Die laatste maatregel wordt vaak over het hoofd gezien door mensen die al het overige goed hebben ingericht. Grid-tradingbots plaatsen bewust ladders met orders. Daardoor is de trade-countlimiet essentieel en niet slechts decoratief.
Wat gebeurt er wanneer een bot handelt op basis van verouderde prijsdata?
Een quote die niet meer wordt bijgewerkt, ziet er nog steeds uit als een getal. De bot leest de quote, prijst er een order tegen en stuurt die order naar een markt die inmiddels is bewogen. De fout blijft onopgemerkt: er ontstaat geen foutmelding, er wordt niets in het logboek vastgelegd en de fills zien er pas achteraf vreemd uit.
De duidelijkst meetbare variant is de overnight gap. Daarbij blijft een bekende prijs urenlang ongewijzigd terwijl de verhandelbare prijs beweegt.
De exacte SQL achter elk getal
WITH daily AS (
SELECT ticker,
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
argMin(toFloat64(open), window_start) AS open_px,
argMax(toFloat64(close), window_start) AS close_px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('SPY', 'KO', 'MSFT', 'AAPL', 'NVDA', 'TSLA')
AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2024-01-01')
AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-07-31')
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
GROUP BY ticker, d
),
gaps AS (
SELECT ticker,
d,
open_px,
any(close_px) OVER (PARTITION BY ticker ORDER BY d ASC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prev_close
FROM daily
)
SELECT ticker,
round(quantileDeterministic(0.5)(abs(open_px / prev_close - 1) * 100, cityHash64(ticker, d)), 2) AS median_gap_pct,
round(quantileDeterministic(0.95)(abs(open_px / prev_close - 1) * 100, cityHash64(ticker, d)), 2) AS p95_gap_pct,
round(max(abs(open_px / prev_close - 1) * 100), 2) AS max_gap_pct
FROM gaps
WHERE prev_close > 0
GROUP BY ticker
ORDER BY p95_gap_pct DESCDe grootste naam op basis van de gap op het 95e percentiel was TSLA met 4.41%, tegenover 1% voor KO. Normale nachten waren veel rustiger, met mediane gaps van respectievelijk 1.01% en 0.24%. Juist voor deze staarten wordt een risk layer gebouwd. De grootste afzonderlijke gap op TSLA in de gemeten periode bedroeg 14.57%. Dat zijn de afstanden waartegen een bot kan handelen wanneer het enige tijd eerder een prijs heeft gelezen. Waarom aandelen overnight gaps vertonen behandelt de werking.
De beschermingsmaatregelen zijn eenvoudig en goedkoop. Stel voor elke quote waartegen de risk layer orders prijst een maximale ouderdom in. Voor een intradaystrategie is dat doorgaans enkele seconden. Controleer naast de data afzonderlijk ook de heartbeat van de feed. Zo kunt u een stille socket onderscheiden van een rustige markt. Behandel ontbrekende data als een halt en niet als een hold. Een bot zonder prijzen kan zijn exits immers ook niet evalueren.
Welke harde limieten horen in de risicolaag?
- Maximale nominale positie per symbool als aandeel van het accountvermogen. Een limiet van 10% voorkomt dat één slecht presterend symbool het hele account domineert.
- Maximale bruto nominale positie over alle open posities. Een limiet van 100% van het accountvermogen betekent dat er geen leverage wordt gebruikt. Dat is een beslissing die u expliciet moet nemen, in plaats van die over te laten aan de standaardinstellingen van de broker.
- Maximale orderfrequentie per minuut en per dag. Tien orders per minuut is voor de meeste retailstrategieën ruim voldoende en begrenst een ontsporende lus binnen één minuut.
- Maximale ordergrootte als aandeel van het gemiddelde dagelijkse volume van het symbool. Een limiet van 1% voorkomt dat een bot de koers beweegt waarop hij probeert te handelen. gemiddeld dagelijks volume is de noemer.
Elk van deze limieten hoort in de risicolaag en niet in de strategie. Elke limiet moet bovendien in backtests, paper trading en live trading via hetzelfde codepad worden uitgevoerd. Een limiet die alleen live bestaat, is een limiet die niemand heeft getest.
Wat moet een auditlog van een trading bot bevatten?
Een append-only log schrijft per beslissing één record en wijzigt of verwijdert nooit gegevens. Elk record bevat het tijdstip, de gebruikte quote en de ouderdom daarvan, elke uitgevoerde limietcontrole met de uitkomst, de verzonden order en het antwoord van de broker. Afgewezen orders worden met hetzelfde gewicht vastgelegd als uitgevoerde orders.
Reconstructie is het doel. Zes weken na een slechte sessie is de vraag nooit wat de P&L was. De vraag is welke controle slaagde en op basis van welke input. Zonder de vastgelegde input moet u de toestand van de bot opnieuw afleiden uit de markttoestand. Dat is dezelfde fout als look-ahead bias bij backtesting: informatie gebruiken waarover het systeem op het beslismoment nog niet beschikte.
Het project riskguard is een open-source-implementatie van deze scheiding. De limietcontroles bevinden zich in een component die door de strategie wordt aangeroepen, in plaats van in logica die door de strategie zelf is verspreid. Het is een van meerdere mogelijke ontwerpen. Het project is het lezen waard, maar u moet het niet zonder meer overnemen. Welke component u ook gebruikt, leg een getagde release vast in plaats van een default branch. Een branch kan veranderen tussen twee runs van dezelfde backtest. Een risicolaag die ongemerkt is gewijzigd, is erger dan helemaal geen risicolaag.
Waarom een eerste deployment op een paper broker draait
De brokeradapter staat standaard op paper. Live trading vereist bewust een expliciete flag. Daarmee voorkomt u een alledaagse fout: een gekopieerd configuratiebestand of een environmentvariabele die niet is overschreven, kan echte orders met echt geld versturen.
Een paper run levert ook het relevante artefact op: een beslislog uit dezelfde risk layer, gebaseerd op live prijzen. Daaruit blijkt welke limieten zijn geactiveerd en welke niet. Dat is bewijs over de risk layer. Dat staat los van de vraag of de strategie geld oplevert. Paper trading voordat u echt geld inzet behandelt wat een paperregistratie wel en niet bewijst. multi-agent AI-tradingsystemen laat zien waarom de bevoegdheid om de handel stil te leggen buiten elke agent moet liggen zodra meerdere agents orders kunnen plaatsen.
FAQ over een circuit breaker in een trading bot
Wat is een circuit breaker in een trading bot?
Een regel in de risicolaag die voorkomt dat de bot nieuwe orders verstuurt zodra een vastgestelde limiet is bereikt. Meestal gaat het om een dagelijkse verliesdrempel of een drawdown vanaf de equity-piek van de rekening. De regel werkt onafhankelijk van de strategie en wordt bij elke order uitgevoerd. De circuit breaker blijft geactiveerd totdat deze wordt gereset.
Hoe vaak noteert de markt een down day van 2%?
SPY noteerde 5 sessies met een daling van 2% of meer in 2019 en 25 in 2020, bij ongeveer 250 handelsdagen per jaar. Het verschil tussen een rustig en een stressjaar laat zien waarom een verlieslimiet op historische gegevens en niet op intuïtie moet worden afgestemd.
Hoe voorkomt u dat een bot na een stop opnieuw instapt?
Een cooldown-periode na elke stop en een dagelijkse limiet voor het aantal trades per symbool maken van een onbeperkte lus een begrensde lus. De halt-flag moet bovendien worden gelatcht en buiten het procesgeheugen worden bewaard. Anders geeft een supervisor die een gecrashte bot herstart de bot opnieuw een actieve status zonder halt.
Hoe kan een bot vaststellen dat een price feed stale is?
Door vóór het prijzen van een order de ouderdom van elke quote te controleren. Daarnaast moet de bot afzonderlijk van de data een heartbeat van de feed bewaken. Overnight gaps laten zien wat een stale price kan verbergen: de overnight move op het 95e percentiel liep tussen januari 2024 en juli 2026 op tot 4.41% op TSLA.
Heeft een kleine trading bot echt een auditlog nodig?
Een log van fills legt vast wat er is gebeurd. Een log van beslissingen legt vast wat de bot dacht te mogen doen. Dat is de enige manier om een verkeerde strategie te onderscheiden van een risicocontrole die nooit is uitgevoerd. Append-only, inclusief afwijzingen, is de minimaal bruikbare versie.
Elke bovenstaande figure komt uit een stored query op basis van minute bars. Elk paneel geeft toegang tot de onderliggende SQL. Pas dezelfde queries toe op uw eigen symboolselectie in de Strasmore-terminal.