08. 파산하지 않는 기술

파산하지 않는 기술

아무리 좋은 모델도

v2.5 모델의 백테스트 결과를 처음 봤을 때, 나는 꽤 자신감이 있었다. 승률 68.51%, MDD 6.96%. 나쁘지 않은 숫자였다. 하지만 실전에 넣어보니 현실은 달랐다.

모델이 맞추든 틀리든, 한 번의 큰 손실이 열 번의 작은 수익을 날려버렸다. 연속으로 손절이 터지면 멘탈이 흔들리고, 멘탈이 흔들리면 시스템을 끄고 싶어진다. 자동매매인데도.

아무리 좋은 모델도 리스크 관리 없이는 파산한다.

이 말이 진심으로 와닿은 건, 내 돈이 실제로 줄어드는 걸 지켜본 다음이었다. v2.6에서 리스크 관리 시스템을 전면 설계했고, v2.7에서 그 효과가 증명되었다. MDD 3.62%, 13개월 연속 양수 수익.


리스크 관리의 핵심 원칙

앞선 편들에서 기대수익 공식, Sharpe Ratio, MDD, Profit Factor 등 핵심 지표의 수학적 정의를 다뤘다. 이번 편에서는 정의보다 적용에 집중한다. 리스크 관리 시스템을 설계하면서 내가 집중한 건 하나였다.

수익을 극대화하는 것이 아니라, 손실을 통제할 수 있는 구조를 만드는 것.

기대수익 공식 $E[R] = WR \cdot \overline{Win} - (1-WR) \cdot \overline{Loss}$에서, $\overline{Win}$은 시장에 의존하지만 $\overline{Loss}$는 시스템으로 통제할 수 있다. 그리고 MDD가 작을수록 복구가 쉽다. 3.62% 손실은 3.76%만 벌면 되지만, 50% 손실은 100%를 벌어야 한다. 이 비대칭성이 리스크 관리가 존재하는 이유다.


Circuit Breaker: 연속 손실 방어

주식 시장에 서킷브레이커가 있듯이, 내 시스템에도 서킷브레이커를 넣었다. 핵심 로직은 간단하다.

연속 손실 쿨다운

연속 3회 손절이 발생하면 거래를 일시 중단한다. 단순히 1시간 쉬는 게 아니라, 지수적 백오프를 적용했다.

$$T_{cooldown} = T_0 \cdot 2^{n-1}$$

여기서 $T_0$는 기본 쿨다운 시간(1시간), $n$은 쿨다운 레벨이다. 첫 발동 시 1시간, 쿨다운 후에도 또 3연패면 2시간, 또 3연패면 4시간, 최대 8시간이다.

# 지수적 백오프 구현
cooldown_seconds = base_cooldown * (2 ** cooldown_level)
cooldown_seconds = min(cooldown_seconds, 8 * 3600)  # 최대 8시간 캡

왜 지수적 백오프인가? 이것은 컴퓨터 네트워크의 충돌 해결 알고리즘(Ethernet의 Binary Exponential Backoff)에서 빌려온 개념이다. 연속 손실은 대부분 시장 환경이 모델에 불리하게 바뀌었다는 신호다. 이럴 때는 쉬는 시간을 점점 늘려서 시장이 정상화될 때까지 기다리는 게 합리적이다.

소개팅에 비유하면, 첫 번째 실패 후 1주 쉬고, 또 실패하면 2주, 또 실패하면 한 달 쉬는 것과 같다. 계속 안 되면 "지금은 내 타이밍이 아닌가 보다" 하고 더 오래 쉬어야 효율적이다.

선형 증가($T = T_0 \cdot n$)가 아닌 지수 증가를 선택한 이유는, 연속 실패의 의미가 비선형적이기 때문이다. 3연패는 우연일 수 있지만, 6연패(쿨다운 2회 연속 발동)는 시장 레짐 변화를 강력히 시사한다. 이 경우 훨씬 오래 쉬어야 한다.

중요한 건, 승리하면 즉시 완전 리셋된다는 것이다. 모델이 다시 맞추기 시작했다면 시장 환경이 돌아왔다는 뜻이니까.

if is_win:
    self.consecutive_losses = 0
    self.cooldown_until = None
    self.cooldown_level = 0  # 레벨도 완전 리셋

일일 손실 한도

서킷브레이커만으로는 부족하다. 하루 종일 소소하게 손절이 쌓이는 경우도 있다.

# 일일 6회 손실 OR 누적 15% 드로다운 → 당일 거래 완전 중지
V26_DAILY_MAX_LOSSES = 6
V26_DAILY_MAX_LOSS_PCT = 15.0  # 증거금 대비

