샤프 비율이란?
샤프 비율(Sharpe Ratio)은 위험 대비 초과 수익을 측정하는 지표로, 퀀트 트레이딩에서 전략 성과를 평가하는 가장 보편적인 기준입니다. 1966년 윌리엄 샤프가 제안한 이 지표는 단순하지만 강력합니다:
샤프 비율 = (전략 수익률 – 무위험 수익률) / 수익률의 표준편차
샤프 비율이 높을수록 동일한 위험을 감수하고 더 많은 수익을 얻는다는 뜻입니다. 일반적으로 1.0 이상이면 양호, 2.0 이상이면 우수, 3.0 이상이면 탁월한 전략으로 평가합니다.
파이썬으로 샤프 비율 계산
기본적인 샤프 비율 계산부터 시작합니다:
import numpy as np
import pandas as pd
import yfinance as yf
def sharpe_ratio(returns: pd.Series, risk_free_rate: float = 0.04,
periods: int = 252) -> float:
"""
연환산 샤프 비율 계산
Parameters:
returns: 일별 수익률 시리즈
risk_free_rate: 연간 무위험 이자율
periods: 연간 거래일 수
"""
excess_returns = returns - risk_free_rate / periods
if excess_returns.std() == 0:
return 0.0
return (excess_returns.mean() / excess_returns.std()) * np.sqrt(periods)
# 실제 데이터로 계산
spy = yf.download("SPY", start="2020-01-01", end="2025-12-31")
returns = spy["Close"].pct_change().dropna()
sr = sharpe_ratio(returns)
print(f"SPY 샤프 비율: {sr:.2f}")
print(f"연 수익률: {returns.mean() * 252:.1%}")
print(f"연 변동성: {returns.std() * np.sqrt(252):.1%}")
주의할 점은 연환산(Annualization)입니다. 일별 데이터의 샤프 비율을 연환산할 때 √252를 곱합니다. 시간 프레임에 따라 √52(주별), √12(월별)를 사용합니다.
샤프 비율의 함정: 왜 단독으로 쓰면 안 되는가
샤프 비율만으로 전략을 판단하면 치명적인 실수를 할 수 있습니다:
def demonstrate_sharpe_pitfalls():
"""샤프 비율의 한계를 보여주는 예시"""
np.random.seed(42)
days = 252 * 3 # 3년
# 전략 A: 꾸준한 수익 (샤프 2.0)
strategy_a = np.random.normal(0.0004, 0.005, days)
# 전략 B: 꾸준하지만 가끔 대폭락 (샤프 비슷)
strategy_b = np.random.normal(0.0005, 0.005, days)
# 분기마다 한 번씩 -5% 폭락
for i in range(0, days, 63):
strategy_b[i] = -0.05
# 전략 C: 옵션 매도형 (높은 샤프, 숨겨진 테일 리스크)
strategy_c = np.random.normal(0.001, 0.003, days)
# 1년에 한 번 -20% 폭락
for i in range(0, days, 252):
strategy_c[i] = -0.20
strategies = {
"전략 A (꾸준)": pd.Series(strategy_a),
"전략 B (간헐 폭락)": pd.Series(strategy_b),
"전략 C (테일 리스크)": pd.Series(strategy_c),
}
for name, rets in strategies.items():
sr = sharpe_ratio(rets)
max_dd = (
(1 + rets).cumprod() /
(1 + rets).cumprod().cummax() - 1
).min()
skew = rets.skew()
print(f"{name}: 샤프={sr:.2f}, "
f"MDD={max_dd:.1%}, 왜도={skew:.2f}")
demonstrate_sharpe_pitfalls()
전략 C는 샤프 비율이 높아 보이지만, 극단적 꼬리 위험(tail risk)을 숨기고 있습니다. 이것이 샤프 비율 단독 사용의 가장 큰 함정입니다. 정규분포를 가정하기 때문에 왜도(skewness)와 첨도(kurtosis)를 반영하지 못합니다.
소르티노 비율: 하방 위험만 측정
소르티노 비율(Sortino Ratio)은 샤프 비율의 한계를 보완합니다. 상방 변동성은 좋은 것이므로, 하방 변동성만 위험으로 간주합니다:
def sortino_ratio(returns: pd.Series, risk_free_rate: float = 0.04,
periods: int = 252) -> float:
"""소르티노 비율: 하방 위험 대비 초과 수익"""
excess = returns - risk_free_rate / periods
downside = excess[excess < 0]
downside_std = np.sqrt((downside ** 2).mean())
if downside_std == 0:
return 0.0
return (excess.mean() / downside_std) * np.sqrt(periods)
def calmar_ratio(returns: pd.Series, periods: int = 252) -> float:
"""칼마 비율: 최대 낙폭 대비 수익률"""
cumulative = (1 + returns).cumprod()
max_dd = (cumulative / cumulative.cummax() - 1).min()
annual_return = returns.mean() * periods
return annual_return / abs(max_dd) if max_dd != 0 else 0.0
def omega_ratio(returns: pd.Series, threshold: float = 0.0) -> float:
"""오메가 비율: 손익 비대칭성 측정"""
excess = returns - threshold
gains = excess[excess > 0].sum()
losses = abs(excess[excess < 0].sum())
return gains / losses if losses > 0 else float("inf")
# 비교 분석
print(f"샤프 비율: {sharpe_ratio(returns):.2f}")
print(f"소르티노: {sortino_ratio(returns):.2f}")
print(f"칼마 비율: {calmar_ratio(returns):.2f}")
print(f"오메가 비율: {omega_ratio(returns):.2f}")
각 지표의 특징을 정리하면:
- 샤프: 전체 변동성 기준. 가장 보편적이지만 꼬리 위험에 둔감
- 소르티노: 하방 변동성만 측정. 상승장 전략 평가에 유리
- 칼마: 최대 낙폭 대비 수익. 드로다운에 민감한 투자자에게 적합
- 오메가: 이익/손실 전체 분포 반영. 분포 가정 없이 측정
롤링 성과 분석 시스템
고정 기간 성과만 보면 시점에 따라 결과가 크게 달라집니다. 롤링 분석으로 전략의 안정성을 평가합니다:
class PerformanceAnalyzer:
"""종합 성과 분석 시스템"""
def __init__(self, returns: pd.Series, benchmark: pd.Series = None,
risk_free_rate: float = 0.04):
self.returns = returns
self.benchmark = benchmark
self.rf = risk_free_rate
def rolling_sharpe(self, window: int = 126) -> pd.Series:
"""롤링 샤프 비율 (기본 6개월)"""
excess = self.returns - self.rf / 252
rolling_mean = excess.rolling(window).mean()
rolling_std = excess.rolling(window).std()
return (rolling_mean / rolling_std) * np.sqrt(252)
def drawdown_analysis(self) -> dict:
"""드로다운 상세 분석"""
cumulative = (1 + self.returns).cumprod()
running_max = cumulative.cummax()
drawdown = cumulative / running_max - 1
# 최대 드로다운
max_dd = drawdown.min()
max_dd_end = drawdown.idxmin()
# 최대 드로다운 시작점
peak_idx = cumulative[:max_dd_end].idxmax()
# 회복 기간
recovery = cumulative[max_dd_end:]
recovered = recovery[recovery >= cumulative[peak_idx]]
recovery_date = recovered.index[0] if len(recovered) > 0 else None
recovery_days = (
(recovery_date - max_dd_end).days if recovery_date else None
)
return {
"max_drawdown": f"{max_dd:.1%}",
"peak_date": str(peak_idx.date()),
"trough_date": str(max_dd_end.date()),
"recovery_days": recovery_days,
"avg_drawdown": f"{drawdown.mean():.1%}",
}
def full_report(self) -> dict:
"""종합 성과 리포트"""
r = self.returns
annual_ret = r.mean() * 252
annual_vol = r.std() * np.sqrt(252)
return {
"annual_return": f"{annual_ret:.1%}",
"annual_volatility": f"{annual_vol:.1%}",
"sharpe_ratio": f"{sharpe_ratio(r, self.rf):.2f}",
"sortino_ratio": f"{sortino_ratio(r, self.rf):.2f}",
"calmar_ratio": f"{calmar_ratio(r):.2f}",
"omega_ratio": f"{omega_ratio(r):.2f}",
"skewness": f"{r.skew():.2f}",
"kurtosis": f"{r.kurtosis():.2f}",
"win_rate": f"{(r > 0).mean():.1%}",
"best_day": f"{r.max():.1%}",
"worst_day": f"{r.min():.1%}",
**self.drawdown_analysis(),
}
# 분석 실행
analyzer = PerformanceAnalyzer(returns)
report = analyzer.full_report()
for k, v in report.items():
print(f" {k}: {v}")
롤링 샤프 비율이 지속적으로 1.0 이상을 유지하는 전략이 진정한 알파를 가진 전략입니다. 특정 시기에만 높고 나머지는 0 근처라면, 그 전략은 시장 환경에 과적합된 것입니다.
벤치마크 대비 성과: 정보 비율
정보 비율(Information Ratio)은 벤치마크 대비 초과 수익의 안정성을 측정합니다:
def information_ratio(strategy_returns: pd.Series,
benchmark_returns: pd.Series,
periods: int = 252) -> float:
"""정보 비율: 벤치마크 대비 초과 수익의 샤프 비율"""
active_returns = strategy_returns - benchmark_returns
tracking_error = active_returns.std() * np.sqrt(periods)
active_return = active_returns.mean() * periods
return active_return / tracking_error if tracking_error > 0 else 0.0
def alpha_beta(strategy_returns: pd.Series,
benchmark_returns: pd.Series) -> dict:
"""CAPM 알파와 베타 계산"""
aligned = pd.concat(
[strategy_returns, benchmark_returns], axis=1
).dropna()
aligned.columns = ["strategy", "benchmark"]
cov = np.cov(aligned["strategy"], aligned["benchmark"])
beta = cov[0, 1] / cov[1, 1]
alpha = (
aligned["strategy"].mean() -
beta * aligned["benchmark"].mean()
) * 252
return {
"alpha": f"{alpha:.1%}",
"beta": f"{beta:.2f}",
}
# 벤치마크(SPY) 대비 모멘텀 전략 분석 예시
# 단순 모멘텀: 20일 수익률 양수면 보유, 음수면 현금
momentum_signal = returns.rolling(20).mean() > 0
momentum_returns = returns * momentum_signal.shift(1)
ir = information_ratio(momentum_returns, returns)
ab = alpha_beta(momentum_returns, returns)
print(f"정보 비율: {ir:.2f}")
print(f"알파: {ab['alpha']}, 베타: {ab['beta']}")
정보 비율이 0.5 이상이면 괜찮은 액티브 전략, 1.0 이상이면 매우 우수합니다. 정보 비율은 백테스트 검증 시 과적합을 판별하는 데도 유용합니다.
통계적 유의성 검정
샤프 비율이 높아도 통계적으로 유의하지 않으면 의미가 없습니다:
def sharpe_significance(returns: pd.Series,
risk_free_rate: float = 0.04,
n_bootstrap: int = 10000) -> dict:
"""부트스트랩으로 샤프 비율의 통계적 유의성 검정"""
observed_sr = sharpe_ratio(returns, risk_free_rate)
# 부트스트랩 샘플링
bootstrap_sharpes = []
n = len(returns)
for _ in range(n_bootstrap):
sample = returns.sample(n, replace=True).values
sr = (
(sample.mean() - risk_free_rate / 252)
/ sample.std() * np.sqrt(252)
)
bootstrap_sharpes.append(sr)
bootstrap_sharpes = np.array(bootstrap_sharpes)
ci_lower = np.percentile(bootstrap_sharpes, 2.5)
ci_upper = np.percentile(bootstrap_sharpes, 97.5)
p_value = (bootstrap_sharpes <= 0).mean()
return {
"observed_sharpe": round(observed_sr, 2),
"ci_95": (round(ci_lower, 2), round(ci_upper, 2)),
"p_value": round(p_value, 4),
"significant": p_value < 0.05,
}
result = sharpe_significance(returns)
print(f"샤프 비율: {result['observed_sharpe']}")
print(f"95% 신뢰구간: {result['ci_95']}")
print(f"p-value: {result['p_value']}")
print(f"유의미: {'Yes' if result['significant'] else 'No'}")
샤프 비율 1.0이어도 데이터가 1년뿐이라면 95% 신뢰구간이 0을 포함할 수 있습니다. 최소 3년 이상의 데이터로 검증해야 신뢰할 수 있는 결과를 얻습니다.
실전 성과 분석 체크리스트
- 다중 지표: 샤프만 보지 말고 소르티노, 칼마, 오메가를 함께 확인
- 롤링 분석: 6개월 롤링 샤프가 안정적인지 확인
- 드로다운: MDD와 회복 기간을 반드시 점검
- 분포 특성: 왜도(음수면 위험)와 첨도(높으면 꼬리 위험)를 확인
- 통계적 유의성: 부트스트랩 p-value < 0.05인지 검증
- 벤치마크 대비: 알파가 양수이고 정보 비율이 유의미한지 확인
- 슬리피지·비용 반영: 비용 차감 후에도 샤프 > 1.0인지 확인
결론
샤프 비율은 퀀트 전략 평가의 출발점이지 종착점이 아닙니다. 샤프 비율 하나로 전략을 판단하면 숨겨진 꼬리 위험, 드로다운 패턴, 시장 환경 의존성을 놓칩니다. 소르티노·칼마·오메가를 함께 분석하고, 롤링 분석으로 안정성을 확인하며, 부트스트랩으로 통계적 유의성까지 검증해야 진짜 알파를 가진 전략인지 판별할 수 있습니다. 성과 분석에 시간을 투자하는 것이 새로운 전략을 개발하는 것만큼 중요합니다.