Overall Statistics
Total Orders
789
Average Win
4.32%
Average Loss
-1.95%
Compounding Annual Return
289.180%
Drawdown
59.900%
Expectancy
0.108
Start Equity
500000
End Equity
799145.38
Net Profit
59.829%
Sharpe Ratio
3.285
Sortino Ratio
5.086
Probabilistic Sharpe Ratio
60.831%
Loss Rate
66%
Win Rate
34%
Profit-Loss Ratio
2.21
Alpha
3.794
Beta
0.4
Annual Standard Deviation
1.141
Annual Variance
1.302
Information Ratio
3.371
Tracking Error
1.146
Treynor Ratio
9.36
Total Fees
$60897.96
Estimated Strategy Capacity
$920000.00
Lowest Capacity Asset
UVXY V0H08FY38ZFP
Portfolio Turnover
472.99%
Drawdown Recovery
13
from AlgorithmImports import *

class OpeningRangeBreakoutStocksInPlay(QCAlgorithm):
    
    def Initialize(self):
        # Configurable parameters
        self.START_YEAR = 2025
        self.START_MONTH = 1
        self.START_DAY = 1
        self.END_YEAR = 2025
        self.END_MONTH = 8
        self.END_DAY = 1
        self.INITIAL_CASH = 500_000
        
        self.MIN_PRICE = 5
        self.MIN_DOLLAR_VOLUME = 50_000_000
        self.TOP_COARSE_SYMBOLS = 100
        
        self.MIN_MARKET_CAP = 2_000_000_000
        self.TOP_FINE_SYMBOLS = 1
        
        self.WARMUP_DAYS = 20
        self.WARMUP_RESOLUTION = Resolution.Daily
        
        self.LIQUIDATION_MINUTES_BEFORE_CLOSE = 10
        self.MARKET_SYMBOL = "SPY"
        
        self.OR_START_HOUR = 9
        self.OR_START_MINUTE = 30
        self.OR_END_MINUTE = 35
        
        self.TRADE_START_HOUR = 9
        self.TRADE_START_MINUTE = 35
        
        self.POSITION_SIZE = 0.95
        self.STOP_LOSS_PCT = 0.02  # 2% stop-loss
        self.TAKE_PROFIT_PCT = 1.00  # 100% take-profit (note: original comment said 2%, but code used equivalent of 100%)

        self.SetStartDate(self.START_YEAR, self.START_MONTH, self.START_DAY)
        self.SetEndDate(self.END_YEAR, self.END_MONTH, self.END_DAY)
        self.SetCash(self.INITIAL_CASH)
        
        # Universe selection for Stocks in Play: Use coarse for high dollar volume, fine for market cap filter
        self.UniverseSettings.Resolution = Resolution.Minute
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        
        # Add VIX for volatility check (data only)
        self.vix = self.AddIndex("VIX", Resolution.Minute).Symbol
        
        # Add tradable VIX ETF (UVXY)
        self.AddEquity("UVXY", Resolution.Minute)
        
        # Store dollar volume from coarse for sorting in fine
        self.dollar_vol = {}
        
        # Warm up for fundamentals
        self.SetWarmUp(self.WARMUP_DAYS, self.WARMUP_RESOLUTION)
        
        # Schedule end-of-day liquidation
        self.Schedule.On(self.DateRules.EveryDay(), 
                         self.TimeRules.BeforeMarketClose(self.MARKET_SYMBOL, self.LIQUIDATION_MINUTES_BEFORE_CLOSE), 
                         self.LiquidateAll)
        
        # Track OR high/low per symbol, entry prices for stops/profits
        self.or_high = {}
        self.or_low = {}
        self.entry_prices = {}
    
    def CoarseSelectionFunction(self, coarse):
        # Filter: Price > MIN_PRICE (avoid low-priced stocks), DollarVolume > MIN_DOLLAR_VOLUME (high activity/liquidity), HasFundamentalData
        filtered = [c for c in coarse if c.Price > self.MIN_PRICE and c.DollarVolume > self.MIN_DOLLAR_VOLUME and c.HasFundamentalData]
        
        # Sort by DollarVolume descending, take top TOP_COARSE_SYMBOLS for fine processing
        sorted_by_dollar_vol = sorted(filtered, key=lambda c: c.DollarVolume, reverse=True)[:self.TOP_COARSE_SYMBOLS]
        
        # Store dollar volume for fine sorting
        self.dollar_vol = {c.Symbol: c.DollarVolume for c in sorted_by_dollar_vol}
        
        return [c.Symbol for c in sorted_by_dollar_vol]
    
    def FineSelectionFunction(self, fine):
        # Filter: MarketCap > MIN_MARKET_CAP (avoid low cap)
        filtered_fine = [f for f in fine if f.MarketCap > self.MIN_MARKET_CAP]
        
        # Sort by stored DollarVolume descending, take top TOP_FINE_SYMBOLS
        sorted_fine = sorted(filtered_fine, key=lambda f: self.dollar_vol.get(f.Symbol, 0), reverse=True)[:self.TOP_FINE_SYMBOLS]
        
        return [f.Symbol for f in sorted_fine]
    
    def OnData(self, data):
        if not self.IsMarketOpen(self.MARKET_SYMBOL): return  # US equities hours
        
        # Get current VIX value for volatility check
        if self.vix in data.Bars:
            vix_close = data.Bars[self.vix].Close
            # Dynamically adjust position size: halve in high vol (VIX > 30)
            current_position_size = self.POSITION_SIZE / 2 if vix_close > 30 else self.POSITION_SIZE
        else:
            current_position_size = self.POSITION_SIZE  # Default if VIX data unavailable
        
        for symbol in list(self.UniverseManager.ActiveSecurities.Keys):
            if symbol not in data.Bars: continue
            if symbol == self.vix: continue  # Skip VIX, use only for data
            
            bar = data.Bars[symbol]
            time = self.Time.time()
            
            # Calculate 5-min OR: From open (9:30) to 9:35 ET
            if time.hour == self.OR_START_HOUR and time.minute >= self.OR_START_MINUTE and time.minute < self.OR_END_MINUTE:
                if symbol not in self.or_high:
                    self.or_high[symbol] = bar.High
                    self.or_low[symbol] = bar.Low
                else:
                    self.or_high[symbol] = max(self.or_high[symbol], bar.High)
                    self.or_low[symbol] = min(self.or_low[symbol], bar.Low)
            
            # After 9:35, check for upside breakout if not invested
            elif time.hour >= self.TRADE_START_HOUR and time.minute >= self.TRADE_START_MINUTE and not self.Portfolio[symbol].Invested:
                or_high = self.or_high.get(symbol, 0)
                or_low = self.or_low.get(symbol, 0)
                
                # Common margin check logic (using dynamic size)
                portfolio_value = self.Portfolio.TotalPortfolioValue
                approx_order_value = current_position_size * portfolio_value
                approx_required_margin = approx_order_value * 0.5  # Assuming 50% initial margin for 2x leverage
                
                if self.Portfolio.MarginRemaining >= approx_required_margin:
                    if bar.Close > or_high:
                        self.SetHoldings(symbol, current_position_size) 
                        self.entry_prices[symbol] = bar.Close
                        self.Debug(f"Buy {symbol} at {bar.Close} on breakout")
                    elif bar.Close < or_low:
                        self.SetHoldings(symbol, -current_position_size) 
                        self.entry_prices[symbol] = bar.Close
                        self.Debug(f"Short {symbol} at {bar.Close} on breakdown")
            
            # Manage exits: stop-loss and take-profit based on position direction
            if self.Portfolio[symbol].Invested:
                entry = self.entry_prices.get(symbol, 0)
                if self.Portfolio[symbol].IsLong:
                    if bar.Close <= entry * (1 - self.STOP_LOSS_PCT):  # Stop-loss
                        self.Liquidate(symbol)
                        self.Debug(f"Stop-loss {symbol} at {bar.Close}")
                    elif bar.Close >= entry * (1 + self.TAKE_PROFIT_PCT):  # Take-profit
                        self.Liquidate(symbol)
                        self.Debug(f"Take-profit {symbol} at {bar.Close}")
                elif self.Portfolio[symbol].IsShort:
                    if bar.Close >= entry * (1 + self.STOP_LOSS_PCT):  # Stop-loss
                        self.Liquidate(symbol)
                        self.Debug(f"Stop-loss {symbol} at {bar.Close}")
                    elif bar.Close <= entry * (1 - self.TAKE_PROFIT_PCT):  # Take-profit
                        self.Liquidate(symbol)
                        self.Debug(f"Take-profit {symbol} at {bar.Close}")
    
    def LiquidateAll(self):
        self.Liquidate()
        self.Debug("End-of-day liquidation")
        # Reset for next day
        self.or_high.clear()
        self.or_low.clear()
        self.entry_prices.clear()