6번 지면 오늘은 끝이다. 또는 누적 손실이 증거금의 15%를 넘어도 끝. UTC 자정에 카운터가 리셋된다. 이건 내가 직접 겪은 경험에서 나온 숫자다. 6번 연속 틀리는 날에 7번째를 맞추는 건 기적에 가깝다.

실제 백테스트에서 서킷브레이커가 발동된 건 98회였다. 전체 거래 거부의 1.4%에 불과하지만, 이 98번이 없었다면 드로다운은 훨씬 컸을 것이다.


Trailing Stop: 수익을 지키는 기술

손실을 제한하는 것만큼 중요한 게, 수익을 지키는 것이다. 내가 가장 고통받았던 시나리오가 있다.

포지션이 +12% 찍었다가 급반전해서 -4%에 손절.

이걸 막기 위해 트레일링 스탑을 넣었다.

# 증거금 대비 +8% 미실현 이익 도달 시 트레일링 활성화
TRAILING_STOP_ACTIVATION = 8.0  # %

# 활성화 후, 최고점 대비 3% 하락하면 청산
TRAILING_STOP_DISTANCE = 3.0    # %

작동 방식은 이렇다:

  1. 포지션 진입
  2. PnL이 +8%(증거금 대비)에 도달하면 트레일링 스탑 활성화
  3. 이후 최고 PnL을 계속 추적
  4. 최고점에서 3% 하락하면 즉시 청산

예를 들어, PnL이 +8% -> +12% -> +15%로 올라가면 최고점은 15%. 여기서 12%로 떨어지면 (15 - 12 = 3%) 트레일링 스탑이 발동된다. +12%에서 수익 실현.

트레일링 스탑의 Risk-Reward Ratio를 생각해보자. Risk-Reward Ratio는 다음과 같이 정의된다.

$$RRR = \frac{\text{Expected Profit}}{\text{Maximum Risk}}$$

트레일링 스탑이 활성화된 시점에서, 최소 수익은 $8\% - 3\% = 5\%$이고 (활성화 직후 최고점에서 바로 3% 하락하는 최악의 경우), 최대 리스크는 사실상 0이다(이미 수익 영역이므로). 활성화 이후에는 손실 없이 수익만 확보하는 구조다.

등산하면서 중간 캠프를 설치하는 것과 같다. 8부 능선(+8%)까지 올라가면 캠프를 치고, 이후로 더 올라가되 3부 능선 이상 내려오면 그 캠프에서 하산한다. 최소한 8부 능선 - 3부 능선 = 5부 능선의 성과는 확보하는 전략이다.

백테스트에서 트레일링 스탑으로 청산된 거래는 298건이었고, 승률 100%, 평균 PnL +5.87%. 총 $270,734의 수익을 지켜냈다. 이 기능이 없었다면 이 수익의 상당 부분이 증발했을 것이다.


ATR 기반 동적 스탑로스

고정 스탑로스의 문제는 명확하다. 시장 변동성이 높을 때와 낮을 때 같은 퍼센트를 쓰면, 변동성 높은 날에 너무 쉽게 손절당하고 낮은 날에는 너무 늦게 손절한다.

그래서 ATR(Average True Range)을 기반으로 동적 스탑로스를 설계했다. 7편에서 ATR의 수학적 정의를 다뤘으니 여기서는 적용 방식에 집중하겠다.

동적 스탑로스의 수학적 정의:

$$SL_{dynamic} = \text{clamp}\left(ATR \times M, \; SL_{floor}, \; SL_{cap}\right)$$

여기서 $M$은 ATR 배수(multiplier), $SL_{floor}$는 하한선(4%), $SL_{cap}$은 상한선이다. clamp 함수는 값을 범위 내로 제한한다.

구체적으로:

$$M \in [M_{min}, M_{max}] = [0.75, 2.0]$$

$$SL_{dynamic} = \max\left(ATR\% \times M, \; 4.0\%\right)$$

ATR_SL_MIN_MULTIPLIER = 0.75   # 최소 ATR 0.75배
ATR_SL_MAX_MULTIPLIER = 2.0    # 최대 ATR 2.0배
ATR_SL_BASE_PERCENT = 0.01     # 기준값 1%
ATR_SL_HARD_FLOOR = 4.0        # 절대 최소 4% (증거금 대비)

ATR이 높으면(시장이 요동칠 때) 스탑로스를 넓게, ATR이 낮으면(조용할 때) 좁게. 하지만 아무리 좁아도 증거금의 4%는 보장한다. 이 하드 플로어가 없으면, 아주 조용한 시장에서 미세한 노이즈에 손절당하는 문제가 생긴다.

