Files
quant/research/trend_rider_v5_eval.py
Gahow Wang 541f7bcf5b research: add strategy evaluation and exploration scripts
Add 28 research scripts covering DCA simulation, momentum evaluation,
Sharpe optimization, trend rider analysis, and US fundamentals exploration.
2026-05-14 12:54:08 +08:00

151 lines
5.1 KiB
Python

"""Evaluate TrendRiderV5 vs V3 baseline and benchmarks.
Run:
uv run python -m research.trend_rider_v5_eval
"""
from __future__ import annotations
import argparse
import os
import sys
from dataclasses import asdict
from itertools import product
import numpy as np
import pandas as pd
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from research.trend_rider_robustness import (
buy_hold_weights,
evaluate_weights,
load_price_panel,
portfolio_returns,
)
from strategies.permanent import TrendRiderV3
from strategies.trend_rider_v5 import TrendRiderV5
IS_START = "2015-01-02"
IS_END = "2020-12-31"
OOS_START = "2021-01-01"
OOS_END = "2026-05-07"
FULL_START = IS_START
FULL_END = OOS_END
def _fmt(x: float) -> str:
return f"{x * 100:7.2f}%"
def print_eval(label: str, ev) -> None:
print(
f" {label:<32s} "
f"CAGR {_fmt(ev.cagr)} Vol {_fmt(ev.volatility)} "
f"Sharpe {ev.sharpe:5.2f} MDD {_fmt(ev.max_drawdown)} "
f"Calmar {ev.calmar:5.2f} X {ev.final_multiple:6.2f} "
f"Sw {ev.switches:4d} Turn {ev.avg_daily_turnover*100:5.2f}%"
)
def evaluate_panel(name: str, weights: pd.DataFrame, prices: pd.DataFrame,
start: str, end: str, transaction_cost: float = 0.001):
return evaluate_weights(name, weights, prices[weights.columns],
transaction_cost=transaction_cost,
start=start, end=end)
def annual_returns_table(weights_map: dict, prices: pd.DataFrame,
transaction_cost: float = 0.001) -> pd.DataFrame:
out = {}
for name, w in weights_map.items():
rets = portfolio_returns(w, prices[w.columns], transaction_cost=transaction_cost)
rets = rets[(rets.index >= FULL_START) & (rets.index <= FULL_END)]
out[name] = (1.0 + rets).groupby(rets.index.year).prod() - 1.0
return pd.DataFrame(out)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--transaction-cost", type=float, default=0.001)
parser.add_argument("--out-dir", default="data")
parser.add_argument("--vol-target", type=float, default=0.30)
args = parser.parse_args()
os.makedirs(args.out_dir, exist_ok=True)
prices = load_price_panel()
print(f"Panel: {prices.index.min().date()} to {prices.index.max().date()}, {prices.shape[1]} cols")
candidates = {
"V3 default": TrendRiderV3(),
"V5 default": TrendRiderV5(),
# Tighter panic detection
"V5 panic 1.4 / 3%": TrendRiderV5(
panic_vol_ratio=1.4, panic_peak_drop_pct=0.03
),
"V5 panic 1.5 / 3.5%": TrendRiderV5(
panic_vol_ratio=1.5, panic_peak_drop_pct=0.035
),
"V5 panic 1.8 / 5%": TrendRiderV5(
panic_vol_ratio=1.8, panic_peak_drop_pct=0.05
),
# Combine panic + harder promote
"V5 panic+conserv": TrendRiderV5(
promote_thresholds=(0.45, 0.70),
demote_thresholds=(0.35, 0.55),
panic_vol_ratio=1.4, panic_peak_drop_pct=0.03,
),
# No panic at all (pure conviction)
"V5 no panic": TrendRiderV5(
panic_vol_ratio=99.0, panic_peak_drop_pct=0.99
),
}
weights_map = {}
print("\n=== Generating signals ===")
for name, strat in candidates.items():
weights_map[name] = strat.generate_signals(prices)
print("\n=== FULL period (2015-01 → 2026-05) ===")
rows = []
for name, w in weights_map.items():
ev = evaluate_panel(name, w, prices, FULL_START, FULL_END, args.transaction_cost)
rows.append(asdict(ev) | {"name": name})
print_eval(name, ev)
spy_w = buy_hold_weights(prices, "SPY")
qqq_w = buy_hold_weights(prices, "QQQ")
bench = {
"SPY B&H": evaluate_panel("SPY B&H", spy_w, prices, FULL_START, FULL_END, 0.0),
"QQQ B&H": evaluate_panel("QQQ B&H", qqq_w, prices, FULL_START, FULL_END, 0.0),
}
for name, ev in bench.items():
print_eval(name, ev)
print("\n=== IS (2015 → 2020) ===")
for name, w in weights_map.items():
ev = evaluate_panel(name, w, prices, IS_START, IS_END, args.transaction_cost)
print_eval(name, ev)
for name, w in [("SPY B&H", spy_w), ("QQQ B&H", qqq_w)]:
ev = evaluate_panel(name, w, prices, IS_START, IS_END, 0.0)
print_eval(name, ev)
print("\n=== OOS (2021 → 2026-05) ===")
for name, w in weights_map.items():
ev = evaluate_panel(name, w, prices, OOS_START, OOS_END, args.transaction_cost)
print_eval(name, ev)
for name, w in [("SPY B&H", spy_w), ("QQQ B&H", qqq_w)]:
ev = evaluate_panel(name, w, prices, OOS_START, OOS_END, 0.0)
print_eval(name, ev)
print("\n=== Annual returns ===")
annual = annual_returns_table(weights_map, prices, args.transaction_cost)
annual = annual.applymap(lambda x: f"{x*100:6.1f}%")
print(annual.to_string())
pd.DataFrame(rows).to_csv(os.path.join(args.out_dir, "v5_eval_full.csv"), index=False)
annual.to_csv(os.path.join(args.out_dir, "v5_eval_annual.csv"))
if __name__ == "__main__":
main()