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>
This commit is contained in:
2026-09-29 01:03:50 +08:00
parent 38a55e7882
commit 2c19e5efe5
16 changed files with 1857 additions and 7 deletions

View File

@@ -0,0 +1,93 @@
"""Contracts of the sleeve portfolio (us_three_sleeve): causal targets, sleeves
kept to their own columns, shares that drift like separate accounts, and the
live trader's session timing."""
import unittest
import numpy as np
import pandas as pd
import trader
from strategies.permanent import ETF_UNIVERSE
from strategies.robust_momentum import RobustMomentumStrategy
from strategies.sleeve_portfolio import (SleevePortfolioStrategy, decisions, drifting_shares,
us_three_sleeve)
def _bars(n_days: int = 520, n_stocks: int = 30, seed: int = 1) -> dict[str, pd.DataFrame]:
rng = np.random.default_rng(seed)
idx = pd.bdate_range("2024-01-01", periods=n_days)
cols = [f"S{i:02d}" for i in range(n_stocks)] + sorted(set(ETF_UNIVERSE))
r = rng.normal(0.0005, 0.015, (n_days, len(cols)))
close = pd.DataFrame(100 * np.exp(np.cumsum(r, axis=0)), index=idx, columns=cols)
opens = close.shift(1).bfill() * np.exp(rng.normal(0, 0.004, close.shape))
return {"close": close, "open": opens}
class SleevePortfolioTests(unittest.TestCase):
def test_targets_on_a_day_ignore_that_day_and_later_bars(self):
bars = _bars()
strat = us_three_sleeve()
cut = bars["close"].index[450]
base = strat.generate_signals(bars)
shocked = {f: b.copy() for f, b in bars.items()}
for f in shocked:
shocked[f].loc[cut:] *= 1.7
again = strat.generate_signals(shocked)
pd.testing.assert_frame_equal(base.loc[:cut], again.loc[:cut])
def test_stock_sleeves_hold_no_etfs_and_the_book_is_not_levered(self):
bars = _bars()
strat = us_three_sleeve()
weights = strat.generate_signals(bars)
etfs = set(ETF_UNIVERSE)
self.assertLessEqual(weights.sum(axis=1).max(), 1.0 + 1e-9)
# After warm-up the stock part is two sleeves' worth of whole names only.
late = weights.iloc[-1]
stocks = late[[c for c in late.index if c not in etfs]]
self.assertEqual(int((stocks > 0).sum()) <= 30, True)
self.assertGreater(stocks.sum(), 0.5)
_, shares = strat.target_weights(bars)
self.assertTrue(np.allclose(shares.sum(axis=1), 1.0))
def test_decisions_are_the_strategy_weights_one_row_earlier(self):
bars = _bars()
strat = RobustMomentumStrategy("low_risk", top_n=5, rebal_freq=5, buffer=1.0)
stock = {f: b[[c for c in b.columns if c.startswith("S")]] for f, b in bars.items()}
d = decisions(strat, stock)
w = strat.generate_signals(stock)
pd.testing.assert_frame_equal(d.iloc[:-1], w.shift(-1).iloc[:-1].fillna(0.0))
self.assertAlmostEqual(d.iloc[-1].sum(), 1.0)
def test_shares_drift_with_sleeve_returns_and_reset_past_the_band(self):
# Two sleeves fully in one asset each; A doubles on day 1, B is flat.
returns = np.array([[0.0, 0.0], [1.0, 0.0], [0.0, 0.0], [0.0, 0.0]])
wa = np.array([[1.0, 0.0]] * 4)
wb = np.array([[0.0, 1.0]] * 4)
base = np.array([0.5, 0.5])
month_start = np.array([False, False, False, True])
shares = drifting_shares([wa, wb], returns, base, month_start, band=0.25)
np.testing.assert_allclose(shares[1], [2 / 3, 1 / 3]) # drifted like two accounts
np.testing.assert_allclose(shares[2], [2 / 3, 1 / 3]) # no reset mid-month
np.testing.assert_allclose(shares[3], [0.5, 0.5]) # 33% off base: reset
kept = drifting_shares([wa, wb], returns, base, month_start, band=0.5)
np.testing.assert_allclose(kept[3], [2 / 3, 1 / 3]) # inside a 50% band
def test_trader_session_targets_use_bars_through_the_previous_day(self):
bars = _bars()
strat = us_three_sleeve()
days = bars["close"].index[-2:]
full = strat.generate_signals(bars).loc[days]
got = trader.bar_session_weights(strat, bars, days)
pd.testing.assert_frame_equal(got, full)
def test_registered_as_a_us_mixed_strategy(self):
self.assertIsInstance(trader.STRATEGY_REGISTRY["us_three_sleeve"](), SleevePortfolioStrategy)
self.assertEqual(set(trader.strategy_extra_tickers("us_three_sleeve")), set(ETF_UNIVERSE))
self.assertTrue(trader.strategy_supports_market("us_three_sleeve", "us"))
self.assertFalse(trader.strategy_supports_market("us_three_sleeve", "cn"))
self.assertEqual(trader.strategy_fields("us_three_sleeve"), ("close", "open"))
if __name__ == "__main__":
unittest.main()