우산 챙기기에 비유하면, 비 올 확률이 높은 날은 큰 우산(넓은 SL)을 가져가고, 맑은 날은 접이식 우산(좁은 SL)을 챙기는 것이다. 다만 아무리 맑아도 최소한 접이식 우산 하나는 가방에 넣어두는 게 하드 플로어 4%다.

초기 v2.6에서는 최소 배수를 0.5로 설정했었는데, SL이 너무 타이트해서 정상적인 가격 변동에도 잘려나갔다. 이것은 통계적으로 설명할 수 있다. 5분봉의 가격 변동이 대략 정규분포를 따른다고 가정하면, SL이 $0.5 \times ATR$(약 $0.5\sigma$)이면 정상적인 노이즈에 의해 잘릴 확률이 약 38%다. $0.75 \times ATR$로 올리면 이 확률이 약 23%로 줄어든다. 0.75로 올리고 하드 플로어 4%를 추가한 후에야 안정됐다.


하드 플로어와 하드 캡

모든 동적 시스템에는 안전장치가 필요하다.

# 손절 하드 플로어: 어떤 경우에도 최소 4%
ATR_SL_HARD_FLOOR = 4.0

# 익절 하드 캡: 증거금 30% 도달 시 강제 청산
TP_HOLD_HARD_CAP = 30.0

# 수익 하한선: 10% 아래로 내려오면 청산
TP_HOLD_FLOOR = 10.0

# 최대 홀딩 시간: 120분
TP_HOLD_MAX_DURATION = 120

하드 캡 30%는 "욕심내지 마라"는 안전장치다. 백테스트에서 실제로 2건이 하드 캡에 걸렸는데, 평균 PnL +29.2%였다. 30%까지 올라간 포지션이 반전할 수 있으니까, 여기서 확실히 챙기는 게 맞다.


포지션 사이징과 시간 필터

보수적 사이징과 Kelly Criterion

POSITION_SIZE = 10      # 마진의 10%
MAX_POSITIONS = 1       # 동시 포지션 1개
LEVERAGE = 10           # 10x 레버리지

마진의 10%만 사용하고, 동시 포지션은 1개로 제한한다. 10x 레버리지에서 마진 10%면, 전체 자본의 1%를 한 거래에 노출하는 셈이다. 보수적이지만, 이 보수성이 MDD 3.62%를 만들었다.

포지션 사이징의 이론적 최적해는 Kelly Criterion이 제시한다.

$$f^* = \frac{p \cdot b - q}{b}$$

여기서 $f^*$는 최적 베팅 비율(전체 자본 대비), $p$는 승률, $q = 1 - p$는 패률, $b$는 승리 시 보상 대 패배 시 손실의 비율(odds)이다.

v2.7의 숫자를 대입해보면: $p = 0.735$, $q = 0.265$, $b = \frac{\overline{Win}}{\overline{Loss}} \approx \frac{5.87}{3.2} \approx 1.83$.

$$f^* = \frac{0.735 \times 1.83 - 0.265}{1.83} \approx \frac{1.346 - 0.265}{1.83} \approx 0.59$$

Kelly Criterion은 자본의 59%를 베팅하라고 한다. 하지만 실전에서 Kelly 비율 그대로 쓰는 사람은 없다.

뷔페에서 접시에 음식을 담는 것과 비슷하다. Kelly는 "위장 용량의 59%까지 채워라"라고 하는데, 실제로 그렇게 담으면 배탈 난다. 70% 용량으로 계산했는데 실제 위장이 더 작을 수도 있으니까. 반만 담는 게(Half Kelly) 현실적인 이유다.

이유는 두 가지다.

첫째, Kelly는 $p$와 $b$를 정확히 안다고 가정하지만, 실전에서는 추정치에 불과하다. 추정 오차가 있으면 Kelly 비율은 과도한 리스크를 초래한다.

둘째, Kelly는 기대값(기대 로그 수익률)을 최적화하지만, 그 과정에서 발생하는 드로다운을 무시한다. Full Kelly를 쓰면 장기적으로 수익이 최대화되지만, 도중에 -50% 이상의 드로다운을 경험할 수 있다.

실무에서는 보통 Half Kelly ($f = f^*/2$) 또는 Quarter Kelly ($f = f^*/4$)를 사용한다. 내 시스템의 실효 베팅 비율 약 1% (마진 10% x 레버리지 보정)는 대략 Kelly의 1/60 수준이다. 극도로 보수적이지만, 이 보수성이 MDD 3.62%라는 결과를 만들었다.

동시 포지션 1개 제한은 단순해 보이지만 강력하다. 상관관계 높은 두 포지션이 동시에 손절당하는 시나리오를 원천 차단한다.

시간대 필터

