자동매매 리스크 관리 완전 가이드 — 포지션 사이징·손절·드로다운·켈리 공식
RiskManager 클래스까지, 모든 단계를 Python 풀코드로 정리했습니다. "좋은 전략 = 8할" 이지만 "리스크 관리 = 9할"입니다.
자동매매에서 사고는 대부분 "전략이 틀려서"가 아니라 "리스크 관리가 없어서" 발생합니다. 백테스트에서 좋게 나온 전략을 실전에 투입했다가 한 번의 큰 변동에 자본 절반이 사라지는 사례 — 알고랩이 의뢰자들에게서 가장 많이 듣는 이야기입니다.
이 글은 입문자도 따라할 수 있는 리스크 관리의 8부 프레임워크를 정리했습니다. 모든 단계에 Python 코드가 있고, 통합 RiskManager 클래스를 만들어 어떤 봇에든 그대로 붙여 쓸 수 있게 했습니다. 한 번 익혀두면 평생 쓸 수 있는 골격입니다.
이 글의 8부 구성
- Part 1 — 리스크 관리의 본질 (왜 전략보다 중요한가)
- Part 2 — 포지션 사이징 (1~2% 룰 · 켈리 공식 · 변동성 기반)
- Part 3 — 5가지 손절 규칙 (고정·ATR·기술적·시간·트레일링)
- Part 4 — 4가지 익절 전략 (고정·R:R·부분·트레일링)
- Part 5 — 다종목·다전략 분산 (상관관계 · 합산 리스크)
- Part 6 — 드로다운 관리 (MDD · 회복기간 · Circuit Breaker)
- Part 7 — 통합 RiskManager 클래스 (모든 봇에 붙이는 골격)
- Part 8 — 16단계 체크리스트
Part 1 — 리스크 관리의 본질
전략이 8할, 리스크 관리가 9할. 한 번의 큰 손실은 모든 좋은 거래를 무력화합니다.
왜 리스크 관리가 전략보다 중요한가
같은 50% 승률 전략이라도 손익비(평균이익 ÷ 평균손실)에 따라 결과가 정반대입니다.
| 승률 | 손익비 | 100거래 후 기대 수익률 (자본의) | 판정 |
|---|---|---|---|
| 70% | 1:3 | −40% | ❌ 망함 |
| 50% | 1:1 | 0% | 본전 |
| 50% | 2:1 | +50% | ✅ 수익 |
| 40% | 3:1 | +80% | ✅ 큰 수익 |
승률 70%가 좋아 보이지만, 손익비 1:3이면 결국 망합니다. 반대로 승률 40%여도 손익비 3:1이면 큰 수익. 승률이 아니라 손익비가 수익을 결정합니다. 그리고 손익비는 익절·손절 규칙으로 만들어집니다.
큰 손실의 비대칭성 — 회복의 어려움
한 번의 큰 손실은 회복이 매우 어렵습니다. 다음은 원금 회복에 필요한 수익률입니다.
| 손실 | 원금 회복에 필요한 수익률 |
|---|---|
| −10% | +11% |
| −20% | +25% |
| −30% | +43% |
| −50% | +100% |
| −80% | +400% |
⚠️ -50%는 사실상 끝: 자본의 절반을 잃으면 원금 회복에 +100%가 필요합니다. 1년 시장 평균 수익률이 +10%인데 +100%는 비현실적입니다. 큰 손실을 막는 것이 큰 수익을 내는 것보다 100배 중요합니다.
리스크 관리 부재의 5가지 실패 패턴
- 손절 규칙 없음 — "조금만 더 기다리면 돌아오겠지"로 -30% 누적
- 한 종목 올인 — 분산 없이 한 자산이 망하면 같이 망함
- 레버리지 과다 — 5배 레버리지에 -20% 움직임으로 청산
- 물타기 — 손실 중인 종목에 자본 추가 → 손실 확대
- 일일 손실 한도 없음 — 나쁜 날 봇이 계속 매매해 누적 손실
이 5가지가 어떤 심리에서 비롯되는지: → 자동매매 전략 30가지 완전 정리
Part 2 — 포지션 사이징
한 거래에 자본의 몇 %를 걸지 결정. 가장 단순한 1~2% 룰부터 켈리 공식까지.
방법 1. 1~2% 룰 (입문자 표준)
한 거래에서 잃을 수 있는 최대 금액을 자본의 1~2%로 제한. 손절가까지의 거리로 포지션 크기 역산.
def position_size_fixed(capital, risk_pct, entry, stop):
"""1~2% 룰 — 자본 대비 리스크 % 기준 포지션 사이징"""
risk_amount = capital * (risk_pct / 100)
risk_per_share = abs(entry - stop)
if risk_per_share == 0:
return 0
qty = risk_amount / risk_per_share
return qty
# 예: 자본 1000만원, 리스크 1%, 진입 60,000원, 손절 58,500원
qty = position_size_fixed(10_000_000, 1, 60000, 58500)
print(f"매수 수량: {qty:.2f}주")
# 매수 수량: 66.67주 (손절 시 자본의 1% = 10만원 손실)
✅ 1% 룰의 강력함: 연속 20번 손절을 봐도 자본의 20%만 잃습니다 (정확히는 18.2%, 복리 영향). 연속 50번 손절은 자본의 39%. 즉 전략이 한참 안 통하는 시기를 견딜 수 있는 유일한 방법이 1~2% 룰입니다.
방법 2. 켈리 공식 (Kelly Criterion)
승률(p)과 손익비(b)를 알 때 자본 대비 최적 베팅 비율을 계산합니다. 1956년 정보이론에서 유래.
def kelly_fraction(win_rate, win_loss_ratio):
"""켈리 공식: 자본 대비 최적 베팅 비율"""
p = win_rate
b = win_loss_ratio # 평균이익 / 평균손실
q = 1 - p
f = (b * p - q) / b
return max(0, f)
# 예: 승률 55%, 손익비 1.5
f = kelly_fraction(0.55, 1.5)
print(f"켈리 비율: {f:.2%}")
# 켈리 비율: 25.00% — 매 거래에 자본의 25%
⚠️ 켈리 공식 그대로 쓰면 안 됨: 위 예시처럼 25%가 나오면 자본의 25%를 한 번에 거는 셈인데, 이는 변동성이 너무 큽니다. 실전에서는 Half-Kelly(켈리값의 1/2) 또는 Quarter-Kelly(1/4)을 씁니다. 또한 승률·손익비 추정이 약간만 틀려도 켈리값이 크게 왜곡됩니다.
def half_kelly(win_rate, win_loss_ratio):
"""안전한 Half-Kelly"""
return kelly_fraction(win_rate, win_loss_ratio) * 0.5
# 위 예시: 25% → 12.5%
# 여전히 1~2% 룰보다 크므로, 입문자는 1~2% 룰 추천
방법 3. ATR 기반 변동성 사이징
시장 변동성(ATR)에 반비례하게 포지션 크기 조정. 변동성 큰 시기에는 작게, 작은 시기에는 크게.
import pandas as pd
def atr(df, period=14):
"""Average True Range — 변동성 지표"""
high_low = df["high"] - df["low"]
high_close = (df["high"] - df["close"].shift()).abs()
low_close = (df["low"] - df["close"].shift()).abs()
tr = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
return tr.rolling(period).mean()
def position_size_atr(capital, risk_pct, atr_value, k=2):
"""ATR 기반 사이징: 손절폭 = k × ATR"""
risk_amount = capital * (risk_pct / 100)
stop_distance = k * atr_value
if stop_distance == 0:
return 0
return risk_amount / stop_distance
# 예: ATR이 2,000원이면 손절폭 = 2 × 2,000 = 4,000원
qty = position_size_atr(10_000_000, 1, 2000, k=2)
print(f"매수 수량: {qty:.2f}주 (ATR 변동성 기반)")
방법별 비교
| 방법 | 장점 | 단점 | 추천 대상 |
|---|---|---|---|
| 1~2% 룰 | 단순, 안전 | 최적 아님 | 입문자 무조건 |
| Half-Kelly | 이론적으로 최적 | 승률·손익비 추정 오차에 민감 | 검증된 전략 운영자 |
| ATR 기반 | 시장 변동성 자동 반영 | ATR 계산 필요 | 변동성 큰 자산(코인) |
Part 3 — 5가지 손절 규칙
손절은 의지의 문제가 아니라 코드의 문제. 봇이 무조건 실행하게.
1. 고정 % 손절
진입가 대비 N% 하락 시 즉시 청산. 가장 단순.
def fixed_pct_stop(entry, stop_pct=3):
"""진입가 -3% 손절"""
return entry * (1 - stop_pct / 100)
장점: 구현 단순. 단점: 시장 변동성 무시 (변동성 큰 자산은 너무 자주 손절).
2. ATR 기반 손절 (권장)
손절폭 = N × ATR. 시장 변동성에 자동 적응.
def atr_stop(entry, atr_value, k=2):
"""진입가 - 2×ATR 손절"""
return entry - k * atr_value
장점: 변동성 자동 반영. 단점: ATR 계산 필요.
3. 기술적 손절 (지지선 깨짐)
최근 N봉의 최저가 또는 이동평균선 아래로 깨지면 청산.
def technical_stop(df, lookback=20):
"""최근 20봉의 최저가를 손절선으로"""
return df["low"].tail(lookback).min()
장점: 시장 구조 반영. 단점: 손절폭이 매번 다름 → 포지션 사이징 어려움.
4. 시간 손절
진입 후 N봉(또는 N일) 동안 수익 안 나면 청산. "시간 가치" 개념.
def time_stop(entry_bar, current_bar, max_bars=20):
"""진입 후 20봉 지나면 청산 (수익 무관)"""
return current_bar - entry_bar >= max_bars
장점: 죽은 포지션 정리, 자본 효율. 단점: 큰 추세 놓칠 수 있음.
5. 트레일링 스탑 (Trailing Stop)
가격이 오를 때마다 손절선도 따라 올라감. 이익 보호 + 추세 따라가기.
class TrailingStop:
def __init__(self, entry, trail_pct=5):
self.entry = entry
self.trail_pct = trail_pct
self.highest = entry
self.stop = entry * (1 - trail_pct / 100)
def update(self, current_price):
if current_price > self.highest:
self.highest = current_price
self.stop = self.highest * (1 - self.trail_pct / 100)
return current_price <= self.stop # True면 손절
# 예: 60,000원 진입, 5% 트레일링
ts = TrailingStop(60000, trail_pct=5)
ts.update(63000) # 최고가 63,000 → 손절선 59,850으로 상향
ts.update(64500) # 최고가 64,500 → 손절선 61,275로 상향
ts.update(61000) # 손절선 아래 → True 반환 (청산)
장점: 이익 보호 + 추세 추종. 단점: 잦은 휩쏘에 손절 자주.
손절 규칙 선택 매트릭스
| 시장 특성 | 추천 손절 |
|---|---|
| 저변동성 주식 (일봉) | 고정 % 또는 ATR |
| 고변동성 코인 (분봉) | ATR (k=2~3) |
| 박스권 횡보 | 기술적 (박스권 하단) |
| 강추세 종목 | 트레일링 스탑 |
| 분봉 스캘핑 | 시간 손절 + 고정 % |
Part 4 — 4가지 익절 전략
"언제 팔지"는 "언제 살지"만큼 중요. 손익비를 만드는 핵심.
1. 고정 익절 (Target Price)
진입가 +N% 도달 시 청산.
def fixed_target(entry, target_pct=6):
return entry * (1 + target_pct / 100)
2. R:R 기반 익절 (Risk:Reward Ratio)
손절 거리의 N배에서 청산. 손익비 명시.
def rr_target(entry, stop, rr=2):
"""손절 거리의 2배에서 익절 (R:R = 1:2)"""
risk = entry - stop
return entry + risk * rr
R:R 2 이상 권장. 승률 40%여도 R:R 2면 장기 수익.
3. 부분 익절 (Scale-out)
1차 목표 도달 시 절반 청산 + 손절선을 본전으로 이동 (위험 0). 나머지는 추세 따라.
class ScaleOutPosition:
def __init__(self, entry, stop, target1_rr=1, target2_rr=3):
self.entry = entry
self.stop = stop
risk = entry - stop
self.target1 = entry + risk * target1_rr
self.target2 = entry + risk * target2_rr
self.scaled_out = False
def check(self, current):
if not self.scaled_out and current >= self.target1:
self.stop = self.entry # 손절선 본전 이동
self.scaled_out = True
return "PARTIAL_EXIT" # 절반 청산
if current >= self.target2:
return "FULL_EXIT"
if current <= self.stop:
return "STOP_OUT"
return None
장점: 일부 이익 확정 + 나머지로 큰 추세 따라가기. 알고랩이 의뢰 봇에 가장 많이 쓰는 패턴.
4. 트레일링 익절
트레일링 스탑과 동일하지만 익절 영역에서 작동. 큰 추세를 끝까지 따라감.
Part 5 — 다종목·다전략 분산
봇을 여러 개 돌리는 것은 분산이 아니라 곱셈일 수 있습니다.
상관관계 매트릭스 — 가짜 분산 피하기
"5종목 분산했어요" 했는데 모두 반도체 종목이면 분산이 아닙니다. 상관관계 0.7 이상이면 사실상 같은 자산.
import pandas as pd
def correlation_matrix(prices_df):
"""일일 수익률 기준 상관관계"""
returns = prices_df.pct_change().dropna()
return returns.corr()
# 예: 5종목 일봉 데이터
corr = correlation_matrix(df_5stocks)
print(corr)
# 0.7 이상은 사실상 동일 자산으로 취급
합산 리스크 한도
봇을 N개 운영해도 모든 봇의 한 거래 리스크 합이 자본의 5% 이내여야 합니다.
class PortfolioRiskCheck:
def __init__(self, total_capital, max_portfolio_risk=5):
self.total = total_capital
self.max_risk = max_portfolio_risk
self.open_positions = [] # 각 포지션의 risk_pct
def can_open(self, new_risk_pct):
current = sum(p["risk_pct"] for p in self.open_positions)
return current + new_risk_pct <= self.max_risk
def open(self, symbol, risk_pct):
if self.can_open(risk_pct):
self.open_positions.append({"symbol": symbol, "risk_pct": risk_pct})
return True
return False # 합산 한도 초과
자산군 분산
| 분산 단계 | 예시 |
|---|---|
| 1. 종목 분산 | 한 섹터 안에 5종목 (약한 분산) |
| 2. 섹터 분산 | 반도체·금융·바이오 등 (중간 분산) |
| 3. 자산군 분산 | 주식·코인·선물·채권 (강한 분산) |
| 4. 전략 분산 | 추세 + 평균회귀 + 차익 (가장 강력) |
Part 6 — 드로다운 관리
MDD가 한도를 넘으면 봇 자체를 멈춰야 합니다. Circuit Breaker.
드로다운 지표 3가지
| 지표 | 의미 | 경계값 |
|---|---|---|
| MDD (Maximum Drawdown) | 역대 최대 낙폭 | -25% 이상 = 위험 |
| Recovery Time | 고점 회복 기간 | 3개월 이상 = 점검 |
| Current Drawdown | 현재 진행 중인 낙폭 | -15% 이상 = 알림 |
Circuit Breaker — 자동 정지 패턴
일일·주간·월간 손실 한도를 미리 정해 자동으로 멈추게 합니다.
class CircuitBreaker:
def __init__(self, daily=2, weekly=5, monthly=10):
"""단위는 % (자본 대비)"""
self.daily = daily
self.weekly = weekly
self.monthly = monthly
self.daily_pnl = 0
self.weekly_pnl = 0
self.monthly_pnl = 0
def record_trade(self, pnl_pct):
self.daily_pnl += pnl_pct
self.weekly_pnl += pnl_pct
self.monthly_pnl += pnl_pct
def should_halt(self):
if self.daily_pnl <= -self.daily:
return "DAILY_LIMIT"
if self.weekly_pnl <= -self.weekly:
return "WEEKLY_LIMIT"
if self.monthly_pnl <= -self.monthly:
return "MONTHLY_LIMIT"
return None
def reset_daily(self):
self.daily_pnl = 0
# reset_weekly, reset_monthly 도 스케줄러로 호출
💡 Circuit Breaker 발동 시 행동: 단순히 신규 매매만 막는 것이 아니라, 관리자(사장님)에게 텔레그램 알림 + 1시간 자동 일시정지가 표준입니다. 1시간 후 자동 재개 또는 수동 점검 후 재개를 선택할 수 있게.
Part 7 — 통합 RiskManager 클래스
모든 봇에 그대로 붙여 쓰는 통합 골격. 매매 전 모든 체크를 한 줄로.
import time
from dataclasses import dataclass, field
@dataclass
class Position:
symbol: str
entry: float
qty: float
stop: float
target: float
entry_time: float
class RiskManager:
"""모든 봇에 그대로 붙이는 통합 리스크 관리 골격"""
def __init__(self, capital, risk_per_trade=1, max_portfolio_risk=5,
daily_limit=2, weekly_limit=5):
self.capital = capital
self.risk_per_trade = risk_per_trade # %
self.max_portfolio_risk = max_portfolio_risk # %
self.positions = []
self.cb = CircuitBreaker(daily_limit, weekly_limit, weekly_limit * 2)
self.halted = False
# ─── 진입 전 체크 ────────────────────────────────
def can_enter(self, symbol, entry, stop):
if self.halted:
return False, "HALTED"
# 1. 같은 종목 중복 진입 금지
if any(p.symbol == symbol for p in self.positions):
return False, "ALREADY_OPEN"
# 2. 합산 리스크 한도
current_risk = sum(self.risk_per_trade for _ in self.positions)
if current_risk + self.risk_per_trade > self.max_portfolio_risk:
return False, "PORTFOLIO_LIMIT"
# 3. 손절폭이 0이면 진입 금지
if abs(entry - stop) < 0.0001:
return False, "INVALID_STOP"
return True, "OK"
# ─── 포지션 사이징 ──────────────────────────────
def calc_qty(self, entry, stop):
risk_amount = self.capital * (self.risk_per_trade / 100)
return risk_amount / abs(entry - stop)
# ─── 진입 ──────────────────────────────────────
def enter(self, symbol, entry, stop, target):
ok, reason = self.can_enter(symbol, entry, stop)
if not ok:
return None, reason
qty = self.calc_qty(entry, stop)
pos = Position(symbol, entry, qty, stop, target, time.time())
self.positions.append(pos)
return pos, "OK"
# ─── 매 봉 체크 (손절·익절) ──────────────────────
def check_exits(self, symbol, current_price):
for pos in self.positions[:]:
if pos.symbol != symbol:
continue
if current_price <= pos.stop:
self._close(pos, current_price, "STOP")
return "STOP"
if current_price >= pos.target:
self._close(pos, current_price, "TARGET")
return "TARGET"
return None
def _close(self, pos, exit_price, reason):
pnl = (exit_price - pos.entry) * pos.qty
pnl_pct = pnl / self.capital * 100
self.cb.record_trade(pnl_pct)
self.positions.remove(pos)
# 일일 한도 체크
halt_reason = self.cb.should_halt()
if halt_reason:
self.halted = True
print(f"[CIRCUIT BREAKER] {halt_reason} 발동 — 봇 일시정지")
# ─── 봇에서 사용 예 ────────────────────────────────
rm = RiskManager(capital=10_000_000, risk_per_trade=1,
max_portfolio_risk=5, daily_limit=2, weekly_limit=5)
# 진입 시
pos, reason = rm.enter("BTCUSDT", entry=60000, stop=58500, target=63000)
if pos:
print(f"매수: {pos.qty:.4f} @ {pos.entry}")
else:
print(f"진입 거부: {reason}")
# 매 봉 체크
rm.check_exits("BTCUSDT", current_price=63100) # TARGET 청산
✅ 이 골격의 강점: 어떤 봇이든 이 클래스만 붙이면 (1) 1% 룰 자동 적용 (2) 같은 종목 중복 금지 (3) 합산 리스크 한도 (4) 손절·익절 자동 (5) Circuit Breaker — 5가지가 즉시 작동합니다. 알고랩에서 제작하는 모든 봇에 이 골격이 기본 포함됩니다.
Part 8 — 16단계 리스크 관리 체크리스트
새 봇 만들 때마다 따라가는 체크리스트.
포지션 사이징 (4단계)
- ☐ 한 거래당 리스크 ≤ 자본의 1~2%
- ☐ 포지션 크기를 손절폭으로 역산 (고정 수량 ❌)
- ☐ ATR 또는 변동성 기반 손절폭 (단순 % 손절은 변동성 무시)
- ☐ 켈리 공식 적용 시 Half-Kelly 이하
손절·익절 (4단계)
- ☐ 모든 진입에 손절가 사전 정의 (진입 후 결정 ❌)
- ☐ 손절가가 코드에 박혀 있음 (수동 판단 ❌)
- ☐ R:R 1:2 이상 (손익비 명시)
- ☐ 부분 익절 패턴 적용 (1차 익절 후 본전 손절)
다종목·분산 (4단계)
- ☐ 같은 종목 중복 진입 금지
- ☐ 상관관계 0.7 이상 자산은 한 묶음으로 취급
- ☐ 모든 포지션 합산 리스크 ≤ 자본의 5%
- ☐ 자산군·전략 분산 (한 카테고리 집중 ❌)
드로다운·Circuit Breaker (4단계)
- ☐ 일일 손실 한도 -2% (Circuit Breaker 자동 발동)
- ☐ 주간 손실 한도 -5%
- ☐ 월간 손실 한도 -10%
- ☐ 한도 발동 시 텔레그램 알림 + 수동 점검 후 재개
자주 묻는 질문
Q. 리스크 관리는 왜 전략보다 중요한가요?
승률이 50%여도 손익비 2:1이면 장기 수익, 70%여도 1:3이면 망함. 또한 한 번의 -50% 손실은 +100% 수익이 필요한 비대칭. 큰 손실을 막는 게 큰 수익보다 100배 중요.
Q. 한 거래당 자본의 몇 %?
표준 1~2%. 1% 룰이면 연속 20번 손절 봐도 자본의 20%만 잃음.
Q. 켈리 공식 그대로 써도 되나요?
안 됩니다. 변동성이 너무 큼. Half-Kelly(1/2) 또는 Quarter-Kelly(1/4) 사용.
Q. 손절 규칙은 어떻게?
입문은 ATR 기반(k=2). 변동성 자동 반영. 강추세 종목은 트레일링 스탑.
Q. MDD -30%면 봇을 멈춰야 하나요?
회복 기간을 봐야 함. 1개월 안 회복은 정상, 3개월 이상이면 점검. Circuit Breaker로 자동 멈춤 설정 권장.
Q. 여러 봇 동시 운영 시 리스크는?
모든 봇 합산 리스크 ≤ 자본의 5%. 봇 개수 늘리는 게 분산이 아니라 곱셈일 수 있음. 상관관계 매트릭스 확인.
마무리
리스크 관리는 "잃지 않는 기술"입니다. 자동매매의 진짜 가치는 감정 없이 이 규칙들을 일관되게 실행하는 것입니다. 위 RiskManager 클래스는 알고랩이 모든 의뢰 봇에 기본 포함하는 골격이고, 한 번 익혀두면 어떤 전략에도 그대로 적용됩니다.
전략을 100개 시도하기보다, 이 8부 프레임워크를 1개 전략에 깊이 적용하는 것이 장기 수익으로 가는 정도입니다.
리스크 관리가 박혀 있는 봇으로
전략은 본인이 만들었거나 정해졌는데, 위 8부 리스크 관리를 직접 구현하기 부담스럽다면 알고랩이 RiskManager + Circuit Breaker + 텔레그램 알림이 통합된 봇을 1~3주 안에 제작해드립니다.
24시간 빠른 답변 가능합니다.