| Overall Statistics |
|
Total Orders 3 Average Win 0% Average Loss -7.96% Compounding Annual Return -99.941% Drawdown 19.500% Expectancy -1 Start Equity 100000 End Equity 90861.4 Net Profit -9.139% Sharpe Ratio 85.322 Sortino Ratio 0 Probabilistic Sharpe Ratio 83.314% Loss Rate 100% Win Rate 0% Profit-Loss Ratio 0 Alpha 37.489 Beta 1.817 Annual Standard Deviation 0.445 Annual Variance 0.198 Information Ratio 180.33 Tracking Error 0.209 Treynor Ratio 20.89 Total Fees $6.45 Estimated Strategy Capacity $460000000.00 Lowest Capacity Asset NQ YQYHC5L1GPA9 Portfolio Turnover 273.48% |
from AlgorithmImports import *
import joblib
import io
import numpy as np
from collections import deque
from sklearn.preprocessing import StandardScaler
from datetime import datetime
class HMMNQRegimeStrategy(QCAlgorithm):
def Initialize(self):
# === SET BACKTEST PERIOD ===
self.SetStartDate(2025, 3, 10) # OUT-OF-SAMPLE (Week after training)
self.SetEndDate(2025, 3, 14)
self.SetCash(100000)
# === TRAINING WINDOW for Model ID (must match QuantBook training period) ===
train_start = datetime(2025, 3, 3)
train_end = datetime(2025, 3, 7)
date_format = "%Y%m%d"
self.model_id = f"hmm_qsl_nq_{train_start.strftime(date_format)}to{train_end.strftime(date_format)}_marco.pkl"
# === SUBSCRIBE TO CONTINUOUS NQ FUTURES ===
future = self.AddFuture(
Futures.Indices.NASDAQ100EMini,
Resolution.Minute,
extendedMarketHours=True,
dataMappingMode=DataMappingMode.OpenInterest,
dataNormalizationMode=DataNormalizationMode.BackwardsRatio,
contractDepthOffset=0
)
future.SetFilter(timedelta(0), timedelta(days=60))
self.future_symbol = future.Symbol
self.current_contract = None
self.previous_contract = None
# === LOAD HMM MODEL + SCALER ===
model_bytes = self.ObjectStore.ReadBytes(self.model_id)
self.model, self.scaler = joblib.load(io.BytesIO(model_bytes))
self.Debug(f"✅ Loaded model: {self.model_id}")
# === ROLLING WINDOWS FOR FEATURE ENGINEERING ===
self.window = deque(maxlen=31)
self.close_window = deque(maxlen=11)
self.high_window = deque(maxlen=1)
self.low_window = deque(maxlen=1)
# Track predictions for diagnostics
self.regime_counts = {0: 0, 1: 0, 2: 0}
self.SetWarmUp(TimeSpan.FromMinutes(31))
def OnData(self, slice: Slice):
if self.IsWarmingUp:
return
# === HANDLE ROLLOVERS ===
for evt in slice.SymbolChangedEvents.Values:
if evt.Symbol == self.future_symbol:
self.previous_contract = evt.OldSymbol
self.current_contract = evt.NewSymbol
self.Debug(f"{self.Time} - Rollover: {self.previous_contract} → {self.current_contract}")
self.HandleRollover()
# === FIRST TIME CONTRACT SELECTION ===
if self.current_contract is None:
if self.future_symbol not in slice.FutureChains:
return
chain = slice.FutureChains[self.future_symbol]
contracts = sorted([c for c in chain if c.Expiry > self.Time], key=lambda c: c.Expiry)
if contracts:
self.current_contract = contracts[0].Symbol
self.Debug(f"{self.Time} - Selected front-month: {self.current_contract}")
else:
self.Debug(f"{self.Time} - No valid contracts in chain")
return
# === GET BAR DATA ===
if not slice.Bars.ContainsKey(self.current_contract):
self.Debug(f"{self.Time} - No bar data for {self.current_contract}")
return
bar = slice.Bars[self.current_contract]
close = bar.Close
high = bar.High
low = bar.Low
# === UPDATE ROLLING WINDOWS ===
self.window.append(close)
self.close_window.append(close)
self.high_window.append(high)
self.low_window.append(low)
if len(self.window) < 31:
return
# === FEATURE ENGINEERING ===
log_ret = np.log(self.window[-1] / self.window[-2])
log_returns = np.log(np.array(self.window)[1:] / np.array(self.window)[:-1])
vol = np.std(log_returns)
slope = (self.close_window[-1] - self.close_window[0]) / 10
range_pct = (self.high_window[-1] - self.low_window[-1]) / self.close_window[-1]
x = np.array([[log_ret, vol, slope, range_pct]])
x_scaled = self.scaler.transform(x)
regime = self.model.predict(x_scaled)[0]
self.regime_counts[regime] += 1
self.Debug(f"{self.Time} - Regime: {regime}")
# === STRATEGY LOGIC: TRADE IN REGIME 0 ONLY ===
if regime == 0:
if not self.Portfolio[self.current_contract].Invested:
self.MarketOrder(self.current_contract, 1)
self.Debug(f"{self.Time} - Entered LONG (Regime 0)")
else:
if self.Portfolio[self.current_contract].Invested:
self.Liquidate(self.current_contract)
self.Debug(f"{self.Time} - Exited position (Regime {regime})")
def HandleRollover(self):
if self.Portfolio[self.previous_contract].Invested:
qty = self.Portfolio[self.previous_contract].Quantity
self.Liquidate(self.previous_contract)
self.MarketOrder(self.current_contract, qty)
self.Debug(f"{self.Time} - Rolled over: {qty} → {self.current_contract}")