Files
quant-trading/research/lab/trend_rider_long.py
Gahow Wang 2c19e5efe5 feat: US 3-sleeve portfolio and lowrisk_top20; US portfolio research harness
Register two live strategies from the 2026-09-29 US research:
- lowrisk_top20: idiosyncratic vol + low beta, top 20, monthly, buffer 2
  (new `low_risk` signal in strategies/robust_momentum.py)
- us_three_sleeve: 1/3 each of overnight_recovery_top10, lowrisk_top20 and
  trend_rider_v7_vt24 in one account (strategies/sleeve_portfolio.py, a
  mixed stock+ETF strategy). Sleeve shares drift with their returns and reset
  at a month start once any is >25% off its share.

Research (research/lab, S&P 500 PIT, next-open fills):
- rotation.py / persistence.py: walk-forward factor weighting (factor
  momentum, rolling IC, market state) and last-year's-winner selection do not
  beat a fixed composite
- us_sweep.py: holdings count, z/vol weights, dynamic N, tranching and market
  overlays (Faber SMA, de-risk vol target, statistical jump model,
  momentum state); engine.py gains zvol weighting and dyn_n
- fetch_us_fundamentals.py / fundamentals.py / fund_eval.py: SEC EDGAR
  point-in-time fundamentals (first-filed values) and factor tests
- trend_rider_long.py / us_final.py: 2010-2026 TrendRider curves and the
  3-sleeve check (2011-2026 Sharpe 1.21, CAGR 21.9%, MaxDD -23%)

Web-UI engine, 2017-09..2026-09, $2/order: us_three_sleeve CAGR 28.9%,
Sharpe 1.49, MaxDD -24.8%.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 01:03:50 +08:00

76 lines
2.8 KiB
Python

"""
TrendRider equity curves from 2010 (the live ETF panel starts in 2016), for
the US portfolio research (research/lab/us_final.py).
PYTHONPATH=. uv run python -m research.lab.trend_rider_long
Downloads the TrendRider ETF basket from 2008 ticker by ticker (batched
Yahoo requests fail intermittently) and runs each variant through the live
book simulation ($2 per order on $10k, 5 bp, 10% resize band). Writes
data/research/tr_long.pkl: {"equity": {name: Series}, "close": DataFrame}.
A ticker that still fails is left out; the strategy then picks among the
rest of its basket (in the 2026-09-29 run DBC was missing, so the risk-off
basket was GLD alone).
"""
import os
import pickle
import time
import warnings
import pandas as pd
import yfinance as yf
from main import simulate_live_book, strategy_positions
from research.lab.fetch_us import _proxy_env
from strategies.permanent import ETF_UNIVERSE
from trader import RESIZE_BAND, STRATEGY_REGISTRY
warnings.filterwarnings("ignore")
OUT = os.path.join("data", "research", "tr_long.pkl")
VARIANTS = ["trend_rider_v7_vt24", "trend_rider_v7_vt36", "trend_rider_v5_vt30"]
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 fetch(tickers: list[str], start: str = "2008-01-01") -> tuple[pd.DataFrame, pd.DataFrame]:
close, opens = {}, {}
for t in tickers:
for attempt in range(5):
try:
h = yf.Ticker(t).history(start=start, auto_adjust=True)
if len(h):
close[t], opens[t] = h["Close"], h["Open"]
break
except Exception as exc: # noqa: BLE001 — retry transient Yahoo errors
print(f" {t}: {exc}")
time.sleep(3 * (attempt + 1))
c, o = pd.DataFrame(close), pd.DataFrame(opens)
c.index = c.index.tz_localize(None).normalize()
o.index = c.index
return c, o
def main() -> None:
_proxy_env()
tickers = sorted(set(ETF_UNIVERSE) | {"SPY", "SHY"})
close, opens = fetch(tickers)
print("missing:", sorted(set(tickers) - set(close.columns)))
out = {}
for name in VARIANTS:
held = strategy_positions(STRATEGY_REGISTRY[name](), close, open_data=opens)
eq, _ = simulate_live_book(held, close, 10_000, 0.0005, 2.0, 0, 0, 50.0, RESIZE_BAND)
out[name] = eq
r = eq.pct_change().dropna()
for a, b in BLOCKS:
x = r.loc[a:b]
e = (1 + x).cumprod()
print(f"{name} {a[:4]}-{b[:4]} cagr {(e.iloc[-1] ** (252 / len(x)) - 1) * 100:5.1f}% "
f"sh {x.mean() / x.std() * 252 ** 0.5:.2f} mdd {((e / e.cummax()) - 1).min() * 100:5.1f}%")
with open(OUT, "wb") as fh:
pickle.dump({"equity": out, "close": close}, fh)
if __name__ == "__main__":
main()