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>
103 lines
4.8 KiB
Python
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()
|