Add 28 research scripts covering DCA simulation, momentum evaluation, Sharpe optimization, trend rider analysis, and US fundamentals exploration.
151 lines
5.1 KiB
Python
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()
|