"""绩效指标计算。"""
from __future__ import annotations
import numpy as np
import pandas as pd
TRADING_DAYS = 252
def max_drawdown(equity: pd.Series) -> float:
peak = equity.cummax()
return float((equity / peak - 1.0).min())
def monthly_returns(returns: pd.Series) -> pd.Series:
"""按月复利合成收益率。"""
eq = (1 + returns).cumprod()
me = eq.resample("ME").last()
first = eq.resample("ME").first().shift(1)
start_val = pd.Series(1.0, index=me.index)
first = first.fillna(1.0)
return (me / first.fillna(1.0) - 1.0).dropna()
def summarize(returns: pd.Series, equity: pd.Series, benchmark_rets: Optional[pd.Series] = None,
rf: float = 0.02, turnover_hist=None, ic_series=None) -> dict:
n = len(returns)
total = float(equity.iloc[-1] / equity.iloc[0] - 1.0)
years = n / TRADING_DAYS
cagr = float((equity.iloc[-1] / equity.iloc[0]) ** (1 / max(years, 1e-9)) - 1.0)
vol = float(returns.std(ddof=1) * np.sqrt(TRADING_DAYS))
sharpe = float((returns.mean() * TRADING_DAYS - rf) / vol) if vol > 1e-9 else np.nan
mdd = max_drawdown(equity)
calmar = cagr / abs(mdd) if mdd < 0 else np.nan
win = float((returns > 0).mean())
out = {
"total_return": total,
"cagr": cagr,
"ann_vol": vol,
"sharpe": sharpe,
"max_drawdown": mdd,
"calmar": calmar,
"daily_win_rate": win,
"n_days": n,
}
if turnover_hist:
out["avg_turnover_per_rebalance"] = float(np.mean(turnover_hist))
if ic_series:
ics = pd.Series(ic_series)
out["ic_mean"] = float(ics.mean())
out["ic_ir"] = float(ics.mean() / ics.std(ddof=1)) if ics.std(ddof=1) > 1e-9 else np.nan
out["ic_positive_ratio"] = float((ics > 0).mean())
out["n_ic_obs"] = int(len(ics))
if benchmark_rets is not None and len(benchmark_rets):
common = returns.index.intersection(benchmark_rets.index)
ex = returns[common] - benchmark_rets[common]
out["excess_cagr_vs_bench"] = float(np.mean(ex) * TRADING_DAYS)
bvol = float(benchmark_rets[common].std(ddof=1) * np.sqrt(TRADING_DAYS))
out["tracking_error"] = float(ex.std(ddof=1) * np.sqrt(TRADING_DAYS)) if len(ex) > 2 else np.nan
if bvol > 1e-9:
out["bench_sharpe"] = float((benchmark_rets[common].mean() * TRADING_DAYS - rf) / bvol)
return out
def yearly_table(returns: pd.Series) -> pd.Series:
eq = (1 + returns).cumprod()
ye = eq.resample("YE").last()
first = eq.resample("YE").first().shift(1).fillna(1.0)
return (ye / first - 1.0).dropna()