Files
quant-trading/research/tr9_long.py
Gahow Wang 36cb125719 live: commit the TrendRider V9/V10 registrations the monitor already runs
trader.py registers trend_rider_v9 (+ vt24/vt32/vt36) and trend_rider_v10
(+ vt32/vt36) from strategies/trend_rider_v9.py, and bridge.py labels them.
These were left uncommitted by the 2026-10-07 research session while the
monitor ran them from the working tree (state files data/trader_us_trend_rider_v9*/v10*).
research/tr9_*.py is the research trail CLAUDE.md cites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:20:28 -07:00

103 lines
4.8 KiB
Python

"""Long-history (2010+) validation for TrendRider V9 vs V7.
Reuses the Yahoo fetch of research/lab/trend_rider_long.py (ETF_UNIVERSE +
SPY/SHY, auto-adjusted, per-ticker with retries). Runs each variant through
main.strategy_positions + main.simulate_live_book with the same live-book
parameters as the earlier long-run study ($10k, 5bp, $2/order, 10% band).
PYTHONPATH=. uv run python -m research.tr9_long [--refresh]
"""
from __future__ import annotations
import os
import pickle
import sys
import pandas as pd
import trader # noqa: F401 (registry side effects)
from main import simulate_live_book, strategy_positions
from research.lab.trend_rider_long import fetch
from strategies.permanent import ETF_UNIVERSE
from trader import RESIZE_BAND, STRATEGY_REGISTRY
import research.tr9_lab as lab
OUT = os.path.join("data", "research", "tr9_long.pkl")
VARIANTS = {
# baseline registry names
"trend_rider_v7": lambda: STRATEGY_REGISTRY["trend_rider_v7"](),
"trend_rider_v7_vt24": lambda: STRATEGY_REGISTRY["trend_rider_v7_vt24"](),
"trend_rider_v7_vt32": lambda: STRATEGY_REGISTRY["trend_rider_v7_vt32"](),
"trend_rider_v7_vt36": lambda: STRATEGY_REGISTRY["trend_rider_v7_vt36"](),
"trend_rider_v9": lambda: STRATEGY_REGISTRY["trend_rider_v9"](),
"trend_rider_v9_vt32": lambda: STRATEGY_REGISTRY["trend_rider_v9_vt32"](),
"trend_rider_v10": lambda: STRATEGY_REGISTRY["trend_rider_v10"](),
"trend_rider_v10_vt32": lambda: STRATEGY_REGISTRY["trend_rider_v10_vt32"](),
"trend_rider_v10_vt36": lambda: STRATEGY_REGISTRY["trend_rider_v10_vt36"](),
# V9 candidates
"v9vt24_t20": lambda: lab.TrendRiderV9(target_vol=0.24, min_lev=0.5, trail_pct=0.20),
"v9vt28_t20": lambda: lab.TrendRiderV9(trail_pct=0.20),
"v9vt32_t20": lambda: lab.TrendRiderV9(target_vol=0.32, min_lev=0.7, trail_pct=0.20),
"v9vt34_t20": lambda: lab.TrendRiderV9(target_vol=0.34, min_lev=0.72, trail_pct=0.20),
"v9vt34_t20_h10": lambda: lab.TrendRiderV9(target_vol=0.34, min_lev=0.72, trail_pct=0.20,
pt_min_hold=10),
"v9vt34_t20_thr28_h10": lambda: lab.TrendRiderV9(target_vol=0.34, min_lev=0.72, trail_pct=0.20,
pt_min_hold=10, pt_threshold=0.28),
"v9vt34_t20_thr28_b08_h10": lambda: lab.TrendRiderV9(
target_vol=0.34, min_lev=0.72, trail_pct=0.20, pt_min_hold=10, pt_threshold=0.28,
pt_band=0.08),
"v9vt32_t20_thr28_h10": lambda: lab.TrendRiderV9(target_vol=0.32, min_lev=0.7, trail_pct=0.20,
pt_min_hold=10, pt_threshold=0.28),
"v9vt36_t20": lambda: lab.TrendRiderV9(target_vol=0.36, min_lev=0.75, trail_pct=0.20),
"v9vt36_t22": lambda: lab.TrendRiderV9(target_vol=0.36, min_lev=0.75, trail_pct=0.22),
"v9vt28_notrail": lambda: lab.TrendRiderV9(),
}
BLOCKS = [("2010-03-01", "2013-02-28"), ("2013-03-01", "2016-09-26"),
("2016-09-27", "2021-09-26"), ("2021-09-27", "2026-09-25"),
("2010-03-01", "2026-09-25")]
def block_stats(eq: pd.Series, a: str, b: str) -> dict:
r = eq.pct_change().dropna().loc[a:b]
e = (1 + r).cumprod()
return {"cagr": e.iloc[-1] ** (252 / len(r)) - 1,
"sharpe": r.mean() / r.std() * 252 ** 0.5,
"mdd": float(((e / e.cummax()) - 1).min())}
def main() -> None:
refresh = "--refresh" in sys.argv
if refresh or not os.path.exists(OUT):
tickers = sorted(set(ETF_UNIVERSE) | {"SPY", "SHY"})
close, opens = fetch(tickers)
with open(OUT, "wb") as fh:
pickle.dump({"close": close, "opens": opens}, fh)
d = pickle.load(open(OUT, "rb"))
close, opens = d["close"], d["opens"]
print("panel", close.shape, close.index.min().date(), close.index.max().date())
rows = []
for name, factory in VARIANTS.items():
held = strategy_positions(factory(), close, open_data=opens)
eq, _ = simulate_live_book(held, close, 10_000, 0.0005, 2.0, 0, 0, 50.0, RESIZE_BAND)
full = block_stats(eq, *BLOCKS[-1])
per = {f"{a[:4]}-{b[:4]}": block_stats(eq, a, b) for a, b in BLOCKS[:-1]}
rows.append({"name": name, **full, **{k: v["sharpe"] for k, v in per.items()}})
df = pd.DataFrame(rows)
pd.set_option("display.width", 220)
print("\nfull-period 2010-2026:")
print(df[["name", "cagr", "sharpe", "mdd"]].to_string(
index=False, formatters={"cagr": "{:.1%}".format, "sharpe": "{:.2f}".format,
"mdd": "{:.1%}".format}))
print("\nblock Sharpes:")
print(df[["name", "2010-2013", "2013-2016", "2016-2021", "2021-2026"]].to_string(
index=False, formatters={c: "{:.2f}".format for c in
["2010-2013", "2013-2016", "2016-2021", "2021-2026"]}))
if __name__ == "__main__":
main()