본문 바로가기
Python

[주식 분석 프로젝트] 7편 – 전략 성과 평가: 수익률 vs 리스크

by ramzee 2025. 4. 28.

📌 이번 실습 목표

  • 단순 수익률뿐만 아니라 리스크 조정 수익률 분석
  • 샤프 지수, 최대 낙폭(Max Drawdown), 승률 계산
  • 전략의 안정성과 지속 가능성 평가

🔍 왜 수익률만으로는 부족한가?

수익률이 높아 보여도, 그 과정에서 극단적인 손실이나 높은 변동성이 있었다면 실전 투자에는 위험합니다.
따라서 전략을 평가할 때는 아래와 같은 성과 지표를 함께 확인해야 합니다:

  • 샤프 지수 (Sharpe Ratio): 수익률 / 리스크 → 리스크 대비 수익
  • 최대 낙폭 (Max Drawdown): 최고점 대비 최대 하락폭 → 심리적 리스크
  • 승률: 전체 거래 중 수익을 낸 비율

🧮 전략 실행 후 평가 지표 계산

import numpy as np

def evaluate_strategy(df):
    df = df.copy()
    returns = df['Strategy Return'].dropna()

    # 샤프 지수 (무위험 수익률 0으로 가정)
    sharpe_ratio = (returns.mean() / returns.std()) * np.sqrt(252)

    # 최대 낙폭 계산
    cum_returns = (1 + returns).cumprod()
    peak = cum_returns.cummax()
    drawdown = (cum_returns - peak) / peak
    max_drawdown = drawdown.min()

    # 승률 계산
    win_rate = (returns > 0).sum() / returns.count()

    return {
        'Final Return': cum_returns.iloc[-1],
        'Sharpe Ratio': sharpe_ratio,
        'Max Drawdown': max_drawdown,
        'Win Rate': win_rate
    }

✅ 샤프 지수 해설

샤프 지수는 수익률을 리스크(표준편차)로 나눈 값입니다.

  • 1.0 이상 → 양호한 전략
  • 2.0 이상 → 우수 전략
  • 3.0 이상 → 탁월한 전략

낮은 리스크로 높은 수익을 냈다는 의미이므로, 실전 투자에서 가장 많이 쓰이는 평가 지표입니다.


📉 최대 낙폭 (Max Drawdown) 해설

최대 낙폭은 수익률 곡선이 최고점에서 얼마나 크게 떨어졌는가를 측정합니다.
예: 누적 수익률이 1.4에서 0.8로 떨어졌다면 최대 낙폭은 약 -43%
낙폭이 크면 투자자가 심리적으로 이탈하거나 중도 청산할 가능성이 큽니다.


📊 전략 결과 출력 예시


# 전략 실행
def run_strategy_with_params(df, rsi_buy=30, rsi_sell=70, std_n=2.0):
    df = df.copy()

    # ✅ MultiIndex 처리
    if isinstance(df.columns, pd.MultiIndex):
        df.columns = df.columns.get_level_values(0)

    # ✅ 'Close'가 없으면 'Adj Close'로 대체
    if 'Close' not in df.columns and 'Adj Close' in df.columns:
        df['Close'] = df['Adj Close']

    df['MA20'] = df['Close'].rolling(window=20).mean()
    df['STD20'] = df['Close'].rolling(window=20).std()
    df['Upper'] = df['MA20'] + (std_n * df['STD20'])
    df['Lower'] = df['MA20'] - (std_n * df['STD20'])

    delta = df['Close'].diff()
    gain = delta.where(delta > 0, 0)
    loss = -delta.where(delta < 0, 0)
    avg_gain = gain.rolling(window=14).mean()
    avg_loss = loss.rolling(window=14).mean()

    epsilon = 1e-10
    avg_loss = avg_loss.replace(0, epsilon)

    rs = avg_gain / avg_loss
    df['RSI'] = 100 - (100 / (1 + rs))

    buy_signal = (df['RSI'] < rsi_buy) & (df['Close'] < df['Lower'])
    sell_signal = (df['RSI'] > rsi_sell) | (df['Close'] > df['Upper'])

    df['Position'] = 0
    position = 0
    for i in range(1, len(df)):
        if buy_signal.iloc[i] and position == 0:
            df.at[df.index[i], 'Position'] = 1
            position = 1
        elif sell_signal.iloc[i] and position == 1:
            df.at[df.index[i], 'Position'] = 0
            position = 0
        else:
            df.at[df.index[i], 'Position'] = df.at[df.index[i-1], 'Position']

    df['Daily Return'] = df['Close'].pct_change()
    df['Strategy Return'] = df['Daily Return'] * df['Position'].shift(1)
    df['Cumulative Return'] = (1 + df['Strategy Return']).cumprod()

    return df
    
df = yf.download("NVDA", period="6mo")
df = run_strategy_with_params(df, rsi_buy=30, rsi_sell=70, std_n=2.0)

eval_result = evaluate_strategy(df)
print(eval_result)
{
'Final Return': 1.0040717539588246, 
'Sharpe Ratio': 0.21872294101513803, 
'Max Drawdown': -0.2248705598923468, 
'Win Rate': 0.13934426229508196
}

✅ 오늘의 요약

  • 수익률 외에도 샤프 지수, 낙폭, 승률은 전략 검증의 핵심 지표
  • 샤프 지수가 높고, 낙폭이 낮고, 승률이 높을수록 안정적인 전략
  • 숫자로 전략의 투자 적합성을 정량적으로 판단 가능