왜 수익률만으로는 부족한가
자동매매 봇을 운영하면서 “연 수익률 50%”라는 결과만 보고 전략을 평가하는 것은 위험하다. 같은 50% 수익이라도 MDD 10%로 달성한 전략과 MDD 60%를 견디고 달성한 전략은 완전히 다르다. 리스크 대비 수익을 정확히 측정하려면 여러 성과 지표를 함께 봐야 한다.
이 글에서는 자동매매 전략 평가에 필수적인 핵심 지표 6가지를 파이썬 구현 코드와 함께 정리한다.
1. 샤프 비율 (Sharpe Ratio)
가장 널리 쓰이는 리스크 조정 수익률 지표다. 무위험 수익률 대비 초과 수익을 변동성으로 나눈 값이다.
import numpy as np
def sharpe_ratio(returns, risk_free_rate=0.035, periods=252):
"""연율화된 샤프 비율 계산
returns: 일간 수익률 배열
risk_free_rate: 연 무위험 수익률 (기본 3.5%)
periods: 연간 거래일 수
"""
excess = returns - risk_free_rate / periods
return np.mean(excess) / np.std(excess) * np.sqrt(periods)
해석 기준:
- 1.0 미만 — 리스크 대비 수익이 부족
- 1.0~2.0 — 양호한 전략
- 2.0 이상 — 우수한 전략
- 3.0 이상 — 매우 드문 수준 (과적합 의심 필요)
샤프 비율의 한계는 상승 변동성과 하락 변동성을 동일하게 취급한다는 점이다. 큰 수익이 발생해도 변동성이 커지므로 샤프가 낮아질 수 있다. 이 문제를 보완하는 것이 소르티노 비율이다.
2. 소르티노 비율 (Sortino Ratio)
소르티노 비율은 하방 변동성(downside deviation)만 리스크로 간주한다. 수익이 큰 날의 변동성은 패널티를 주지 않으므로, 비대칭 수익 분포를 가진 전략에 더 적합하다.
def sortino_ratio(returns, risk_free_rate=0.035, periods=252):
"""소르티노 비율: 하방 변동성만 고려"""
excess = returns - risk_free_rate / periods
downside = returns[returns < 0]
downside_std = np.std(downside) if len(downside) > 0 else 1e-6
return np.mean(excess) / downside_std * np.sqrt(periods)
변동성 돌파 전략처럼 승률은 낮지만 수익 날 때 크게 버는 전략은, 샤프 비율보다 소르티노 비율이 더 높게 나오는 경향이 있다. 두 지표의 차이가 클수록 수익 분포가 양의 방향으로 편향되어 있다는 의미다.
3. 칼마 비율 (Calmar Ratio)
칼마 비율은 연 수익률을 최대 낙폭(MDD)으로 나눈 값이다. “최악의 드로다운 대비 얼마나 벌었는가”를 직관적으로 보여준다.
def max_drawdown(equity_curve):
"""최대 낙폭 계산"""
peak = equity_curve[0]
mdd = 0
for value in equity_curve:
if value > peak:
peak = value
dd = (peak - value) / peak
if dd > mdd:
mdd = dd
return mdd
def calmar_ratio(returns, equity_curve, periods=252):
"""칼마 비율: 연 수익률 / MDD"""
annual_return = np.mean(returns) * periods
mdd = max_drawdown(equity_curve)
return annual_return / mdd if mdd > 0 else 0
| 칼마 비율 | 해석 | 예시 |
|---|---|---|
| 0.5 미만 | MDD 대비 수익 부족 | 수익 20%, MDD 50% |
| 1.0~2.0 | 양호 | 수익 30%, MDD 20% |
| 3.0 이상 | 우수 | 수익 30%, MDD 10% |
칼마 비율은 특히 실전 운용에서 심리적 한계를 가늠하는 데 유용하다. MDD 40%를 경험하면 대부분의 투자자가 전략을 포기한다. 칼마가 높을수록 실전에서 전략을 유지하기 쉽다.
4. 프로핏 팩터 (Profit Factor)
프로핏 팩터는 총 수익 / 총 손실의 비율이다. 매매 단위로 전략의 효율성을 측정한다.
def profit_factor(trade_returns):
"""프로핏 팩터: 총 이익 / 총 손실"""
gains = sum(r for r in trade_returns if r > 0)
losses = abs(sum(r for r in trade_returns if r < 0))
return gains / losses if losses > 0 else float('inf')
def avg_win_loss_ratio(trade_returns):
"""평균 수익/손실 비율"""
wins = [r for r in trade_returns if r > 0]
losses = [abs(r) for r in trade_returns if r < 0]
avg_win = np.mean(wins) if wins else 0
avg_loss = np.mean(losses) if losses else 1e-6
return avg_win / avg_loss
- 1.0 미만 — 손실이 수익보다 큼 (손실 전략)
- 1.0~1.5 — 수수료와 슬리피지 반영 시 수익 불확실
- 1.5~2.0 — 실전에서도 수익 가능한 수준
- 2.0 이상 — 강한 전략
프로핏 팩터는 계좌 생존 규칙과 직결된다. 프로핏 팩터가 1.5 이상이면 연속 손실이 와도 장기적으로 복구할 수 있다는 통계적 근거가 된다.
5. 승률과 손익비의 관계
승률(win rate)과 손익비(reward-to-risk ratio)는 반비례 관계에 있다. 높은 승률을 추구하면 손익비가 낮아지고, 큰 손익비를 추구하면 승률이 낮아진다.
def expectancy(win_rate, avg_win, avg_loss):
"""매매 기대값 계산"""
return (win_rate * avg_win) - ((1 - win_rate) * avg_loss)
def min_win_rate(risk_reward_ratio):
"""손익비 대비 손익분기 승률"""
return 1 / (1 + risk_reward_ratio)
# 손익비별 최소 필요 승률
for rr in [1.0, 1.5, 2.0, 3.0]:
print(f"손익비 {rr}:1 → 최소 승률 {min_win_rate(rr)*100:.1f}%")
# 손익비 1.0:1 → 최소 승률 50.0%
# 손익비 1.5:1 → 최소 승률 40.0%
# 손익비 2.0:1 → 최소 승률 33.3%
# 손익비 3.0:1 → 최소 승률 25.0%
추세추종 전략은 보통 승률 35~45%, 손익비 2:1~3:1이다. 평균회귀 전략은 승률 60~70%, 손익비 0.5:1~1:1이 일반적이다. 자신의 전략 유형에 맞는 승률-손익비 조합인지 확인해야 한다.
6. 최대 연속 손실 (Max Consecutive Losses)
실전에서 가장 중요하지만 간과되는 지표다. 아무리 기대값이 양수인 전략이라도 연속 10번 손절을 맞으면 대부분의 사람이 전략을 의심하고 포기한다.
def max_consecutive_losses(trade_returns):
"""최대 연속 손실 횟수"""
max_streak = 0
current = 0
for r in trade_returns:
if r < 0:
current += 1
max_streak = max(max_streak, current)
else:
current = 0
return max_streak
def expected_max_streak(win_rate, num_trades):
"""통계적으로 예상되는 최대 연속 손실"""
import math
loss_rate = 1 - win_rate
return math.log(num_trades) / abs(math.log(loss_rate))
예를 들어 승률 50%인 전략으로 연 250번 매매하면, 통계적으로 최대 약 8연패가 예상된다. 승률 40%면 약 12연패까지 올 수 있다. 이 수치를 미리 알고 있어야 실제로 연패가 왔을 때 시스템을 신뢰하고 유지할 수 있다.
종합 성과 보고서 만들기
위 지표들을 하나의 함수로 묶어 종합 보고서를 생성할 수 있다:
def performance_report(daily_returns, trade_returns, equity_curve):
"""종합 성과 보고서"""
report = {
'연 수익률': f"{np.mean(daily_returns) * 252 * 100:.1f}%",
'샤프 비율': f"{sharpe_ratio(daily_returns):.2f}",
'소르티노 비율': f"{sortino_ratio(daily_returns):.2f}",
'칼마 비율': f"{calmar_ratio(daily_returns, equity_curve):.2f}",
'MDD': f"{max_drawdown(equity_curve) * 100:.1f}%",
'프로핏 팩터': f"{profit_factor(trade_returns):.2f}",
'승률': f"{sum(1 for r in trade_returns if r > 0) / len(trade_returns) * 100:.1f}%",
'총 매매 횟수': len(trade_returns),
'최대 연속 손실': max_consecutive_losses(trade_returns),
}
print("=" * 40)
print(" 전략 성과 보고서")
print("=" * 40)
for k, v in report.items():
print(f" {k: <12} {v}")
return report
지표 해석 시 주의사항
- 단일 지표로 판단하지 말 것 — 샤프가 높아도 MDD가 크면 실전에서 유지 불가
- 샘플 수가 충분해야 의미 있다 — 매매 횟수 30회 미만이면 통계적 신뢰도 낮음
- 백테스트 지표는 할인해서 볼 것 — 실전은 슬리피지, 수수료, API 지연으로 성과가 10~30% 하락
- 시장 국면별로 분리 분석 — 상승장/하락장/횡보장 각각의 성과를 봐야 전략의 약점이 드러남
- 과적합 경고 신호 — 샤프 3.0+, 승률 80%+, 프로핏 팩터 4.0+ 같은 극단적 수치는 과적합 가능성이 높다
정리
자동매매 전략을 제대로 평가하려면 수익률 하나가 아닌 다각도 지표가 필요하다. 샤프 비율로 전반적인 리스크 효율을, 소르티노로 하방 리스크를, 칼마로 최악 시나리오 대비 수익을, 프로핏 팩터로 매매 효율을 측정한다. 이 지표들을 자동화하여 매일 모니터링하는 시스템을 구축하면, 전략의 성능 저하를 조기에 감지하고 대응할 수 있다.