트레이딩 봇 서킷 브레이커 설정법
트레이딩 봇의 일일 손실 한도와 변동성 기반 주문 규모 조정법을 설명합니다. 손실 확산을 막는 기준과 설정 이유를 확인할 수 있습니다.
자동매매 봇의 서킷 브레이커는 정해진 한도에 도달하면 자동화 전략이 주문을 전송하지 못하도록 중단하는 규칙이다. 서킷 브레이커는 전략과 브로커 사이의 리스크 관리 계층에 위치하며, 전략의 동의 여부와 관계없이 모든 주문에 적용된다. 어떤 종목을 거래할지는 전략이 결정한다. 거래 자체를 허용할지는 리스크 관리 계층이 결정한다.
이러한 분리가 설계의 핵심이다. 스스로를 통제하는 전략에는 자체 가정이 무너지는 순간 이를 독립적으로 점검할 장치가 없다. 바로 그 순간에 점검 장치가 가장 필요하다.
트레이딩 봇에서 서킷 브레이커가 하는 일
리스크 관리 계층은 네 가지 요소로 구성되며, 각 요소는 특정한 문제를 방지하기 위해 존재한다.
- 포지션 규모, 종목별 명목 익스포저, 주문 빈도에 하드 캡을 설정한다. 그렇지 않으면 제한 없이 커질 수 있는 버그의 손실 규모를 제한한다.
- 드로다운 서킷 브레이커는 해당 세션에서 계좌가 사전에 정한 금액만큼 하락하거나, 계좌자산이 고점 대비 사전에 정한 금액만큼 감소하면 신규 주문을 중단한다.
- 주문 규모를 변동성에 맞춰 조정한다. 최근 변동성 또는 Kelly 베팅 비중의 일부를 기준으로 주문 규모를 정한다. 고정된 주식 수를 사용하는 대신 시장의 가격 변동폭이 달라져도 거래당 리스크를 대체로 일정하게 유지한다.
- 모든 의사결정을 추가만 가능한 감사 로그에 기록한다. 리스크 관리 계층이 거부한 주문도 포함한다. 이 로그만이 “전략이 잘못됐다”와 “검사가 실행되지 않았다”를 구분해 준다.
이하의 내용은 위 네 가지 요소를 구체화한 것이다.
트레이딩 봇의 합리적인 일일 손실 한도는 얼마인가?
일일 손실 한도는 해당 세션의 손실이 정해진 기준을 넘으면 신규 주문을 중단한다. 적정 수준을 정하는 일은 취향이 아니라 보정의 문제다. 통상적인 시장 소음 안에 한도를 설정하면 봇은 대부분의 주에 거래가 중단된다. 반대로 한도를 지나치게 높게 설정하면 작동하지 않는다. 출발점은 시장 자체가 특정 규모의 하락을 보이는 빈도다.
모든 수치 뒤에 숨겨진 정확한 SQL
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 year1% 이상 하락한 세션은 연간 약 250거래일 가운데 15일이 2019에, 45일이 2020에 발생했다. 3% 이상 하락한 세션의 횟수는 양상이 다르다. 2019에는 0일이 발생한 반면, 2020에는 16일이 발생했다. 9의 마지막 행은 2026년 7월 31일까지의 세션만 반영한다.
이 수치는 일정하게 움직이기보다 들쭉날쭉하며, 바로 그 불규칙성이 설계의 핵심이다. 어려운 장세는 군집을 이루어 나타난다. 봇이 군집의 첫날 거래를 중단하고 둘째 날 재개한다면, 실질적으로 거래를 중단한 것이 아니다.
두 한도는 서로 다른 역할을 한다. 일일 손실 한도는 일반적으로 계좌의 자기자본 대비 2%로 설정하며, 해당 세션을 종료한다. 고점 대비 후행 낙폭 한도는 자기자본의 최고점에서 측정하며, 일반적으로 약 10%로 설정한다. 이 한도에 도달하면 전략을 중단하고 사람의 검토를 거친다. 첫 번째 한도는 일상적인 방어 장치이고, 두 번째 한도는 드물게 작동하도록 설계된다. 첫 번째 한도만 적용하는 봇은 한 번에 2%씩 계좌를 계속 줄이면서도 어떤 차단 장치도 작동시키지 않을 수 있다.
변동성 스케일링은 포지션 규모를 어떻게 바꾸는가?
변동성 타기팅은 최근 실현변동성에 반비례하도록 포지션 규모를 조정한다. 일중 가격 변동폭이 두 배로 커지면 포지션 규모는 대략 절반으로 줄어들어, 거래당 달러 기준 위험을 일정한 수준에 가깝게 유지한다. 여기서 실현변동성은 일간 수익률의 연율화 표준편차를 뜻하며, 많은 투자자가 예상하는 것보다 더 크게 움직인다.
모든 수치 뒤에 숨겨진 정확한 SQL
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)실현변동성은 11.1% 기준으로 연율화해 2024-01 및 12%에 측정됐고, 2026-07에 걸쳐 31개월 동안 집계됐다. 두 번째 열은 각 측정값을 변동성 목표치 12%를 적용했을 때의 포지션 규모로 환산하며, 최대치는 전체 라인으로 제한된다. 100% 및 2024-01에 비해 99.7% 및 2026-07이다. 전략과 확신 수준은 같지만 보유 주식 수는 크게 달라진다.
켈리 기준은 변동성만이 아니라 추정된 초과수익과 분산을 바탕으로 포지션 규모를 산정해 같은 문제에 접근한다. 두 입력값 모두 제한된 표본에서 추정한 값이므로, 대부분의 시스템 트레이더는 켈리 기준의 일부만 적용한다. 일반적으로 하프 켈리 또는 쿼터 켈리를 사용한다. 켈리 기준 포지션 규모 산정에서 관련 계산 과정을 설명한다.
트레이딩 봇은 왜 손절 후 계속 재진입하는가?
손절 주문이 실행된다. 포지션이 청산된다. 90초 후 진입 조건이 다시 충족되고, 봇은 재진입한다. 같은 손실을 다시 기록한 뒤 이 과정을 반복한다. 특정 구성 요소 하나가 고장 난 것은 아니다. 전략은 작성된 대로 작동했고, 손절도 설계된 대로 작동했다. 그 결과 계좌는 한 번의 왕복 거래마다 조금씩 손실이 누적된다.
이러한 반복 빈도는 가격 경로에서 직접 발생한다. 이 패널은 세션별로 SPY가 시가보다 0.1% 넘게 높은 수준에서 시가보다 0.1% 넘게 낮은 수준으로 이동한 횟수 또는 그 반대 방향으로 이동한 횟수를 집계한다.
모든 수치 뒤에 숨겨진 정확한 SQL
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는 2025-08에 세션당 평균 0.7회 해당 구간을 통과했다. 2026-07에는 세션당 1.5회 통과했으며, 2026-07의 한 세션에서는 5회 통과했다. 한쪽에서 진입하고 반대쪽에서 손절하는 규칙은 하루에도 그만큼 실행될 기회를 갖는다.
네 가지 장치로 이를 제한할 수 있다.
- 모든 손절 후 쿨다운을 적용한다. 쿨다운은 분 또는 바 단위로 설정하며, 이 기간에는 해당 종목에 대한 신규 주문이 리스크 관리 계층을 통과하지 못한다.
- 종목별 일일 거래 횟수 상한을 설정한다. 무제한 반복을 제한된 횟수로 바꾸는 장치다.
- 중단 플래그를 래치 방식으로 유지한다. 일일 손실 한도가 발동되면 담당자가 해제할 때까지 계속 발동 상태로 남는다.
- 해당 플래그를 프로세스 메모리 외부에 저장한다. 충돌한 봇을 감독 프로세스가 재시작하면 초기 상태로 실행될 수 있다. 그러나 바로 그 초기 상태를 막기 위해 플래그가 존재한다.
마지막 장치는 다른 조치를 모두 제대로 적용한 사람도 놓치기 쉽다. 그리드 트레이딩 봇은 설계상 여러 가격대에 주문을 계단식으로 배치한다. 따라서 거래 횟수 상한은 형식적인 장치가 아니라 핵심적인 안전장치가 된다.
봇이 오래된 가격 피드로 거래하면 어떻게 되는가?
업데이트가 멈춘 호가도 여전히 숫자로 보인다. 봇은 해당 호가를 읽고 그 가격을 기준으로 주문 가격을 산정한 뒤, 이미 움직인 시장에 주문을 전송한다. 오류는 조용히 발생한다. 예외가 발생하지도 않고 오류 로그도 남지 않으며, 체결 결과가 이상하다는 사실도 사후에야 드러난다.
이를 가장 명확하게 측정할 수 있는 사례가 오버나이트 갭이다. 일정 가격이 몇 시간 동안 변하지 않는 동안 실제 거래 가능한 가격은 움직인다.
모든 수치 뒤에 숨겨진 정확한 SQL
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 DESC95백분위수 갭이 가장 컸던 종목은 TSLA로, 갭은 4.41%였다. 비교 대상인 KO의 갭은 1%였다. 일반적인 밤에는 변동이 훨씬 작았다. 중간값 기준 갭은 각각 1.01%과 0.24%였다. 리스크 관리 계층이 필요한 이유는 꼬리위험에 있다. 측정 기간 중 TSLA에서 발생한 단일 최대 갭은 14.57%였다. 이는 봇이 얼마 전 읽은 가격을 기준으로 행동할 때 노출되는 가격 차이다. 주가가 오버나이트 갭을 보이는 이유에서 그 작동 원리를 설명한다.
방어 장치는 저렴하게 구축할 수 있다. 리스크 관리 계층이 가격 산정에 사용할 수 있는 모든 호가에 최대 경과 시간을 설정해야 한다. 장중 전략에서는 일반적으로 몇 초를 적용한다. 데이터 자체와 별도로 피드에서 하트비트를 받아야 한다. 이를 통해 조용한 시장과 응답이 끊긴 소켓을 구분할 수 있다. 데이터가 누락되면 보유 상태가 아니라 거래 중단으로 처리해야 한다. 가격이 없으면 봇은 청산 조건도 평가할 수 없기 때문이다.
리스크 레이어에 포함해야 할 하드 캡은 무엇인가?
- 계좌 자기자본 대비 종목별 최대 명목금액. 10% 상한을 두면 한 종목의 손실이 계좌 전체 손실로 이어지는 것을 막을 수 있다.
- 모든 미결제 포지션을 합산한 최대 총명목금액. 자기자본의 100%로 설정하면 레버리지를 사용하지 않게 된다. 이는 브로커의 기본 설정을 그대로 따르기보다 명시적으로 결정할 사안이다.
- 분당 및 일일 최대 주문 횟수. 대부분의 개인투자 전략에서는 분당 10건이면 충분히 여유가 있으며, 동시에 폭주하는 루프가 1분 안에 과도한 주문을 내는 것도 제한할 수 있다.
- 종목의 평균 일일 거래량 대비 주문 최대 규모. 1% 상한을 두면 봇이 거래하려는 가격을 스스로 움직이는 일을 막을 수 있다. 이때 분모는 평균 일일 거래량이다.
이 모든 항목은 전략 레이어가 아니라 리스크 레이어에 포함해야 한다. 또한 백테스트, 모의거래, 실거래에서 모두 동일한 코드 경로를 거쳐야 한다. 실거래에서만 존재하는 한도는 아무도 테스트하지 않은 한도다.
트레이딩 봇 감사 로그에 필요한 내용은 무엇인가?
추가 전용 로그는 의사결정마다 하나의 레코드를 기록하며, 기존 레코드를 수정하거나 삭제하지 않는다. 각 레코드에는 타임스탬프, 사용한 호가와 해당 호가의 경과 시간, 실행된 모든 한도 점검과 각 점검의 판정, 전송한 주문, 브로커의 응답이 포함된다. 거부된 주문도 체결된 주문과 동일한 비중으로 기록한다.
핵심은 재구성이다. 문제가 발생한 거래일로부터 6주가 지난 뒤 확인해야 할 사항은 손익이 얼마였는지가 아니다. 어떤 입력값을 기준으로 어떤 점검을 통과했는지가 핵심이다. 입력값을 기록하지 않으면 시장의 상태를 바탕으로 봇의 상태를 다시 추정하게 된다. 이는 백테스팅에서 미래 정보가 유입되는 편향과 같은 실수다. 의사결정 당시 시스템이 보유하지 않았던 정보를 사용하는 것이기 때문이다.
riskguard 프로젝트는 이러한 분리를 구현한 오픈소스 사례 중 하나다. 이 프로젝트에서는 한도 점검이 전략 내부에 흩어진 로직이 아니라 전략이 호출하는 별도 컴포넌트에 배치된다. 여러 설계 방식 중 하나일 뿐이므로, 그대로 도입하기보다 내용을 검토할 가치가 있다. 어떤 도구에 의존하든 기본 브랜치 대신 태그가 지정된 릴리스를 고정해 사용해야 한다. 동일한 백테스트를 두 번 실행하는 사이에도 브랜치는 변경될 수 있다. 조용히 변경된 리스크 계층은 없는 것보다 더 위험하다.
첫 배포가 모의 브로커에서 실행되는 이유
브로커 어댑터의 기본 설정은 모의 거래이며, 실거래는 명시적인 플래그를 설정해야 활성화된다. 이는 의도된 설계다. 방지하려는 문제는 단순하다. 복사한 설정 파일이나 재정의되지 않은 환경변수가 실제 자금으로 주문을 전송할 수 있기 때문이다.
모의 거래를 실행하면 중요한 결과물도 생성된다. 실시간 가격에 동일한 리스크 계층을 적용해 작성한 의사결정 로그다. 이 로그를 통해 어떤 한도가 작동했고 어떤 한도가 작동하지 않았는지 확인할 수 있다. 이는 리스크 계층에 대한 근거이며, 전략이 수익을 내는지와는 별개의 문제다. 실제 자금을 투입하기 전 모의 거래에서는 모의 거래 기록으로 입증할 수 있는 내용과 입증할 수 없는 내용을 다룬다. 또한 멀티 에이전트 AI 트레이딩 시스템에서는 여러 에이전트가 주문을 제출할 수 있을 때 거래 중지 권한을 모든 에이전트 외부에 두어야 하는 이유를 설명한다.
트레이딩 봇 서킷 브레이커 FAQ
트레이딩 봇에서 서킷 브레이커란 무엇인가?
위험 관리 계층에 설정된 규칙으로, 정해진 한도에 도달하면 봇이 신규 주문을 전송하지 못하게 한다. 일반적으로 일일 손실 한도나 계좌 자기자본 고점 대비 낙폭이 기준이 된다. 이 규칙은 전략과 독립적으로 모든 주문에 적용되며, 해제 조건이 충족될 때까지 작동 상태를 유지한다.
시장에서 2% 하락일은 얼마나 자주 발생하는가?
SPY는 2019년에 2% 이상 하락한 거래일을 5회 기록했으며, 2020년에는 25회 기록했다. 각 연도의 거래일은 약 250일이다. 평온한 해와 변동성이 큰 해 사이의 차이가 크기 때문에 손실 한도는 직관이 아니라 과거 데이터에 근거해 조정해야 한다.
중단 후 봇이 다시 진입하지 않도록 하려면 어떻게 해야 하는가?
각 중단 이후 쿨다운 기간을 설정하고 종목별 일일 거래 한도를 두면 무제한 반복을 제한된 반복으로 바꿀 수 있다. 또한 중단 플래그는 프로세스 메모리 외부에 저장하고 유지해야 한다. 그렇지 않으면 감독 프로세스가 장애가 발생한 봇을 재시작할 때 중단되지 않은 새로운 상태를 부여하게 된다.
가격 피드가 오래된 상태라는 점을 봇은 어떻게 판단하는가?
주문 가격을 산정하기 전에 모든 호가의 경과 시간을 확인해야 한다. 이와 별도로 데이터 자체와 구분되는 피드의 하트비트도 감시해야 한다. 장중 거래가 없는 야간에는 오래된 가격이 숨기는 변동 규모가 드러난다. 2024년 1월부터 2026년 7월까지 TSLA의 야간 변동률 95백분위수는 4.41%에 도달했다.
소형 트레이딩 봇에도 정말 감사 로그가 필요한가?
체결 로그는 실제로 발생한 일을 기록한다. 의사결정 로그는 봇이 어떤 거래를 허용된 것으로 판단했는지를 기록한다. 이는 잘못된 전략과 실행되지 않은 위험 점검을 구분할 수 있는 유일한 방법이다. 최소한 유용한 형태는 거부된 주문을 포함한 추가 전용 로그다.
위의 모든 수치는 분봉 데이터를 대상으로 저장된 쿼리에서 산출되며, 각 패널을 열면 그 배경이 된 SQL을 확인할 수 있다. Strasmore 터미널에서 동일한 쿼리를 자신의 종목 목록에 적용하면 된다.