191 lines
4.5 KiB
Python
191 lines
4.5 KiB
Python
"""Performance metrics calculation for backtesting."""
|
|
from typing import List
|
|
from decimal import Decimal
|
|
import math
|
|
|
|
|
|
def calculate_total_return(initial_value: Decimal, final_value: Decimal) -> float:
|
|
"""
|
|
총 수익률 계산.
|
|
|
|
Args:
|
|
initial_value: 초기 자산
|
|
final_value: 최종 자산
|
|
|
|
Returns:
|
|
총 수익률 (%)
|
|
"""
|
|
if initial_value == 0:
|
|
return 0.0
|
|
return float((final_value - initial_value) / initial_value * 100)
|
|
|
|
|
|
def calculate_cagr(initial_value: Decimal, final_value: Decimal, years: float) -> float:
|
|
"""
|
|
연평균 복리 수익률(CAGR) 계산.
|
|
|
|
Args:
|
|
initial_value: 초기 자산
|
|
final_value: 최종 자산
|
|
years: 투자 기간 (년)
|
|
|
|
Returns:
|
|
CAGR (%)
|
|
"""
|
|
if initial_value == 0 or years == 0:
|
|
return 0.0
|
|
return float((pow(float(final_value / initial_value), 1 / years) - 1) * 100)
|
|
|
|
|
|
def calculate_max_drawdown(equity_curve: List[Decimal]) -> float:
|
|
"""
|
|
최대 낙폭(MDD) 계산.
|
|
|
|
Args:
|
|
equity_curve: 자산 곡선 리스트
|
|
|
|
Returns:
|
|
MDD (%)
|
|
"""
|
|
if not equity_curve:
|
|
return 0.0
|
|
|
|
max_dd = 0.0
|
|
peak = equity_curve[0]
|
|
|
|
for value in equity_curve:
|
|
if value > peak:
|
|
peak = value
|
|
|
|
drawdown = float((peak - value) / peak * 100) if peak > 0 else 0.0
|
|
max_dd = max(max_dd, drawdown)
|
|
|
|
return max_dd
|
|
|
|
|
|
def calculate_sharpe_ratio(returns: List[float], risk_free_rate: float = 0.0) -> float:
|
|
"""
|
|
샤프 비율 계산 (연율화).
|
|
|
|
Args:
|
|
returns: 일별 수익률 리스트 (%)
|
|
risk_free_rate: 무위험 이자율 (기본 0%)
|
|
|
|
Returns:
|
|
샤프 비율
|
|
"""
|
|
if not returns or len(returns) < 2:
|
|
return 0.0
|
|
|
|
# 평균 수익률
|
|
mean_return = sum(returns) / len(returns)
|
|
|
|
# 표준편차
|
|
variance = sum((r - mean_return) ** 2 for r in returns) / (len(returns) - 1)
|
|
std_dev = math.sqrt(variance)
|
|
|
|
if std_dev == 0:
|
|
return 0.0
|
|
|
|
# 샤프 비율 (연율화: sqrt(252) - 주식 시장 거래일 수)
|
|
sharpe = (mean_return - risk_free_rate) / std_dev * math.sqrt(252)
|
|
|
|
return sharpe
|
|
|
|
|
|
def calculate_sortino_ratio(returns: List[float], risk_free_rate: float = 0.0) -> float:
|
|
"""
|
|
소르티노 비율 계산 (연율화).
|
|
|
|
Args:
|
|
returns: 일별 수익률 리스트 (%)
|
|
risk_free_rate: 무위험 이자율 (기본 0%)
|
|
|
|
Returns:
|
|
소르티노 비율
|
|
"""
|
|
if not returns or len(returns) < 2:
|
|
return 0.0
|
|
|
|
# 평균 수익률
|
|
mean_return = sum(returns) / len(returns)
|
|
|
|
# 하방 편차 (Downside Deviation)
|
|
downside_returns = [r for r in returns if r < risk_free_rate]
|
|
if not downside_returns:
|
|
return float('inf') # 손실이 없는 경우
|
|
|
|
downside_variance = sum((r - risk_free_rate) ** 2 for r in downside_returns) / len(downside_returns)
|
|
downside_std = math.sqrt(downside_variance)
|
|
|
|
if downside_std == 0:
|
|
return 0.0
|
|
|
|
# 소르티노 비율 (연율화)
|
|
sortino = (mean_return - risk_free_rate) / downside_std * math.sqrt(252)
|
|
|
|
return sortino
|
|
|
|
|
|
def calculate_win_rate(trades: List[dict]) -> float:
|
|
"""
|
|
승률 계산.
|
|
|
|
Args:
|
|
trades: 거래 리스트 (각 거래는 pnl 필드 포함)
|
|
|
|
Returns:
|
|
승률 (%)
|
|
"""
|
|
if not trades:
|
|
return 0.0
|
|
|
|
winning_trades = sum(1 for trade in trades if trade.get('pnl', 0) > 0)
|
|
total_trades = len(trades)
|
|
|
|
return (winning_trades / total_trades * 100) if total_trades > 0 else 0.0
|
|
|
|
|
|
def calculate_volatility(returns: List[float]) -> float:
|
|
"""
|
|
변동성 계산 (연율화).
|
|
|
|
Args:
|
|
returns: 일별 수익률 리스트 (%)
|
|
|
|
Returns:
|
|
연율화 변동성 (%)
|
|
"""
|
|
if not returns or len(returns) < 2:
|
|
return 0.0
|
|
|
|
mean_return = sum(returns) / len(returns)
|
|
variance = sum((r - mean_return) ** 2 for r in returns) / (len(returns) - 1)
|
|
std_dev = math.sqrt(variance)
|
|
|
|
# 연율화
|
|
annualized_volatility = std_dev * math.sqrt(252)
|
|
|
|
return annualized_volatility
|
|
|
|
|
|
def calculate_calmar_ratio(total_return_pct: float, max_drawdown_pct: float, years: float) -> float:
|
|
"""
|
|
칼마 비율 계산.
|
|
|
|
Args:
|
|
total_return_pct: 총 수익률 (%)
|
|
max_drawdown_pct: MDD (%)
|
|
years: 투자 기간 (년)
|
|
|
|
Returns:
|
|
칼마 비율
|
|
"""
|
|
if max_drawdown_pct == 0 or years == 0:
|
|
return 0.0
|
|
|
|
cagr = (math.pow(1 + total_return_pct / 100, 1 / years) - 1) * 100
|
|
calmar = cagr / max_drawdown_pct
|
|
|
|
return calmar
|