← 返回 下载 metrics.py
"""绩效指标计算。"""
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()