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>
76 lines
2.8 KiB
Python
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()
|