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:
93
tests/test_sleeve_portfolio.py
Normal file
93
tests/test_sleeve_portfolio.py
Normal 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()
|
||||
Reference in New Issue
Block a user