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)