| Overall Statistics |
|
Total Orders 6514 Average Win 0.15% Average Loss -0.07% Compounding Annual Return 38.553% Drawdown 24.700% Expectancy 0.867 Start Equity 100000 End Equity 598573.21 Net Profit 498.573% Sharpe Ratio 1.27 Sortino Ratio 1.568 Probabilistic Sharpe Ratio 80.930% Loss Rate 41% Win Rate 59% Profit-Loss Ratio 2.17 Alpha 0.173 Beta 0.897 Annual Standard Deviation 0.186 Annual Variance 0.035 Information Ratio 1.201 Tracking Error 0.138 Treynor Ratio 0.263 Total Fees $7230.51 Estimated Strategy Capacity $110000000.00 Lowest Capacity Asset BSV TRO5ZARLX6JP Portfolio Turnover 4.89% Drawdown Recovery 151 |
# =============================================================================
# "Balanced-v2" — 4-sleeve deployable book, ONE daily account, fund-of-funds
# netting. Sleeves + book weights (sum=1.0, gross<=100%, never borrows):
# A4 reversal 0.35 (1-day cross-sectional low-reversal, mcap-weighted)
# gen263 momentum 0.25 (sector-neutral multi-horizon momentum + brakes)
# 531 momentum-breadth 0.25 (285-authoritative momentum + breadth + BIL sweep)
# kinfo regime 0.15 (0379+0273 Nasdaq regime rotation, *0.98 buffer)
# net_w[sym] = 0.35*a4 + 0.25*gen263 + 0.25*531 + 0.15*(0.98*kinfo) (gross<=1)
#
# Derived from the prior "BookB_gen263" book (A4/gen263/410/kinfo) by (1) keeping
# the A4, gen263, kinfo sleeves and the ENTIRE netting + T+1 MarketOnOpenOrder
# execution harness VERBATIM, (2) REMOVING the 410 Ledoit-Wolf ETF sleeve, and
# (3) ADDING a 531 momentum-breadth sleeve ported from S531_285_authoritative.py.
#
# EXECUTION CONTRACT (all sleeves): a sleeve NEVER calls Liquidate/SetHoldings/
# market_order. Each only WRITES its in-bucket target dict (sum<=1.0) + flags
# self._dirty. Risk-off / empty branches set that sleeve's dict = {} (531 sweeps
# its bucket to BIL, its own yield hedge) — never a book-wide liquidate. The
# shared _execute_net harness nets the four dicts, submits sells before buys, and
# fills everything via MarketOnOpenOrder at the T+1 open (decision on T close, no
# look-ahead). The harness scales each dict by its book weight.
#
# 531 uses its OWN breadth latch (allow_531/was_risk_off_531/max_stress_531) and
# its OWN band-ceiling history (band_hist_531) so it never corrupts gen263's
# identically-named state. It shares the read-only per-symbol EMA/ADX/stretch/
# close-window indicators (same band_len=189, hist_len=126, adx_period=14) that
# OnData already maintains — no new hot-path work.
#
# Dates: get_parameter -> default 2010-01-01 .. 2026-06-26.
# =============================================================================
from AlgorithmImports import *
from collections import defaultdict, deque
import numpy as np
import pandas as pd
# ---- shared universe (VERBATIM); serves gen263 momentum + A4 ----
class SectorTopUniverse(FundamentalUniverseSelectionModel):
def __init__(self, algo, blacklist=None):
self.algo = algo
self.blacklist = set(blacklist or [])
super().__init__(self._select)
def _select(self, fundamentals):
buckets = defaultdict(list)
for f in fundamentals:
if not f.has_fundamental_data:
continue
if f.symbol.Value in self.blacklist:
continue
if f.company_reference.primary_exchange_id not in ("NYS", "NAS", "ASE"):
continue
if f.price is None or f.price <= 5:
continue
if f.market_cap is None or f.market_cap < 5_000_000_000:
continue
sector = f.asset_classification.morningstar_sector_code
if sector is None:
continue
buckets[sector].append(f)
symbols = []
for _, stocks in buckets.items():
stocks.sort(key=lambda x: x.market_cap, reverse=True)
symbols.extend(s.symbol for s in stocks[:100])
return symbols
class BalancedV2(QCAlgorithm):
# ---- book weights (sum=1) ----
W_A4 = 0.35
W_476 = 0.25 # gen263 momentum-sleeve bucket weight
W_531 = 0.25 # 531 momentum-breadth sleeve bucket weight
W_KINFO = 0.15
KINFO_BUFFER = 0.98
DEBUG_FILLS = True
USE_VIX = True # CBOE VIX feeds kinfo's vol-scaling. If your account errors on
# /Data/alternative/cboe/vix.csv, set False -> kinfo runs vix_scale=1.0.
# ---- kinfo params (VERBATIM) ----
W_0379 = 0.70
W_0273 = 0.30
REBAL_TOL = 0.08
T_0379 = ["QQQ", "SPY", "TLT", "BSV", "GLD", "QLD", "PSQ", "SHV", "IEF", "QID", "SMH", "USD"]
T_0273 = ["TQQQ", "SQQQ"]
HISTORY_BARS = 1500
STREAK_MIN = 40
DIST_THRESHOLD = -0.08
BOUNCE_ENTRY = 0.045
MAX_SQQQ = 0.90
# ---- momentum-sleeve params: gen263 (replaces 476) ----
lookbacks = [21, 42, 63, 126, 189]
mom_weights = [0.25, 0.20, 0.20, 0.20, 0.15]
stock_count_base = 10
stock_count_choppy = 5
choppy_threshold = 0.25
max_weight = 0.20
band_len = 189
hist_len = 126
adx_limit = 35
adx_period = 14
# ---- A4 params (VERBATIM) ----
A4_TOP_N = 100
A4_TOP_K = 10
A4_MAX_W = 0.30
A4_WIN = 9
# ---- 531 momentum-breadth params (from S531_285_authoritative.py) ----
lookbacks531 = [21, 63, 126, 189, 252] # equal-weight avg (1/3/6/9/12-mo)
stock_count_531 = 10
max_weight531 = 0.20
# NOTE: 531 reuses band_len=189, hist_len=126, adx_limit=35, adx_period=14
# (identical to gen263 above) so it shares OnData's per-symbol indicators.
# =====================================================================
def Initialize(self):
sy = int(self.get_parameter("start_year", "2010"))
sm = int(self.get_parameter("start_month", "1"))
sd = int(self.get_parameter("start_day", "1"))
ey = int(self.get_parameter("end_year", "2026"))
em = int(self.get_parameter("end_month", "6"))
ed = int(self.get_parameter("end_day", "26"))
self.SetStartDate(sy, sm, sd)
self.SetEndDate(ey, em, ed)
self.SetCash(100_000)
self.SetBrokerageModel(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.0
self._n_fills = 0; self._max_moo = 0.0; self._n_moo = 0
self._max_liq = 0.0; self._n_liq = 0; self._n_inv = 0; self._max_gross = 0.0
# ---- kinfo fixed-ETF subscriptions ----
self.syms = {}
for t in self.T_0379 + self.T_0273:
if t in self.syms:
continue
self.syms[t] = self.add_equity(t, Resolution.DAILY).symbol
self.kinfo_symset = set(self.syms.values()) # kinfo ETFs -> NOT momentum/A4 universe
# 531's yield hedge (fixed ETF, outside the fundamental universe)
self.bil = self.add_equity("BIL", Resolution.DAILY).symbol
self.kinfo_symset.add(self.bil)
self.ixic = self.add_equity("ONEQ", Resolution.DAILY).symbol
self.vix = None; self._has_vix = False
if self.USE_VIX:
try:
self.vix = self.add_data(CBOE, "VIX", Resolution.DAILY).symbol
self._has_vix = True
except Exception:
self.vix = None; self._has_vix = False
self.kinfo_symset.add(self.ixic)
if self.vix is not None:
self.kinfo_symset.add(self.vix)
# ---- momentum/A4 shared universe ----
self.UniverseSettings.Resolution = Resolution.Daily
self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.TOTAL_RETURN
self.UniverseSettings.Leverage = 4
self.SetUniverseSelection(SectorTopUniverse(self, blacklist={"GME", "AMC"}))
# gen263 momentum per-symbol state
self.symbols = set()
self.symbol_to_sector = {}
self.ma = {}; self.adx = {}; self.close_win = {}
self.stretch_ema = {}; self.band_hist = {}; self.stretch_max = {}
self.current_band_idx = {}
self.BOTTOM_LEVELS = {0, 1, 2, 3, 4}
self.max_stress_level = 0.0
self.was_risk_off = False
self.allow_universe = True
self.prev_bottom_frac = None
self.portfolio_peak = 100_000.0
# ---- sleeve target dicts + dirty flag ----
self.tgt_kinfo = {}; self.tgt_476 = {}; self.tgt_a4 = {}; self.tgt_531 = {}
self._kinfo_last = None
self._dirty = False
# ---- 531 sleeve's OWN breadth-latch + band-ceiling state (isolated
# from gen263's identically-named allow_universe/was_risk_off/band_hist) ----
self.allow_531 = True
self.was_risk_off_531 = False
self.max_stress_531 = 0.0
self.band_hist_531 = {}
self.SetBenchmark("SPY")
self.SetWarmUp(self.HISTORY_BARS, Resolution.Daily)
# momentum (gen263) + A4 monthly decision at month end (write target dicts). kinfo daily in OnData.
self.Schedule.On(self.DateRules.MonthEnd("SPY"),
self.TimeRules.BeforeMarketClose("SPY", 5), self.Rebalance476)
self.Schedule.On(self.DateRules.MonthEnd("SPY"),
self.TimeRules.BeforeMarketClose("SPY", 5), self.RebalanceA4)
# 531 momentum-breadth monthly decision at month end (T-close -> T+1 open)
self.Schedule.On(self.DateRules.MonthEnd("SPY"),
self.TimeRules.BeforeMarketClose("SPY", 5), self.Rebalance531)
# =====================================================================
# kinfo helpers + sleeves (VERBATIM)
@staticmethod
def _rsi_wilder(px, window):
delta = px.diff(); gain = delta.clip(lower=0.0); loss = (-delta).clip(lower=0.0)
ag = gain.ewm(alpha=1.0 / window, adjust=False).mean()
al = loss.ewm(alpha=1.0 / window, adjust=False).mean()
rs = ag / al.replace(0.0, np.nan)
return (100.0 - 100.0 / (1.0 + rs)).fillna(50.0)
@staticmethod
def _rsi_sma_seed(series, period):
delta = series.diff(); gain = delta.clip(lower=0.0).to_numpy(); loss = (-delta.clip(upper=0.0)).to_numpy()
n = len(series); ag = np.full(n, np.nan); al = np.full(n, np.nan)
if n > period:
ag[period] = np.nanmean(gain[1:period + 1]); al[period] = np.nanmean(loss[1:period + 1])
ip = 1.0 / period
for i in range(period + 1, n):
ag[i] = ag[i - 1] * (1 - ip) + gain[i] * ip
al[i] = al[i - 1] * (1 - ip) + loss[i] * ip
rs = np.where(al > 0, ag / np.where(al == 0, np.nan, al), np.nan)
return pd.Series(100.0 - 100.0 / (1.0 + rs), index=series.index).fillna(50.0)
@staticmethod
def _top(tickers, rsi10):
best = tickers[0]; bv = rsi10[best]
for t in tickers[1:]:
if rsi10[t] > bv: best, bv = t, rsi10[t]
return best
def _decide_0379(self, close):
rsi_assets = ["QQQ", "SPY", "TLT", "BSV", "GLD", "PSQ", "IEF", "QLD", "USD", "SMH"]
rsi10 = {t: float(self._rsi_sma_seed(close[t], 10).iloc[-1]) for t in rsi_assets}
rsi60_spy = float(self._rsi_sma_seed(close["SPY"], 60).iloc[-1])
spy = close["SPY"]; qqq = close["QQQ"]
spy_ma200 = float(spy.rolling(200).mean().iloc[-1])
spy_ma20 = float(spy.rolling(20).mean().iloc[-1])
qqq_ma20 = float(qqq.rolling(20).mean().iloc[-1])
spy_l = float(spy.iloc[-1]); qqq_l = float(qqq.iloc[-1])
cr10 = float((qqq.iloc[-1] / qqq.iloc[-11] - 1.0) * 100.0)
cr60 = float((qqq.iloc[-1] / qqq.iloc[-61] - 1.0) * 100.0)
cr252 = float((qqq.iloc[-1] / qqq.iloc[-253] - 1.0) * 100.0)
w = {}
def add(t, x): w[t] = w.get(t, 0.0) + x
if spy_l > spy_ma200:
if rsi10["QQQ"] > 80.0: add("TLT", 1.0)
elif rsi10["SPY"] > 80.0: add("TLT", 1.0)
elif rsi60_spy > 60.0: add(self._top(["TLT", "BSV", "GLD"], rsi10), 1.0)
else: add("QQQ", 1.0)
else:
if rsi10["QQQ"] < 30.0:
add("QLD", 1.0)
else:
tlt_gt_psq = rsi10["TLT"] > rsi10["PSQ"]
qqq_below = qqq_l < qqq_ma20
if qqq_l > qqq_ma20:
if rsi10["PSQ"] < 30.0: add("PSQ", 0.50)
elif cr10 > 5.5: add("SHV", 0.50)
else: add("QQQ", 0.50)
else:
add(self._top(["IEF", "PSQ"], rsi10), 0.50)
if cr252 < -20.0:
if qqq_below:
if cr60 <= -12.0:
sib1 = "SPY" if spy_l > spy_ma20 else ("QQQ" if tlt_gt_psq else "PSQ")
sib2 = "QQQ" if tlt_gt_psq else "PSQ"
add(sib1, 0.25); add(sib2, 0.25)
else:
add("QLD" if tlt_gt_psq else "QID", 0.50)
else:
if rsi10["PSQ"] < 31.0: add("PSQ", 0.50)
elif cr10 > 5.5: add("PSQ", 0.50)
else: add(self._top(["QQQ", "SMH"], rsi10), 0.50)
else:
if qqq_below:
add("QLD" if tlt_gt_psq else "QID", 0.50)
else:
if rsi10["PSQ"] < 31.0: add("QID", 0.50)
elif cr10 > 5.5: add("QID", 0.50)
else: add(self._top(["QLD", "USD"], rsi10), 0.50)
return w
def _decide_0273(self, df, vix_series):
df = df.copy()
df["qqq_sma_50"] = df["QQQ"].rolling(50).mean()
df["qqq_sma_200"] = df["QQQ"].rolling(200).mean()
df["qqq_dist_sma_50"] = (df["QQQ"] - df["qqq_sma_50"]) / df["qqq_sma_50"]
df["qqq_dist_sma_200"] = (df["QQQ"] - df["qqq_sma_200"]) / df["qqq_sma_200"]
df["qqq_sma_200_slope_20"] = (df["qqq_sma_200"] - df["qqq_sma_200"].shift(20)) / df["qqq_sma_200"].shift(20)
df["qqq_roc_21"] = df["QQQ"].pct_change(21)
df["ixic_sma_50"] = df["IXIC"].rolling(50).mean()
df["ixic_sma_100"] = df["IXIC"].rolling(100).mean()
df["ixic_sma_175"] = df["IXIC"].rolling(175).mean()
df["ixic_sma_200"] = df["IXIC"].rolling(200).mean()
df["ixic_sma_200_dist"] = (df["IXIC"] - df["ixic_sma_200"]) / df["ixic_sma_200"]
df["ixic_above_sma_50"] = (df["IXIC"] > df["ixic_sma_50"]).astype(float)
df["ixic_above_sma_100"] = (df["IXIC"] > df["ixic_sma_100"]).astype(float)
df["ixic_above_sma_175"] = (df["IXIC"] > df["ixic_sma_175"]).astype(float)
df["qqq_rsi_14"] = self._rsi_wilder(df["QQQ"], 14)
df["tqqq_vol20"] = df["TQQQ"].pct_change().rolling(20).std() * np.sqrt(252)
if vix_series is not None and not vix_series.empty:
v = vix_series.reindex(df.index).ffill()
df["vix_ratio_20"] = (v / v.rolling(20).mean()).fillna(1.0)
else:
df["vix_ratio_20"] = 1.0
feat = df.iloc[-1]
req = ["qqq_dist_sma_50", "qqq_dist_sma_200", "qqq_sma_200_slope_20", "qqq_roc_21",
"tqqq_vol20", "qqq_rsi_14", "ixic_sma_200_dist", "ixic_above_sma_50",
"ixic_above_sma_100", "ixic_above_sma_175", "vix_ratio_20"]
if feat[req].isna().any():
return {}
above175 = bool(feat["ixic_above_sma_175"] > 0.5)
above100 = bool(feat["ixic_above_sma_100"] > 0.5)
above50 = bool(feat["ixic_above_sma_50"] > 0.5)
rsi14 = float(feat["qqq_rsi_14"]); roc21 = float(feat["qqq_roc_21"])
dist_200_ixic = float(feat["ixic_sma_200_dist"]); vix_ratio = float(feat["vix_ratio_20"])
strong_bull = above175 and above50 and (rsi14 > 55.0) and (roc21 > 0.0)
full_momentum = (rsi14 > 60.0) and (roc21 > 0.0)
regime = 4
if above100: regime = 3
if above175 and (not strong_bull): regime = 2
if above175 and strong_bull and (not full_momentum): regime = 5
if above175 and strong_bull and full_momentum: regime = 1
vix_scale = 1.0
if vix_ratio >= 2.2: vix_scale = 0.38
elif vix_ratio >= 1.5: vix_scale = 0.62
elif vix_ratio >= 1.1: vix_scale = 0.88
w_qqq, w_tqqq, w_sqqq = 0.0, 0.0, 0.0
very_extended = (regime == 1) and (dist_200_ixic > 0.15)
normal_bull = (regime == 1) and (not very_extended)
if very_extended: w_tqqq = float(np.clip(0.84 * vix_scale, 0.35, 0.84))
elif normal_bull: w_tqqq = float(np.clip(0.90 * vix_scale, 0.35, 0.90))
elif regime == 5: w_tqqq = float(np.clip(0.60 * vix_scale, 0.28, 0.60))
elif regime == 2: w_tqqq = float(np.clip(0.70 * vix_scale, 0.28, 0.70))
elif regime == 4: w_qqq = 0.45
elif regime == 3:
w_tqqq = 0.62 if rsi14 > 55 else (0.48 if rsi14 > 45 else 0.34)
t_vol = float(feat["tqqq_vol20"])
if above175 and (t_vol < 0.60): w_tqqq = min(0.95, w_tqqq * 1.05)
if above175 and (t_vol >= 0.80): w_tqqq = min(0.95, w_tqqq * 0.75)
if very_extended: w_tqqq = min(0.95, w_tqqq * 0.92)
now = df.iloc[-1]
dist50 = float(now["qqq_dist_sma_50"]); dist200 = float(now["qqq_dist_sma_200"])
slope200 = float(now["qqq_sma_200_slope_20"])
if any(np.isnan([dist50, dist200, slope200])):
return {}
below_200 = (df["qqq_dist_sma_200"] < 0).astype(int).values
streak = 0
for b in below_200[:-1][::-1]:
if b == 1: streak += 1
else: break
bear_gate = (streak >= self.STREAK_MIN) and (dist200 < dist50) and (dist200 < self.DIST_THRESHOLD) and (slope200 < 0.0)
roc3 = float(df["QQQ"].pct_change(3).shift(1).iloc[-1])
if bear_gate and (roc3 >= self.BOUNCE_ENTRY):
w_qqq, w_tqqq, w_sqqq = 0.0, 0.0, self.MAX_SQQQ
gross = w_qqq + w_tqqq + w_sqqq
if gross > 1.0:
w_qqq /= gross; w_tqqq /= gross; w_sqqq /= gross
return {"QQQ": w_qqq, "TQQQ": w_tqqq, "SQQQ": w_sqqq}
def _decide_kinfo(self):
all_syms = [self.syms[t] for t in (self.T_0379 + self.T_0273)] + [self.ixic]
hist = self.history(all_syms, self.HISTORY_BARS, Resolution.DAILY)
if hist is None or hist.empty or "close" not in hist.columns:
return None
try:
wide = hist["close"].unstack(level=0)
except Exception:
return None
col = {}
for t in (self.T_0379 + self.T_0273):
s = self.syms[t]
if s in wide.columns: col[t] = wide[s]
if self.ixic in wide.columns: col["IXIC"] = wide[self.ixic]
if any(t not in col for t in self.T_0379) or "IXIC" not in col:
return None
close = pd.DataFrame(col).dropna()
if len(close) < 260:
return None
vix_series = None
if self._has_vix and self.vix is not None:
try:
vh = self.history(self.vix, self.HISTORY_BARS, Resolution.DAILY)
if vh is not None and not vh.empty:
s = vh["value"] if "value" in vh.columns else (vh["close"] if "close" in vh.columns else None)
if s is not None:
if isinstance(s.index, pd.MultiIndex):
s.index = s.index.get_level_values(-1)
s.index = pd.to_datetime(s.index).tz_localize(None)
vix_series = s[~pd.Index(s.index).duplicated(keep="last")].sort_index().astype(float)
except Exception:
vix_series = None
w379 = self._decide_0379(close[self.T_0379])
df0273 = close[["QQQ", "TQQQ", "SQQQ", "IXIC"]]
w273 = self._decide_0273(df0273, vix_series)
target = {}
for t, x in w379.items():
target[t] = target.get(t, 0.0) + self.W_0379 * x
for t, x in w273.items():
if x > 0:
target[t] = target.get(t, 0.0) + self.W_0273 * x
target = {t: round(w, 3) for t, w in target.items() if w > 0.005}
if self._kinfo_last is not None:
same = set(target) == set(self._kinfo_last)
moves = [abs(target.get(t, 0.0) - self._kinfo_last.get(t, 0.0)) for t in set(target) | set(self._kinfo_last)]
if same and (max(moves) if moves else 0.0) < self.REBAL_TOL:
return None
self._kinfo_last = target
return target
# =====================================================================
def OnSecuritiesChanged(self, changes):
for sec in changes.AddedSecurities:
s = sec.Symbol
if s in self.kinfo_symset:
continue
sec.SetFeeModel(InteractiveBrokersFeeModel())
sec.SetSlippageModel(ConstantSlippageModel(0.001))
self.symbols.add(s)
self.stretch_max[s] = 0.0
self.ma[s] = self.EMA(s, self.band_len, Resolution.Daily)
self.adx[s] = self.ADX(s, self.adx_period, Resolution.Daily)
self.stretch_ema[s] = self.EMA(s, self.band_len, Resolution.Daily)
self.close_win[s] = RollingWindow[float](self.band_len)
self.band_hist[s] = RollingWindow[int](self.hist_len)
self.band_hist_531[s] = RollingWindow[int](self.hist_len) # 531's own ceiling history
try:
sector = sec.Fundamentals.AssetClassification.MorningstarSectorCode
if sector is not None and sector != 0:
self.symbol_to_sector[s] = sector
except Exception:
pass
for sec in changes.RemovedSecurities:
s = sec.Symbol
if s in self.kinfo_symset:
continue
self.symbols.discard(s)
self.ma.pop(s, None); self.adx.pop(s, None); self.stretch_ema.pop(s, None)
self.close_win.pop(s, None); self.band_hist.pop(s, None)
self.band_hist_531.pop(s, None)
self.current_band_idx.pop(s, None); self.stretch_max.pop(s, None)
self.symbol_to_sector.pop(s, None)
def _band_index(self, price, bands):
for i in range(len(bands) - 1):
if bands[i] <= price < bands[i + 1]:
return i
return len(bands) - 2
# =====================================================================
def OnData(self, data):
for s in list(self.symbols):
if not data.ContainsKey(s):
continue
bar = data[s]
if bar is None:
continue
close = bar.Close
self.close_win[s].Add(close)
if not self.close_win[s].IsReady or not self.ma[s].IsReady:
continue
dev = np.std(list(self.close_win[s]))
if dev <= 0:
continue
mid = self.ma[s].Current.Value
stretch = abs(close - mid) / dev
self.stretch_ema[s].Update(self.Time, stretch)
if stretch > self.stretch_max[s]:
self.stretch_max[s] = stretch
bands = [mid - dev * 1.618, mid - dev * 1.382, mid - dev, mid - dev * 0.809,
mid - dev * 0.5, mid - dev * 0.382, mid, mid + dev * 0.382, mid + dev * 0.5,
mid + dev * 0.809, mid + dev, mid + dev * 1.382, mid + dev * 1.618]
self.current_band_idx[s] = self._band_index(close, bands)
if self.IsWarmingUp:
return
kt = self._decide_kinfo()
if kt is not None:
self.tgt_kinfo = kt
self._dirty = True
if self._dirty:
self._execute_net()
self._dirty = False
# =====================================================================
def Rebalance476(self):
# MOMENTUM SLEEVE == gen263 (replaces 476). Ported faithfully but it only
# WRITES self.tgt_476 (sum<=1.0 within the 0.35 bucket) + flags _dirty;
# it NEVER calls Liquidate/SetHoldings. Risk-off / empty branches set
# self.tgt_476 = {} (momentum goes to cash inside its bucket, no book-wide
# liquidate). Everything fills via _execute_net (MarketOnOpenOrder, T+1).
if self.IsWarmingUp:
return
idxs = list(self.current_band_idx.values())
if len(idxs) < 50:
return
# Stock-level breadth
stock_bottom_frac = sum(i in self.BOTTOM_LEVELS for i in idxs) / len(idxs)
# Sector-level breadth: fraction of sectors where majority in bottom bands
sector_bottoms = defaultdict(list)
for s, idx in self.current_band_idx.items():
sector = self.symbol_to_sector.get(s)
if sector is not None:
sector_bottoms[sector].append(idx in self.BOTTOM_LEVELS)
if sector_bottoms:
sector_stress_count = sum(
1 for flags in sector_bottoms.values()
if len(flags) > 0 and sum(flags) / len(flags) > 0.50
)
sector_bottom_frac = sector_stress_count / len(sector_bottoms)
# 50/50 blend: sector signal smooths intra-sector noise (gen82 proven)
bottom_frac = 0.5 * stock_bottom_frac + 0.5 * sector_bottom_frac
else:
bottom_frac = stock_bottom_frac
# Stress rate of change (clean scalar, not deque)
stress_roc = 0.0
if self.prev_bottom_frac is not None:
stress_roc = bottom_frac - self.prev_bottom_frac
self.prev_bottom_frac = bottom_frac
self.max_stress_level = max(self.max_stress_level, bottom_frac)
# Portfolio equity curve tracking for conditional brake
current_value = self.Portfolio.TotalPortfolioValue
self.portfolio_peak = max(self.portfolio_peak, current_value)
portfolio_dd = (self.portfolio_peak - current_value) / self.portfolio_peak if self.portfolio_peak > 0 else 0.0
# Risk-off latch
if bottom_frac >= 0.45:
self.allow_universe = False
self.was_risk_off = True
elif self.was_risk_off:
denominator = max(self.max_stress_level, 0.10)
improvement = (self.max_stress_level - bottom_frac) / denominator
if improvement >= 0.60 or bottom_frac < 0.15:
for s in self.symbols:
if s in self.band_hist:
self.band_hist[s] = RollingWindow[int](self.hist_len)
self.allow_universe = True
self.was_risk_off = False
self.max_stress_level = 0.0
self.prev_bottom_frac = None # fresh RoC after recovery
# Reset peak to current (post-crash) level so brake starts fresh
self.portfolio_peak = current_value
# Begin graduated re-entry: cap exposure for 3 months post-crash
self.in_recovery = True
self.recovery_months = 0
else:
self.allow_universe = True
if not self.allow_universe:
self.tgt_476 = {}; self._dirty = True
return
# Dynamic position count: concentrate in choppy regimes (gen100 proven)
stock_count = self.stock_count_choppy if bottom_frac >= self.choppy_threshold else self.stock_count_base
hist = self.History(list(self.symbols), max(self.lookbacks) + 1, Resolution.Daily)
if hist.empty:
return
closes = hist["close"].unstack(0)
momentum = {}
consistency_map = {}
for s in self.symbols:
if s not in closes:
continue
px = closes[s]
if len(px) < max(self.lookbacks) + 1:
continue
if not self.adx[s].IsReady or self.adx[s].Current.Value > self.adx_limit:
continue
# Compute per-lookback returns once (reused for both momentum and consistency)
lb_returns = [px.iloc[-1] / px.iloc[-lb - 1] - 1 for lb in self.lookbacks]
mom = sum(w * r for w, r in zip(self.mom_weights, lb_returns))
if not self.ma[s].IsReady:
continue
price = self.Securities[s].Price
ema = self.ma[s].Current.Value
if price <= ema:
continue
if mom > 0:
momentum[s] = mom
# Consistency: fraction of lookback horizons with positive return
n_positive = sum(1 for r in lb_returns if r > 0)
consistency_map[s] = n_positive / len(self.lookbacks)
if not momentum:
self.tgt_476 = {}; self._dirty = True
return
top = sorted(momentum, key=momentum.get, reverse=True)[:stock_count]
scaled = {}
vol_20d = {}
for s in top:
if not self.ma[s].IsReady or not self.stretch_ema[s].IsReady:
continue
dev = np.std(list(self.close_win[s]))
if dev <= 0:
continue
mid = self.ma[s].Current.Value
lm = self.stretch_ema[s].Current.Value
lm2 = lm / 2.0
lm3 = lm2 * 0.38196601
lm4 = lm * 1.38196601
lm5 = lm * 1.61803399
lm6 = (lm + lm2) / 2.0
bands = [
mid - dev * lm5, mid - dev * lm4, mid - dev * lm,
mid - dev * lm6, mid - dev * lm2, mid - dev * lm3, mid,
mid + dev * lm3, mid + dev * lm2, mid + dev * lm6,
mid + dev * lm, mid + dev * lm4, mid + dev * lm5
]
price = self.Securities[s].Price
idx = self._band_index(price, bands)
self.band_hist[s].Add(idx)
hist_idx = list(self.band_hist[s])
historical_high = max(hist_idx) if hist_idx else idx
if historical_high <= 0:
scale = 1.0
elif idx >= historical_high:
scale = 0.0
else:
scale = max(0.2, 1.0 - idx / historical_high)
current_stretch = self.stretch_ema[s].Current.Value
peak_stretch = self.stretch_max.get(s, 0.0)
if idx >= 10 and peak_stretch > 0:
if current_stretch < (peak_stretch * 0.80):
scale = 0.2
# Momentum consistency soft multiplier: multi-horizon-confirmed trends
# get full weight, mixed-horizon signals down-weighted. Range [0.6, 1.0].
# Only affects RELATIVE weights within top-K, not total exposure.
consistency = consistency_map.get(s, 0.6)
consist_factor = 0.6 + 0.4 * consistency
scaled[s] = momentum[s] * scale * consist_factor
# 20-day vol for position cap
px = closes[s]
if len(px) >= 21:
vol_20d[s] = float(px.pct_change().iloc[-20:].std())
else:
vol_20d[s] = 0.015
if not scaled:
self.tgt_476 = {}; self._dirty = True
return
# Base exposure from breadth interpolation
min_stress = 0.15
max_stress = 0.45
target_exposure = float(np.interp(bottom_frac, [min_stress, max_stress], [1.0, 0.0]))
# RoC early warning: accelerating stress -> cut exposure 30%
if stress_roc >= 0.15:
target_exposure *= 0.70
# Conditional portfolio equity brake: fires only when BOTH portfolio is
# materially down AND breadth is worsening. NOTE: in the book this reads
# WHOLE-BOOK equity (not gen263-sleeve equity) since it is one account.
if portfolio_dd > 0.07 and stress_roc > 0.03:
dd_brake = max(0.25, 1.0 - (portfolio_dd - 0.07) / 0.22)
target_exposure = min(target_exposure, dd_brake)
target_exposure = float(round(max(0.0, min(1.0, target_exposure)), 2))
# Weight computation
total_scaled = sum(scaled.values())
raw_weights = {s: v / total_scaled for s, v in scaled.items()}
capped_weights = {s: min(self.max_weight, w) for s, w in raw_weights.items()}
# Volatility cap without renormalization (gen100 proven)
for s in list(capped_weights.keys()):
vol = vol_20d.get(s, 0.015)
if vol > 0:
vol_cap_factor = min(1.0, 0.015 / vol)
capped_weights[s] = min(capped_weights[s], self.max_weight * vol_cap_factor)
current_sum = sum(capped_weights.values())
final_weights = {}
if current_sum > 0:
for s, w in capped_weights.items():
final_weights[s] = (w / current_sum) * target_exposure
# WRITE in-bucket momentum targets (sum == target_exposure <= 1.0); the
# harness scales by W_476 = 0.35. No SetHoldings / Liquidate here.
self.tgt_476 = {s: w for s, w in final_weights.items() if w > 0}
self._dirty = True
# =====================================================================
def RebalanceA4(self):
# A4: 1-day cross-sectional low-reversal on the top-100-by-mcap subset of the universe.
if self.IsWarmingUp or len(self.symbols) < 20:
return
# top-100 by market cap among universe members (== A4's standalone universe)
cand = []
for s in self.symbols:
f = self.Securities[s].Fundamentals
if f is None or f.MarketCap is None:
continue
cand.append((s, float(f.MarketCap)))
if len(cand) < 20:
return
cand.sort(key=lambda x: x[1], reverse=True)
a4_syms = [s for s, _ in cand[:self.A4_TOP_N]]
hist = self.History(a4_syms, self.A4_WIN + 2, Resolution.Daily)
if hist.empty or "low" not in hist.columns:
return
lows = hist["low"].unstack(level=0)
cs_rank = lows.rank(axis=1, pct=True)
ts_rank = cs_rank.rolling(self.A4_WIN).apply(lambda x: x.iloc[-1] / len(x) if len(x) > 0 else 0).iloc[-1]
score = (-ts_rank).dropna()
if score.empty:
return
top = score.nlargest(self.A4_TOP_K).index.tolist()
if not top:
return
mcaps = {}
for s in top:
f = self.Securities[s].Fundamentals
if f is None or f.MarketCap is None:
continue
mcaps[s] = float(f.MarketCap)
if not mcaps:
return
total = sum(mcaps.values())
target = {s: min(self.A4_MAX_W, mcaps[s] / total) for s in mcaps}
sw = sum(target.values())
self.tgt_a4 = {s: v / sw for s, v in target.items()} if sw > 0 else {}
self._dirty = True
# =====================================================================
def Rebalance531(self):
# 531 momentum-breadth sleeve (285-authoritative). Ported faithfully from
# S531_285_authoritative.py but it only WRITES self.tgt_531 (in-bucket,
# sum<=1.0) + flags _dirty; it NEVER calls Liquidate/SetHoldings. Risk-off
# / no-signal branches sweep the WHOLE 531 bucket to BIL (531's own yield
# hedge), not a book-wide liquidate. Uses its OWN breadth latch + band
# ceiling history so it never corrupts gen263's identically-named state.
# Reuses OnData's shared read-only EMA/ADX/stretch/close-window indicators.
if self.IsWarmingUp:
return
idxs = list(self.current_band_idx.values())
if len(idxs) < 50:
return
bottom_frac = sum(i in self.BOTTOM_LEVELS for i in idxs) / len(idxs)
self.max_stress_531 = max(self.max_stress_531, bottom_frac)
if bottom_frac >= 0.45:
self.allow_531 = False
self.was_risk_off_531 = True
elif self.was_risk_off_531:
denominator = max(self.max_stress_531, 0.10)
improvement = (self.max_stress_531 - bottom_frac) / denominator
if improvement >= 0.60 or bottom_frac < 0.15:
for s in self.symbols:
if s in self.band_hist_531:
self.band_hist_531[s] = RollingWindow[int](self.hist_len)
self.allow_531 = True
self.was_risk_off_531 = False
self.max_stress_531 = 0.0
else:
self.allow_531 = True
# Risk-off: sweep 531's bucket 100% to the Treasury hedge (BIL)
if not self.allow_531:
self.tgt_531 = {self.bil: 1.0}; self._dirty = True
return
hist = self.History(list(self.symbols), max(self.lookbacks531) + 1, Resolution.Daily)
if hist.empty:
return
closes = hist["close"].unstack(0)
momentum = {}
for s in self.symbols:
if s not in closes:
continue
px = closes[s]
if len(px) < max(self.lookbacks531) + 1:
continue
if not self.adx[s].IsReady or self.adx[s].Current.Value > self.adx_limit:
continue
mom = np.mean([px.iloc[-1] / px.iloc[-lb - 1] - 1 for lb in self.lookbacks531])
if not self.ma[s].IsReady:
continue
price = self.Securities[s].Price
ema = self.ma[s].Current.Value
if price <= ema:
continue
if mom > 0:
momentum[s] = mom
if not momentum:
self.tgt_531 = {self.bil: 1.0}; self._dirty = True
return
top = sorted(momentum, key=momentum.get, reverse=True)[:self.stock_count_531]
scaled = {}
for s in top:
if not self.ma[s].IsReady or not self.stretch_ema[s].IsReady:
continue
dev = np.std(list(self.close_win[s]))
if dev <= 0:
continue
mid = self.ma[s].Current.Value
lm = self.stretch_ema[s].Current.Value
lm2 = lm / 2.0
lm3 = lm2 * 0.38196601
lm4 = lm * 1.38196601
lm5 = lm * 1.61803399
lm6 = (lm + lm2) / 2.0
bands = [
mid - dev * lm5, mid - dev * lm4, mid - dev * lm,
mid - dev * lm6, mid - dev * lm2, mid - dev * lm3, mid,
mid + dev * lm3, mid + dev * lm2, mid + dev * lm6,
mid + dev * lm, mid + dev * lm4, mid + dev * lm5
]
price = self.Securities[s].Price
idx = self._band_index(price, bands)
self.band_hist_531[s].Add(idx)
hist_idx = list(self.band_hist_531[s])
historical_high = max(hist_idx) if hist_idx else idx
if historical_high <= 0:
scale = 1.0
elif idx >= historical_high:
scale = 0.0
else:
scale = max(0.2, 1.0 - idx / historical_high)
current_stretch = self.stretch_ema[s].Current.Value
peak_stretch = self.stretch_max.get(s, 0.0)
if idx >= 10 and peak_stretch > 0 and current_stretch < (peak_stretch * 0.80):
scale = 0.2
scaled[s] = (momentum[s] * self.adx[s].Current.Value) * scale
if not scaled:
self.tgt_531 = {self.bil: 1.0}; self._dirty = True
return
min_stress = 0.15
max_stress = 0.45
target_exposure = float(round(np.interp(bottom_frac, [min_stress, max_stress], [1.0, 0.0]), 2))
total_scaled = sum(scaled.values())
raw_weights = {s: v / total_scaled for s, v in scaled.items()}
capped_weights = {s: min(self.max_weight531, w) for s, w in raw_weights.items()}
current_sum = sum(capped_weights.values())
final_weights = {}
if current_sum > 0:
for s, w in capped_weights.items():
final_weights[s] = (w / current_sum) * target_exposure
# Sweep all unallocated bucket capital into BIL to eliminate cash drag
hedge_allocation = round(1.0 - target_exposure, 2)
if hedge_allocation > 0:
final_weights[self.bil] = hedge_allocation
self.tgt_531 = {s: w for s, w in final_weights.items() if w > 0}
self._dirty = True
# =====================================================================
def _execute_net(self):
net = {}
for tk, w in self.tgt_kinfo.items():
sym = self.syms.get(tk)
if sym is None:
continue
net[sym] = net.get(sym, 0.0) + self.W_KINFO * self.KINFO_BUFFER * w
for sym, w in self.tgt_476.items():
net[sym] = net.get(sym, 0.0) + self.W_476 * w
for sym, w in self.tgt_a4.items():
net[sym] = net.get(sym, 0.0) + self.W_A4 * w
for sym, w in self.tgt_531.items():
net[sym] = net.get(sym, 0.0) + self.W_531 * w
net = {s: w for s, w in net.items() if w > 0.0005}
equity = self.Portfolio.TotalPortfolioValue
keep = set(net)
for kvp in list(self.Portfolio):
sym, h = kvp.Key, kvp.Value
if sym in (self.ixic, self.vix):
continue
if h.Invested and sym not in keep and h.Quantity != 0:
self.MarketOnOpenOrder(sym, -h.Quantity, tag=f"EXIT now={self.Time.date()}")
def cur_w(sym):
return (float(self.Portfolio[sym].HoldingsValue) / equity) if equity > 0 else 0.0
for sym, w in sorted(net.items(), key=lambda kv: kv[1] - cur_w(kv[0])):
if not self.Securities.ContainsKey(sym):
continue
price = float(self.Securities[sym].Price)
if price <= 0:
continue
tq = int(equity * w / price)
cur = self.Portfolio[sym].Quantity if self.Portfolio.ContainsKey(sym) else 0
d = tq - cur
if d != 0:
self.MarketOnOpenOrder(sym, d, tag=f"ENTRY w={w:.3f} px={price:.2f} now={self.Time.date()}")
# =====================================================================
def OnOrderEvent(self, oe):
if not self.DEBUG_FILLS:
return
if oe.Status == OrderStatus.INVALID:
self._n_inv += 1
self.SetRuntimeStatistic("Invalid", str(self._n_inv))
msg = str(oe.Message)[:50] if oe.Message else "?"
self.SetRuntimeStatistic("InvMsg", f"{oe.Symbol.Value}:{msg}")
return
if oe.Status != OrderStatus.FILLED or oe.FillQuantity == 0:
return
sec = self.Securities[oe.Symbol]; fp = float(oe.FillPrice); o = float(sec.Open)
dev = abs(fp - o) / o if o > 0 else 0.0
try:
od = self.Transactions.GetOrderById(oe.OrderId); ot = int(od.Type) if od is not None else -1
except Exception:
ot = -1
if ot == 4:
self._max_moo = max(self._max_moo, dev); self._n_moo += 1
else:
self._max_liq = max(self._max_liq, dev); self._n_liq += 1
self._n_fills += 1
gross = sum(abs(p.HoldingsValue) for p in self.Portfolio.Values) / self.Portfolio.TotalPortfolioValue
self._max_gross = max(self._max_gross, gross)
self.SetRuntimeStatistic("Fills", str(self._n_fills))
self.SetRuntimeStatistic("MaxVsOpen_MOO_pct", f"{self._max_moo*100:.4f}")
self.SetRuntimeStatistic("nMOO", str(self._n_moo))
self.SetRuntimeStatistic("nLiq", str(self._n_liq))
self.SetRuntimeStatistic("MaxGross_pct", f"{self._max_gross*100:.1f}")