| Overall Statistics |
|
Total Orders 1535 Average Win 0.79% Average Loss -0.55% Compounding Annual Return 16.491% Drawdown 15.500% Expectancy 0.152 Start Equity 10000 End Equity 18404.71 Net Profit 84.047% Sharpe Ratio 0.78 Sortino Ratio 1.096 Probabilistic Sharpe Ratio 37.927% Loss Rate 53% Win Rate 47% Profit-Loss Ratio 1.43 Alpha 0 Beta 0 Annual Standard Deviation 0.129 Annual Variance 0.017 Information Ratio 0.934 Tracking Error 0.129 Treynor Ratio 0 Total Fees $1542.26 Estimated Strategy Capacity $53000000.00 Lowest Capacity Asset AMD R735QTJ8XC9X Portfolio Turnover 77.91% Drawdown Recovery 574 |
# `gap-orb` Algorithm Implementation
# Version: 4.0 (Final, Corrected for Optimization)
# Author: OC & Claude (Generated for pinjoy)
# Date: 2026-03-16
# Description: This version correctly parameterizes the stable v2.4 code
# for optimization without altering the class constructors.
from AlgorithmImports import *
from scipy.stats import linregress
from datetime import time, timedelta, datetime
# ==============================================================================
# 1. Main Algorithm Class
# ==============================================================================
class GapOrbOptimization(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2020, 1, 1)
self.SetEndDate(2023, 12, 31)
self.SetCash(10000)
self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)
self.SetSecurityInitializer(self.CustomSecurityInitializer)
# --- CORRECTED PARAMETERIZATION ---
volatility_window = int(self.GetParameter("volatility_window", 90))
orb_minutes = int(self.GetParameter("orb_minutes", 10)) # Using 10min as the new baseline
gap_z_score = float(self.GetParameter("gap_z_score", 0.05)) # Using 0.05 as the new baseline
volume_multiplier = float(self.GetParameter("volume_multiplier", 1.25)) # Using 1.25 as the new baseline
drawdown_percent = float(self.GetParameter("maximumDrawdownPercent", 0.012))
self.SetWarmUp(timedelta(days=volatility_window + 10))
tech_tickers = ["TSLA", "NVDA", "AMD", "AAPL", "MSFT"]
for ticker in tech_tickers:
self.AddEquity(ticker, Resolution.Minute)
# The AlphaModel constructor takes the parameters directly.
self.SetAlpha(GapOrbAlphaModel(
volatility_window,
orb_minutes,
gap_z_score,
volume_multiplier
))
self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
self.SetRiskManagement(TrailingStopRiskManagementModel(maximumDrawdownPercent=drawdown_percent))
self.SetExecution(ImmediateExecutionModel())
def CustomSecurityInitializer(self, security: Security):
security.SetDataNormalizationMode(DataNormalizationMode.Raw)
security.SetSlippageModel(VolumeShareSlippageModel())
def OnOrderEvent(self, orderEvent: OrderEvent):
if orderEvent.Status != OrderStatus.Filled: return
order = self.Transactions.GetOrderById(orderEvent.OrderId)
if order is None: return
if not self.Portfolio[order.Symbol].Invested:
all_insights = self.Insights.GetInsights()
active_insights = [i for i in all_insights if i.Symbol == order.Symbol and i.IsActive(self.UtcTime) and i.Direction != InsightDirection.Flat]
if any(active_insights):
self.Log(f"RISK MODEL CLOSURE DETECTED for {order.Symbol}. Cancelling active insights to prevent re-entry.")
self.Insights.Cancel(active_insights)
# ==============================================================================
# 2. Alpha Model Class
# ==============================================================================
class GapOrbAlphaModel(AlphaModel):
# Constructor uses default values which are passed from Initialize
def __init__(self,
volatility_window: int = 90,
orb_minutes: int = 10,
gap_z_score: float = 0.05,
volume_multiplier: float = 1.25):
self.volatility_window = volatility_window
self.orb_minutes = orb_minutes
self.gap_z_score = gap_z_score
self.volume_multiplier = volume_multiplier
self.symbol_data = {}
def Update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
insights = []
if algorithm.IsWarmingUp: return insights
for symbol, sd in self.symbol_data.items():
signal = sd.Update(algorithm, data)
if signal != InsightDirection.Flat:
algorithm.Log(f"INSIGHT GENERATED: Symbol={symbol.Value}, Direction={signal}, Time={algorithm.Time}")
confidence = 1.0
insight_duration = algorithm.Time.replace(hour=16, minute=0) - algorithm.Time
insights.append(Insight.Price(symbol, insight_duration, signal, confidence=confidence))
return insights
def OnSecuritiesChanged(self, algorithm: QCAlgorithm, changes: SecurityChanges):
for added in changes.AddedSecurities:
if added.Symbol not in self.symbol_data:
# The original, working dependency injection pattern
self.symbol_data[added.Symbol] = SymbolData(added.Symbol, self, algorithm)
for removed in changes.RemovedSecurities:
if removed.Symbol in self.symbol_data:
self.symbol_data[removed.Symbol].CleanUp()
del self.symbol_data[removed.Symbol]
# ==============================================================================
# 3. SymbolData Class
# ==============================================================================
class SymbolData:
def __init__(self, symbol: Symbol, alpha_model: GapOrbAlphaModel, algorithm: QCAlgorithm):
self.symbol = symbol
self.algorithm = algorithm
self.orb_minutes = alpha_model.orb_minutes
self.gap_z_score = alpha_model.gap_z_score
self.volume_multiplier = alpha_model.volume_multiplier
self.daily_return_indicator = RateOfChange(1)
self.volatility_indicator = StandardDeviation(alpha_model.volatility_window)
self.daily_consolidator = TradeBarConsolidator(timedelta(days=1))
self.daily_consolidator.DataConsolidated += self.OnDailyData
self.algorithm.SubscriptionManager.AddConsolidator(symbol, self.daily_consolidator)
self.prev_daily_high = 0
self.prev_daily_low = 0
self.last_update_date = None
self.ResetDailyState()
def ResetDailyState(self):
self.is_gap_day = False
self.orb_high = 0
self.orb_low = float('inf')
self.orb_slope_data = []
self.orb_volume = 0
self.orb_slope = 0
self.orb_calculated = False
self.is_breakout_found_today = False
def OnDailyData(self, sender, consolidated_bar: TradeBar):
self.daily_return_indicator.Update(consolidated_bar.Time, consolidated_bar.Close)
if self.daily_return_indicator.IsReady:
self.volatility_indicator.Update(self.daily_return_indicator.Current.Time, self.daily_return_indicator.Current.Value)
self.prev_daily_high = consolidated_bar.High
self.prev_daily_low = consolidated_bar.Low
def Update(self, algorithm: QCAlgorithm, data: Slice) -> InsightDirection:
if not data.Bars.ContainsKey(self.symbol): return InsightDirection.Flat
bar = data.Bars[self.symbol]
if self.last_update_date is not None and algorithm.Time.date() > self.last_update_date:
self.ResetDailyState()
self.last_update_date = algorithm.Time.date()
if algorithm.Time.hour == 9 and algorithm.Time.minute == 31 and self.volatility_indicator.IsReady:
if self.prev_daily_high > 0:
vol = self.volatility_indicator.Current.Value
if vol > 0:
upper_threshold = self.prev_daily_high * (1 + self.gap_z_score * vol)
lower_threshold = self.prev_daily_low * (1 - self.gap_z_score * vol)
if bar.Open > upper_threshold or bar.Open < lower_threshold:
self.is_gap_day = True
self.algorithm.Log(f"GAP DETECTED: {self.symbol.Value} on {algorithm.Time.date()}")
if not self.is_gap_day: return InsightDirection.Flat
current_time_of_day = algorithm.Time.time()
orb_start_time = time(9, 30)
orb_end_datetime = datetime.combine(algorithm.Time.date(), orb_start_time) + timedelta(minutes=self.orb_minutes)
orb_end_time = orb_end_datetime.time()
end_of_trading_time = time(15, 55)
if current_time_of_day >= orb_start_time and current_time_of_day < orb_end_time:
self.orb_high = max(self.orb_high, bar.High)
self.orb_low = min(self.orb_low, bar.Low)
self.orb_slope_data.append(bar.Close)
self.orb_volume += bar.Volume
elif not self.orb_calculated and current_time_of_day >= orb_end_time:
if len(self.orb_slope_data) > 1:
self.orb_slope, _, _, _, _ = linregress(range(len(self.orb_slope_data)), self.orb_slope_data)
self.orb_calculated = True
if self.orb_calculated and current_time_of_day < end_of_trading_time and not self.is_breakout_found_today:
if self.orb_slope == 0: return InsightDirection.Flat
avg_orb_minute_volume = self.orb_volume / self.orb_minutes if self.orb_minutes > 0 else 0
volume_confirmed = avg_orb_minute_volume > 0 and bar.Volume > (avg_orb_minute_volume * self.volume_multiplier)
if not volume_confirmed: return InsightDirection.Flat
if self.orb_slope > 0 and bar.High > self.orb_high:
self.is_breakout_found_today = True
self.algorithm.Log(f"GOLDEN BREAKOUT (UP) on {self.symbol.Value}")
return InsightDirection.Up
elif self.orb_slope < 0 and bar.Low < self.orb_low:
self.is_breakout_found_today = True
self.algorithm.Log(f"GOLDEN BREAKOUT (DOWN) on {self.symbol.Value}")
return InsightDirection.Down
return InsightDirection.Flat
def CleanUp(self):
self.algorithm.SubscriptionManager.RemoveConsolidator(self.symbol, self.daily_consolidator)