| Overall Statistics |
|
Total Orders 4933 Average Win 0.35% Average Loss -0.55% Compounding Annual Return 32.628% Drawdown 30.100% Expectancy 0.326 Start Equity 1000000 End Equity 71313091.44 Net Profit 7031.309% Sharpe Ratio 0.929 Sortino Ratio 0.85 Probabilistic Sharpe Ratio 32.444% Loss Rate 19% Win Rate 81% Profit-Loss Ratio 0.63 Alpha 0 Beta 0 Annual Standard Deviation 0.252 Annual Variance 0.064 Information Ratio 1.011 Tracking Error 0.252 Treynor Ratio 0 Total Fees $0.00 Estimated Strategy Capacity $81000000.00 Lowest Capacity Asset BGU U7EC123NWZTX Portfolio Turnover 9.63% Drawdown Recovery 458 |
# region imports
from AlgorithmImports import *
import numpy as np
import pandas as pd
import math
# endregion
class RiskOffRocket(QCAlgorithm):
"""
Trades SPXL and TQQQ as separate competing sleeves using Zephyr-style
conviction + persistence + vol targeting.
Bond regime ONLY gates participation.
Excess risk always goes to SHV.
"""
def Initialize(self):
self.SetStartDate(2011, 1, 1)
self.SetCash(1_000_000)
self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.CASH)
self.SetSecurityInitializer(lambda s: s.SetFeeModel(ConstantFeeModel(0)))
# -----------------------------
# Regime params (UNCHANGED)
# -----------------------------
self.hold_bars = 0
self.lb_short = 3
self.lb_mid = 12
self.lb_long = 84
self.risk_off_hold_counter = 0
self.is_risk_off = False
self.last_rebalance_date = None
# -----------------------------
# Zephyr-style sizing params
# -----------------------------
self.winrate_lookback = 63
self.vol_lookback = 21
self.group_vol_target = 0.50 # annualized target per sleeve
# -----------------------------
# Signal assets (bond regime)
# -----------------------------
self.sym_vgit = self.AddEquity("VGIT", Resolution.Daily).Symbol
self.sym_vcit = self.AddEquity("VCIT", Resolution.Daily).Symbol
self.sym_hyg = self.AddEquity("HYG", Resolution.Daily).Symbol
# -----------------------------
# Trade assets (sleeves)
# -----------------------------
self.sym_spxl = self.AddEquity("SPXL", Resolution.Daily).Symbol
self.sym_tqqq = self.AddEquity("TQQQ", Resolution.Daily).Symbol
self.sym_shv = self.AddEquity(
"SHV",
Resolution.Daily,
dataNormalizationMode=DataNormalizationMode.TOTAL_RETURN
).Symbol
# -----------------------------
# Warmup
# -----------------------------
self.SetWarmUp(self.lb_long + self.vol_lookback + 5, Resolution.Daily)
# -----------------------------
# Schedule
# -----------------------------
self.Schedule.On(
self.DateRules.EveryDay(self.sym_spxl),
self.TimeRules.BeforeMarketClose(self.sym_spxl, 5),
self.Rebalance
)
# ==================================================
# History helpers
# ==================================================
def _history_closes(self, symbols, bars, norm_mode):
hist = self.History(
symbols,
bars,
Resolution.Daily,
dataNormalizationMode=norm_mode
)
if hist.empty:
return None
return hist["close"].unstack(0).dropna(how="all")
# ==================================================
# Bond regime (UNCHANGED, Pine-exact)
# ==================================================
def _avg_cum_mom_last(self, symbol):
bars = self.lb_long + 1
closes = self._history_closes([symbol], bars, norm_mode=DataNormalizationMode.SCALED_RAW)
# closes = self._history_closes([symbol], bars)
if closes is None or symbol not in closes:
return np.nan
px = closes[symbol]
try:
r10 = px.iloc[-1] / px.iloc[-(self.lb_short + 1)] - 1
r21 = px.iloc[-1] / px.iloc[-(self.lb_mid + 1)] - 1
r63 = px.iloc[-1] / px.iloc[-(self.lb_long + 1)] - 1
return (r10 + r21 + r63) / 3.0
except:
return np.nan
def _compute_regime_pine_exact(self):
mT = self._avg_cum_mom_last(self.sym_vgit)
mI = self._avg_cum_mom_last(self.sym_vcit)
mH = self._avg_cum_mom_last(self.sym_hyg)
if any(math.isnan(x) for x in [mT, mI, mH]):
return None
hyHardRiskOff = (mH <= 0)
treasAboveBoth = (mT > mI) and (mT > mH)
riskNow = hyHardRiskOff or treasAboveBoth
if riskNow:
self.risk_off_hold_counter = self.hold_bars
elif self.risk_off_hold_counter > 0:
self.risk_off_hold_counter -= 1
self.is_risk_off = riskNow or (self.risk_off_hold_counter > 0)
reason = (
"HY<0" if hyHardRiskOff else
"VGIT>VCIT&HYG" if treasAboveBoth else
"HOLD" if self.risk_off_hold_counter > 0 else
"—"
)
return self.is_risk_off, riskNow, reason, mT, mI, mH, self.risk_off_hold_counter
# ==================================================
# Zephyr-style group momentum
# ==================================================
def _compute_group_momentum(self, symbol, closes):
if symbol not in closes:
return 0.0
px = closes[symbol]
if len(px) < 253:
return 0.0
return float(np.mean([
px.iloc[-1] / px.iloc[-22] - 1,
px.iloc[-1] / px.iloc[-64] - 1,
px.iloc[-1] / px.iloc[-127] - 1,
px.iloc[-1] / px.iloc[-190] - 1,
px.iloc[-1] / px.iloc[-253] - 1,
]))
# ==================================================
# Rebalance
# ==================================================
def Rebalance(self):
if self.IsWarmingUp:
return
if self.last_rebalance_date == self.Time.date():
return
self.last_rebalance_date = self.Time.date()
out = self._compute_regime_pine_exact()
if out is None:
return
isRiskOff, riskNow, reason, mT, mI, mH, hold_left = out
# -----------------------------
# Risk OFF → 100% SHV
# -----------------------------
if isRiskOff:
self.SetHoldings(self.sym_spxl, 0.0)
self.SetHoldings(self.sym_tqqq, 0.0)
self.SetHoldings(self.sym_shv, 1.0)
return
# -----------------------------
# Zephyr-style sizing block
# -----------------------------
risk_groups = {
"spxl": self.sym_spxl,
"tqqq": self.sym_tqqq
}
closes = self._history_closes(
list(risk_groups.values()),
self.vol_lookback + 253,
norm_mode=DataNormalizationMode.TOTAL_RETURN
)
if closes is None:
return
edges, vols = {}, {}
for g, sym in risk_groups.items():
if sym not in closes:
continue
g_rets = closes[sym].pct_change().dropna()
if len(g_rets) < self.winrate_lookback:
continue
log_group = np.log1p(g_rets)
p_win = float(np.mean(log_group.tail(self.winrate_lookback) > 0))
group_mom = self._compute_group_momentum(sym, closes)
if not np.isfinite(group_mom) or group_mom <= 0:
continue
g_vol = float(
np.std(log_group.tail(self.vol_lookback)) * np.sqrt(252)
)
if not np.isfinite(g_vol) or g_vol <= 0:
continue
vols[g] = g_vol
# Pattern Match: Confidence calculation
confidence = group_mom / (g_vol + 1e-6)
edge = p_win * (1.0 + confidence)
edges[g] = edge
if not edges:
self.SetHoldings(self.sym_shv, 1.0)
return
# -----------------------------
# Normalize + vol targeting
# -----------------------------
edge_eff = {g: e + 0.01 for g, e in edges.items()}
total_edge = sum(edge_eff.values())
w_raw = {g: e / total_edge for g, e in edge_eff.items()}
w_scaled = {
g: w_raw[g] * min(1.0, self.group_vol_target / vols[g])
for g in w_raw
}
risk_weight = sum(w_scaled.values())
cash_weight = max(0.0, 1.0 - risk_weight)
# -----------------------------
# Allocate
# -----------------------------
self.SetHoldings(self.sym_spxl, w_scaled.get("spxl", 0.0))
self.SetHoldings(self.sym_tqqq, w_scaled.get("tqqq", 0.0))
self.SetHoldings(self.sym_shv, cash_weight)
# -----------------------------
# Debug
# -----------------------------
self.Debug(
f"{self.Time.date()} "
f"riskNow={int(riskNow)} isRiskOff={int(isRiskOff)} reason={reason} "
f"SPXL={w_scaled.get('spxl',0):.3f} "
f"TQQQ={w_scaled.get('tqqq',0):.3f} "
f"CASH={cash_weight:.3f}"
)