Wyłączniki bezpieczeństwa dla botów transakcyjnych
Wyłączniki zatrzymują bota przed eskalacją strat w trudnej sesji. Sprawdzono limity dziennej straty oraz wpływ skalowania zmienności na wielkość pozycji.
Wyłączniki bezpieczeństwa dla botów transakcyjnych to reguły, które blokują strategię automatyczną przed wysyłaniem zleceń po osiągnięciu określonego limitu. Znajdują się w warstwie zarządzania ryzykiem między strategią a brokerem. Działają przy każdym zleceniu, niezależnie od tego, czy strategia je akceptuje. Strategia decyduje, czym handlować. Warstwa ryzyka decyduje, czy handel w ogóle się odbędzie.
Ten podział stanowi podstawę całej konstrukcji. Strategia, która kontroluje sama siebie, nie ma niezależnego zabezpieczenia w momencie, gdy założenia strategii przestają być trafne. Właśnie wtedy takie zabezpieczenie jest najbardziej potrzebne.
Co robi wyłącznik bezpieczeństwa w bocie transakcyjnym
Warstwa zarządzania ryzykiem składa się z czterech elementów. Każdy z nich zapobiega konkretnemu, przyziemnemu błędowi.
- Twarde limity wielkości pozycji, wartości nominalnej ekspozycji na dany walor oraz częstotliwości składania zleceń. Ograniczają straty wynikające z błędu, który w przeciwnym razie mógłby doprowadzić do nieograniczonego ryzyka.
- Wyłącznik bezpieczeństwa uruchamiany przy obsunięciu, który wstrzymuje składanie nowych zleceń po spadku wartości rachunku o określoną kwotę w trakcie sesji albo po spadku o określoną kwotę względem szczytowego poziomu kapitału.
- Skalowanie wielkości zleceń na podstawie bieżącej zmienności albo jako ułamka stawki Kelly'ego, zamiast stosowania stałej liczby akcji. Pozwala to utrzymać względnie stałe ryzyko pojedynczej transakcji, gdy zakres wahań rynku się zmienia.
- Dziennik audytowy prowadzony wyłącznie w trybie dopisywania, obejmujący każdą decyzję, w tym zlecenia odrzucone przez warstwę zarządzania ryzykiem. Jest to jedyny artefakt pozwalający odróżnić sytuację, w której „strategia była błędna”, od tej, w której „kontrola w ogóle się nie uruchomiła”.
Wszystkie poniższe elementy stanowią konkretne zastosowania jednego z tych czterech mechanizmów.
Jaki dzienny limit straty jest rozsądny dla bota tradingowego?
Dzienny limit straty wstrzymuje składanie nowych zleceń po przekroczeniu przez stratę w danej sesji określonego progu. Wybór wartości jest kwestią kalibracji, a nie gustu. Ustawienie jej w granicach zwykłych wahań rynkowych sprawi, że bot będzie wstrzymywany przez większość tygodni. Ustawienie jej znacznie powyżej tych wahań oznacza, że limit prawdopodobnie nigdy nie zadziała. Punktem wyjścia jest częstotliwość, z jaką sam rynek odnotowuje spadkową sesję o określonej skali.
Dokładny kod SQL dla każdej liczby
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 yearSesji ze spadkiem o 1% lub większym było 15 w 2019 oraz 45 w 2020, przy około 250 sesjach giełdowych rocznie. Liczba sesji ze spadkiem o 3% lub większym przedstawia zupełnie inny obraz: 0 w 2019 wobec 16 w 2020. Ostatni wiersz zestawienia 9 obejmuje wyłącznie sesje do 31 lipca 2026 r.
Przebieg jest nierównomierny, a właśnie ta nierównomierność ma znaczenie. Trudne sesje pojawiają się seriami. Bot, który zostaje wstrzymany pierwszego dnia takiej serii, a następnie wznowiony drugiego dnia, w praktyce nie został skutecznie zatrzymany.
Dwa progi pełnią różne funkcje. Dzienny limit straty, zwykle wynoszący 2% kapitału własnego rachunku, kończy handel w danej sesji. Kroczący limit obsunięcia, liczony od najwyższego poziomu kapitału własnego, zwykle na poziomie około 10%, wyłącza strategię do czasu przeprowadzenia analizy przez człowieka. Pierwszy mechanizm ma zastosowanie rutynowe, drugi powinien uruchamiać się rzadko. Bot wyposażony wyłącznie w pierwszy limit może stopniowo obniżać wartość rachunku o 2% za każdym razem, nie uruchamiając żadnego dodatkowego zabezpieczenia.
How does volatility scaling change position size?
Volatility targeting sizes a position inversely to recent realized volatility: when the daily range doubles, the position roughly halves, holding the dollar risk per trade near constant. Realized volatility here is the annualized standard deviation of daily returns, and it travels further than most people expect.
Dokładny kod SQL dla każdej liczby
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 measured 11.1% annualized in 2024-01 and 12% in 2026-07, across 31 months. The second column turns each reading into the position a 12% volatility target would carry, capped at the full line: 100% in 2024-01 against 99.7% in 2026-07. Same strategy, same conviction, a very different share count.
The Kelly criterion approaches the same problem from the other end, sizing from estimated edge and variance rather than from volatility alone. Most systematic operators run a fraction of it, half or quarter Kelly, given that both inputs are estimates off a finite sample. Kelly criterion position sizing works through that arithmetic.
Dlaczego bot transakcyjny ponownie otwiera pozycję po zleceniu stop?
Zlecenie stop zostaje uruchomione. Pozycja zostaje zamknięta. Dziewięćdziesiąt sekund później warunek wejścia ponownie jest spełniony, bot otwiera pozycję, ponosi tę samą stratę i powtarza cykl. Żaden pojedynczy element nie jest uszkodzony. Strategia działa zgodnie z założeniami, zlecenie stop działa zgodnie z założeniami, a saldo rachunku nadal się kurczy, jedna transakcja zamykająca pozycję po drugiej.
Częstotliwość wynika bezpośrednio z przebiegu ceny. Panel pokazuje, ile razy w każdej sesji SPY przemieszczał się z poziomu przekraczającego o 0,1% cenę otwarcia do poziomu niższego o ponad 0,1% od ceny otwarcia lub w przeciwnym kierunku.
Dokładny kod SQL dla każdej liczby
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 przekraczał ten przedział średnio 0.7 razy na sesję w 2025-08 oraz 1.5 razy w 2026-07. W jednej z sesji w 2026-07 odnotowano 5 takich przejść. Każda reguła, która otwiera pozycję po jednej stronie poziomu, a zlecenie stop ustawia po drugiej, może zostać uruchomiona tyle razy w ciągu jednego dnia.
Ograniczają to cztery mechanizmy.
- Okres wstrzymania po każdym zleceniu stop, liczony w minutach lub świecach, podczas którego żadne nowe zlecenie dla danego instrumentu nie przechodzi przez warstwę kontroli ryzyka.
- Dzienny limit liczby transakcji dla danego instrumentu, który zamienia nieograniczoną pętlę w proces o ograniczonej liczbie powtórzeń.
- Flaga zatrzymania z mechanizmem zatrzaskowym. Po uruchomieniu dziennego limitu straty pozostaje aktywna do czasu ręcznego wyzerowania.
- Zapisywanie tej flagi poza pamięcią procesu. Nadzorca, który uruchamia ponownie po awarii bota, przekazuje mu stan początkowy. Właśnie przed takim stanem ma chronić ta flaga.
Ostatni mechanizm bywa pomijany przez osoby, które wdrożyły wszystkie pozostałe zabezpieczenia. Boty do handlu siatkowego z założenia składają drabiny zleceń, dlatego limit liczby transakcji pełni funkcję kluczowego zabezpieczenia, a nie wyłącznie formalnego dodatku.
Co się dzieje, gdy bot składa zlecenia na podstawie nieaktualnych kwotowań?
Kwotowanie, które przestało się aktualizować, nadal wygląda jak liczba. Bot odczytuje je, wycenia na jego podstawie zlecenie i wysyła je na rynek, który zdążył się zmienić. Błąd pozostaje niezauważony: nic się nie wysypuje, w logach nie pojawia się komunikat o błędzie, a zawarte transakcje wyglądają dziwnie dopiero później.
Najłatwiej mierzalnym przypadkiem jest luka overnight. Znana cena pozostaje niezmieniona przez wiele godzin, podczas gdy cena, po której można zawrzeć transakcję, się zmienia.
Dokładny kod SQL dla każdej liczby
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 DESCNajwiększą wartość luki w ujęciu 95. percentyla odnotowano dla TSLA i wyniosła ona 4.41%, wobec 1% dla KO. Typowe noce były znacznie spokojniejsze: mediana luk wyniosła odpowiednio 1.01% i 0.24%. To właśnie na wypadek takich skrajnych odchyleń buduje się warstwę zarządzania ryzykiem. Największa pojedyncza luka na TSLA w analizowanym okresie wyniosła 14.57%. Są to odległości, jakie może pokonać rynek, gdy bot działa na podstawie ceny odczytanej jakiś czas wcześniej. Mechanizm opisano w artykule Dlaczego akcje otwierają się z luką overnight.
Zabezpieczenia są proste i tanie. Należy ustawić maksymalny wiek każdego kwotowania, na podstawie którego warstwa zarządzania ryzykiem będzie wyceniać zlecenia. W strategii intraday zwykle jest to kilka sekund. Sygnał heartbeat z feedu należy odbierać niezależnie od danych. Pozwala to odróżnić nieaktywny rynek od zerwanego połączenia. Brak danych należy traktować jak zatrzymanie notowań, a nie jak utrzymanie ostatniej ceny. Bot bez kwotowań nie jest bowiem w stanie ocenić również warunków wyjścia z pozycji.
Jakie twarde limity powinny należeć do warstwy ryzyka?
- Maksymalny nominał pozycji w jednym walorze jako udział w kapitale rachunku. Limit na poziomie 10% ogranicza ryzyko, że jedna nietrafiona pozycja obejmie cały rachunek.
- Maksymalny łączny nominał brutto wszystkich otwartych pozycji. Ustawienie go na poziomie 100% kapitału oznacza brak dźwigni. To decyzja, którą należy podjąć świadomie, zamiast dziedziczyć ustawienie domyślne brokera.
- Maksymalna liczba zleceń na minutę i na dzień. Dziesięć zleceń na minutę to dla większości strategii detalicznych wysoki limit, który jednocześnie ogranicza skutki niekontrolowanej pętli w ciągu jednej minuty.
- Maksymalna wielkość zlecenia jako udział w średnim dziennym wolumenie danego waloru. Limit na poziomie 1% ogranicza ryzyko, że bot wpłynie na cenę, po której próbuje zawrzeć transakcję, a mianownikiem jest średni dzienny wolumen.
Każdy z tych limitów powinien należeć do warstwy ryzyka, a każdy powinien korzystać z tej samej ścieżki kodu w backteście, handlu paper i handlu na żywo. Limit, który istnieje wyłącznie w handlu na żywo, jest limitem, którego nikt wcześniej nie przetestował.
Co powinien zawierać dziennik audytowy bota tradingowego?
Dziennik tylko do dopisywania zapisuje jeden rekord dla każdej decyzji i nigdy nie edytuje ani nie usuwa zapisów. Każdy rekord zawiera znacznik czasu, wykorzystaną kwotację i jej wiek, wynik każdej przeprowadzonej kontroli limitów, wysłane zlecenie oraz odpowiedź brokera. Odrzucone zlecenia są zapisywane z taką samą wagą jak zlecenia zrealizowane.
Najważniejsza jest możliwość odtworzenia przebiegu zdarzeń. Sześć tygodni po nieudanej sesji pytanie nigdy nie brzmi: jaki był wynik P&L. Chodzi o ustalenie, która kontrola zakończyła się wynikiem pozytywnym i na jakich danych wejściowych. Bez zapisanych danych wejściowych trzeba odtwarzać stan bota na podstawie stanu rynku. To ten sam błąd co błąd wyprzedzania informacji w testach historycznych: wykorzystywanie informacji, których system nie posiadał w momencie podejmowania decyzji.
Projekt riskguard jest jednym z dostępnych rozwiązań open source rozdzielających te elementy. Kontrole limitów znajdują się w komponencie, do którego odwołuje się strategia, zamiast być rozproszone w samej strategii. Jest to jedno z kilku możliwych podejść. Warto się z nim zapoznać, ale nie należy go bezrefleksyjnie wdrażać. Niezależnie od używanego rozwiązania należy przypiąć oznaczoną wersję, a nie korzystać z domyślnej gałęzi. Gałąź może zmienić się między dwoma uruchomieniami tego samego testu historycznego. Cicho zmieniona warstwa zarządzania ryzykiem jest gorsza niż jej brak.
Dlaczego pierwsze wdrożenie działa na rachunku demonstracyjnym
Adapter brokera domyślnie korzysta z rachunku demonstracyjnego, a handel na rynku rzeczywistym wymaga celowego ustawienia jawnego przełącznika. Zapobiega to typowemu błędowi: skopiowany plik konfiguracyjny albo zmienna środowiskowa, której nie nadpisano, może wysłać rzeczywiste zlecenia i zaangażować realne środki.
Uruchomienie na rachunku demonstracyjnym tworzy także najważniejszy artefakt: dziennik decyzji z tej samej warstwy zarządzania ryzykiem, działającej na rzeczywistych cenach. Pokazuje on, które limity zadziałały, a które nie. Jest to dowód dotyczący warstwy zarządzania ryzykiem, a nie odpowiedź na odrębne pytanie, czy strategia przynosi zyski. Handel demonstracyjny przed użyciem realnych środków omawia, co taki zapis potwierdza, a czego nie potwierdza, natomiast systemy transakcyjne AI z wieloma agentami pokazuje, dlaczego uprawnienia do zatrzymania systemu muszą znajdować się poza każdym agentem, gdy kilka agentów może składać zlecenia.
FAQ dotyczące wyłącznika bezpieczeństwa w bocie transakcyjnym
Czym jest wyłącznik bezpieczeństwa w bocie transakcyjnym?
Jest to reguła w warstwie zarządzania ryzykiem, która zatrzymuje wysyłanie nowych zleceń przez bota po osiągnięciu określonego limitu. Najczęściej chodzi o dzienny próg straty albo spadek wartości kapitału od szczytu equity rachunku. Reguła działa niezależnie od strategii, przy każdym zleceniu, i pozostaje aktywna do czasu jej wyzerowania.
Jak często rynek odnotowuje sesję ze spadkiem o 2%?
SPY odnotował 5 sesji ze spadkiem o co najmniej 2% w 2019 oraz 25 w 2020, przy około 250 sesjach giełdowych w każdym roku. Ta różnica między spokojnym a trudnym rokiem pokazuje, dlaczego limit straty należy kalibrować na podstawie danych historycznych, a nie intuicji.
Jak uniemożliwić botowi ponowne wejście po zatrzymaniu?
Okno schłodzenia po każdym zatrzymaniu oraz dzienny limit transakcji dla każdego symbolu zamieniają nieograniczoną pętlę w proces o określonych granicach. Flaga zatrzymania musi również zostać utrwalona poza pamięcią procesu. W przeciwnym razie nadzorca po ponownym uruchomieniu awarii bota nada mu nowy stan, bez aktywnego zatrzymania.
Jak bot może wykryć, że źródło cen przestało dostarczać aktualne dane?
Należy sprawdzać wiek każdego kwotowania przed wyceną zlecenia na jego podstawie oraz niezależnie monitorować sygnał heartbeat ze źródła danych. Luki po sesji pokazują skalę ryzyka ukrywanego przez nieaktualną cenę: ruch overnight w 95. percentylu osiągnął 4.41% w dniu TSLA w okresie od stycznia 2024 roku do lipca 2026 roku.
Czy mały bot transakcyjny rzeczywiście potrzebuje dziennika audytowego?
Dziennik wykonanych transakcji rejestruje to, co się wydarzyło. Dziennik decyzji rejestruje to, co bot uznawał za dozwolone. Jest to jedyny sposób na odróżnienie błędnej strategii od kontroli ryzyka, która nigdy się nie uruchomiła. Minimalnie użyteczna wersja powinna być prowadzona wyłącznie w trybie dopisywania i obejmować również odrzucone zlecenia.
Wszystkie powyższe dane liczbowe pochodzą z zapisanych zapytań wykonywanych na słupkach minutowych, a każdy panel otwiera SQL wykorzystany do jego utworzenia. Te same zapytania można zastosować do własnej listy symboli w terminalu Strasmore.