모든 시간이 거래에 적합한 건 아니다.

# UTC 0-3시, 20-23시 거래 제외
EXCLUDED_HOURS_UTC = [0, 1, 2, 3, 20, 21, 22, 23]

UTC 기준 03시(한국 시간 912시 오전)와 2023시(한국 시간 새벽 58시)는 거래를 하지 않는다. 이 시간대는 유동성이 낮고, 스프레드가 넓어지며, 가격 변동이 불규칙하다.

백테스트에서 시간 필터로 거부된 거래는 2,781건(39.2%). 전체 거래 거부의 두 번째로 큰 사유였다. 하루의 1/3을 쉬는 셈인데, 이 1/3이 불필요한 손실을 막아준다.


전체 방어 계층 요약

내 리스크 관리 시스템은 여러 겹의 방어선으로 구성된다. 이것은 소프트웨어 공학의 Defense in Depth 원칙과 같다. 어떤 한 계층이 실패해도 다음 계층이 방어한다.

계층 메커니즘 역할 수학적 근거
1 시간대 필터 저유동성 시간 회피 유동성과 스프레드의 시간대별 분포
2 서킷브레이커 연속 손실/일일 한도 방어 지수 백오프 $T_0 \cdot 2^{n-1}$
3 ATR 동적 SL 변동성 적응형 손절 $SL = \max(ATR \times M, 4\%)$
4 하드 플로어 SL 최소 손절폭 보장 노이즈 필터링 확률 임계값
5 트레일링 스탑 수익 보존 활성화 후 RRR > 1 보장
6 하드 캡 TP 최대 익절 강제 실현 극단값 리스크 제한
7 포지션 사이징 단일 거래 리스크 제한 Kelly Criterion의 보수적 적용

6편에서 보여준 v2.7의 결과 중 내가 정말 자랑스러운 건 MDD 3.62%다. 승률 73.5%보다, 최대 낙폭 4% 미만이 리스크 관리 시스템의 진짜 성과다.


배운 것

리스크 관리를 설계하면서 가장 크게 깨달은 건, 수익을 극대화하는 것보다 손실을 최소화하는 것이 더 중요하다는 것이다.

수학적으로도 그렇다. 기대수익 공식 $E[R] = WR \cdot \overline{Win} - (1-WR) \cdot \overline{Loss}$를 보면, $E[R]$을 키우는 방법은 두 가지다. $\overline{Win}$을 키우거나 $\overline{Loss}$를 줄이거나. 하지만 실전에서는 $\overline{Loss}$를 줄이는 것이 훨씬 안정적이다. $\overline{Win}$은 시장 환경에 크게 의존하지만, $\overline{Loss}$는 시스템으로 통제할 수 있기 때문이다.

그리고 리스크 관리는 한 번 설계하면 끝이 아니다. 처음에 설정한 값들이 대부분 틀렸다. ATR 최소 배수 0.5가 너무 타이트하다는 걸 알기까지 여러 번의 백테스트가 필요했고, 하드 플로어 4%라는 숫자도 시행착오의 결과다.

시스템을 만드는 건 한 달이면 되지만, 파라미터를 튜닝하는 건 끝이 없다. 그래도 이 작업이 가장 중요하다. 모델이 틀릴 때 나를 지켜주는 건 결국 리스크 관리니까.


실전을 위한 경량화: Celeron에서 돌아가는 모델

리스크 관리 시스템을 완성하고 나니, 다음 문제가 생겼다. 실전 서버가 너무 약했다.

24시간 돌아가는 자동매매 서버로 쓸 수 있는 건 구석에 놓인 Celeron J4125 미니PC뿐이었다. TensorFlow가 포함된 LSTM 앙상블은 이 하드웨어에서 예측 한 번에 수 초가 걸렸다. 5분봉 트레이딩에서 수 초의 지연은 치명적이다.

결국 v2.5 Lite를 만들었다. TensorFlow를 완전히 제거하고 XGBoost + LightGBM 앙상블만 남겼다. 6편에서 이야기한 "모델 단순화의 역설"이 여기서 실전으로 증명된 셈이다. LSTM 없이도 트리 모델 두 개로 충분했다. 오히려 예측 속도가 10배 빨라지면서 레이턴시가 줄고, 슬리피지도 줄었다.

좋은 모델이 아니라, 제 시간에 답을 내는 모델이 돈을 번다.

이 경량화 과정에서 리스크 관리 파라미터도 재조정했다. 서버가 약하면 장애 확률이 높으니, 포지션 사이징을 더 보수적으로, 서킷브레이커를 더 민감하게 설정했다. 하드웨어 제약이 오히려 더 안전한 시스템을 만든 아이러니한 상황이였다.