- main.py: add IBKR-style tiered fee schedule (fee_base + fee_per_share), PIT universe support, and open-to-close execution improvements - metrics.py: add raw_summary helper for JSON-safe metric export - Misc strategy fixes: deprecation warnings, NaN handling Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
88 lines
2.9 KiB
Python
88 lines
2.9 KiB
Python
import numpy as np
|
|
import pandas as pd
|
|
|
|
|
|
def total_return(equity: pd.Series) -> float:
|
|
return equity.iloc[-1] / equity.iloc[0] - 1
|
|
|
|
|
|
def annualized_return(equity: pd.Series, periods: int = 252) -> float:
|
|
n = len(equity)
|
|
return (1 + total_return(equity)) ** (periods / n) - 1
|
|
|
|
|
|
def annualized_volatility(returns: pd.Series, periods: int = 252) -> float:
|
|
return returns.std() * np.sqrt(periods)
|
|
|
|
|
|
def sharpe_ratio(returns: pd.Series, risk_free_rate: float = 0.0, periods: int = 252) -> float:
|
|
excess = returns - risk_free_rate / periods
|
|
std = excess.std()
|
|
if std == 0:
|
|
return 0.0
|
|
return (excess.mean() / std) * np.sqrt(periods)
|
|
|
|
|
|
def sortino_ratio(returns: pd.Series, risk_free_rate: float = 0.0, periods: int = 252) -> float:
|
|
excess = returns - risk_free_rate / periods
|
|
downside_std = excess[excess < 0].std()
|
|
if downside_std == 0:
|
|
return 0.0
|
|
return (excess.mean() / downside_std) * np.sqrt(periods)
|
|
|
|
|
|
def max_drawdown(equity: pd.Series) -> float:
|
|
rolling_peak = equity.cummax()
|
|
drawdown = (equity - rolling_peak) / rolling_peak
|
|
return drawdown.min()
|
|
|
|
|
|
def calmar_ratio(equity: pd.Series, periods: int = 252) -> float:
|
|
ann_ret = annualized_return(equity, periods)
|
|
mdd = abs(max_drawdown(equity))
|
|
if mdd == 0:
|
|
return np.inf
|
|
return ann_ret / mdd
|
|
|
|
|
|
def win_rate(returns: pd.Series) -> float:
|
|
active = returns[returns != 0]
|
|
if len(active) == 0:
|
|
return 0.0
|
|
return (active > 0).sum() / len(active)
|
|
|
|
|
|
def raw_summary(equity: pd.Series) -> dict:
|
|
"""Return numeric metrics suitable for JSON serialization."""
|
|
returns = equity.pct_change().dropna()
|
|
return {
|
|
"totalReturn": float(total_return(equity)),
|
|
"annualizedReturn": float(annualized_return(equity)),
|
|
"annualizedVolatility": float(annualized_volatility(returns)),
|
|
"sharpeRatio": float(sharpe_ratio(returns)),
|
|
"sortinoRatio": float(sortino_ratio(returns)),
|
|
"maxDrawdown": float(max_drawdown(equity)),
|
|
"calmarRatio": float(calmar_ratio(equity)),
|
|
"winRate": float(win_rate(returns)),
|
|
}
|
|
|
|
|
|
def summary(equity: pd.Series, name: str = "Strategy") -> dict:
|
|
returns = equity.pct_change().dropna()
|
|
metrics = {
|
|
"Total Return": f"{total_return(equity):.2%}",
|
|
"Annualized Return": f"{annualized_return(equity):.2%}",
|
|
"Annualized Volatility": f"{annualized_volatility(returns):.2%}",
|
|
"Sharpe Ratio": f"{sharpe_ratio(returns):.2f}",
|
|
"Sortino Ratio": f"{sortino_ratio(returns):.2f}",
|
|
"Max Drawdown": f"{max_drawdown(equity):.2%}",
|
|
"Calmar Ratio": f"{calmar_ratio(equity):.2f}",
|
|
"Win Rate": f"{win_rate(returns):.2%}",
|
|
}
|
|
print(f"\n{'='*45}")
|
|
print(f" {name}")
|
|
print(f"{'='*45}")
|
|
for k, v in metrics.items():
|
|
print(f" {k:<26} {v:>10}")
|
|
return metrics
|