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.
This commit is contained in:
150
research/trend_rider_v5_eval.py
Normal file
150
research/trend_rider_v5_eval.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user