Overall Statistics
Total Orders
4188
Average Win
0.29%
Average Loss
-0.19%
Compounding Annual Return
7.005%
Drawdown
11.400%
Expectancy
0.107
Start Equity
100000
End Equity
155702
Net Profit
55.702%
Sharpe Ratio
0.305
Sortino Ratio
0.124
Probabilistic Sharpe Ratio
22.855%
Loss Rate
56%
Win Rate
44%
Profit-Loss Ratio
1.51
Alpha
0.016
Beta
0.049
Annual Standard Deviation
0.069
Annual Variance
0.005
Information Ratio
-0.451
Tracking Error
0.174
Treynor Ratio
0.43
Total Fees
$2094.00
Estimated Strategy Capacity
$2000.00
Lowest Capacity Asset
SPXW 32T3VOCQH146M|SPX 31
Portfolio Turnover
0.14%
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from AlgorithmImports import *

class IndexOptionOpeningRangeBreakoutAlgorithm(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2019, 1, 1)
        # self.set_end_date(2020, 2, 1)
        self.set_cash(100000)
        self.set_security_initializer(lambda security: security.set_market_price(self.get_last_known_price(security)))

        index = self.add_index("SPX", Resolution.MINUTE).symbol
        option = self.add_index_option(index, "SPXW", Resolution.MINUTE)
        option.set_filter(lambda x: x.include_weeklys().strikes(-50, 10).expiration(0, 0))
        
        self.spxw = option.symbol
        self.spx = index
        self.tickets = []
        
        # Opening range variables
        self.opening_range_minutes = 60  # Use 60-minute opening range
        self.opening_range_high = None
        self.opening_range_low = None
        self.range_established = False
        self.breakout_traded = False
        self.daily_reset_done = False
        
        # Minimum range threshold (0.2% of underlying price)
        self.min_range_threshold = 0.002
        self.fomc_dates = self.get_fomc_dates()

    def on_data(self, slice: Slice) -> None:
        current_time = self.time

        if current_time.strftime("%Y-%m-%d") in self.fomc_dates:
            return


        # Reset daily variables at market open
        if current_time.hour == 9 and current_time.minute == 31 and not self.daily_reset_done:
            self.opening_range_high = None
            self.opening_range_low = None
            self.range_established = False
            self.breakout_traded = False
            self.daily_reset_done = True
            
        # Reset flag at end of day
        if current_time.hour == 16:
            self.daily_reset_done = False
            
        # Get current SPX price
        if self.spx not in slice or slice[self.spx] is None:
            return
            
        current_price = slice.bars[self.spx].close
        
        # Establish opening range during first 60 minutes
        if (current_time.hour == 9 and current_time.minute >= 30) or \
           (current_time.hour == 10 and current_time.minute < 31):
            
            if self.opening_range_high is None:
                self.opening_range_high = current_price
                self.opening_range_low = current_price
            else:
                self.opening_range_high = max(self.opening_range_high, current_price)
                self.opening_range_low = min(self.opening_range_low, current_price)
                
        # Mark range as established at 10:30 AM
        if current_time.hour == 10 and current_time.minute == 31 and not self.range_established:
            self.range_established = True
            
            # Check if range meets minimum threshold
            if self.opening_range_high and self.opening_range_low:
                range_size = self.opening_range_high - self.opening_range_low
                min_range_required = current_price * self.min_range_threshold
                
                if range_size < min_range_required:
                    self.log(f"Range too small: {range_size:.2f} < {min_range_required:.2f}")
                    return
                    
                self.log(f"Opening range established: {self.opening_range_low:.2f} - {self.opening_range_high:.2f}")
        
        # Only trade after range is established and before noon
        if not self.range_established or self.breakout_traded or current_time.hour >= 12:
            return
            
        # Return if open position exists
        if any([self.portfolio[x.symbol].invested for x in self.tickets]):
            return
            
        # Check for breakout signals
        upside_breakout = current_price > self.opening_range_high
        downside_breakout = current_price < self.opening_range_low
        
        if upside_breakout:
            self.log(f"Upside breakout detected: {current_price:.2f} > {self.opening_range_high:.2f}")
        elif downside_breakout:
            self.log(f"Downside breakout detected: {current_price:.2f} < {self.opening_range_low:.2f}")
        
        if not (upside_breakout or downside_breakout):
            return
            
        # Get option chain
        chain = slice.option_chains.get(self.spxw)
        if not chain:
            self.log("No option chain available")
            return

        # Get the nearest expiry date of the contracts
        expiry = min([x.expiry for x in chain])
        self.log(f"Using expiry: {expiry}, Chain has {len(chain)} contracts")
        
        if upside_breakout:
            # Sell short put spread (bullish breakout)
            # Short strike at opening range low
            puts = sorted([i for i in chain if i.expiry == expiry and i.right == OptionRight.PUT], 
                         key=lambda x: x.strike)
            if len(puts) < 2:
                self.log(f"Insufficient puts available: {len(puts)}")
                return
                
            self.log(f"Available put strikes: {[p.strike for p in puts[:5]]}...{[p.strike for p in puts[-5:]]}")
            
            # Find puts near opening range low for short strike
            short_put = min(puts, key=lambda x: abs(x.strike - self.opening_range_low))
            self.log(f"Selected short put strike: {short_put.strike} (target: {self.opening_range_low:.2f})")
            
            # Find put $15 below for long strike
            long_put = None
            for put in puts:
                if put.strike <= short_put.strike - 15:
                    long_put = put
                    break
                    
            if long_put:
                put_spread = OptionStrategies.bull_put_spread(self.spxw, short_put.strike, long_put.strike, expiry)
                self.tickets = self.buy(put_spread, 1)
                self.log(f"Upside breakout: Sold put spread {long_put.strike}/{short_put.strike}")
                self.breakout_traded = True
            else:
                self.log(f"No long put found $15 below short strike {short_put.strike}")
                
        elif downside_breakout:
            # Sell short call spread (bearish breakout)
            # Short strike at opening range high
            calls = sorted([i for i in chain if i.expiry == expiry and i.right == OptionRight.CALL], 
                          key=lambda x: x.strike)
            if len(calls) < 2:
                self.log(f"Insufficient calls available: {len(calls)}")
                return
                
            self.log(f"Available call strikes: {[c.strike for c in calls[:5]]}...{[c.strike for c in calls[-5:]]}")
            
            # Find calls near opening range high for short strike
            short_call = min(calls, key=lambda x: abs(x.strike - self.opening_range_high))
            self.log(f"Selected short call strike: {short_call.strike} (target: {self.opening_range_high:.2f})")
            
            # Find call $15 above for long strike
            long_call = None
            for call in calls:
                if call.strike >= short_call.strike + 15:
                    long_call = call
                    break
                    
            if long_call:
                call_spread = OptionStrategies.bear_call_spread(self.spxw, short_call.strike, long_call.strike, expiry)
                self.tickets = self.buy(call_spread, 1)
                self.log(f"Downside breakout: Sold call spread {short_call.strike}/{long_call.strike}")
                self.breakout_traded = True
            else:
                self.log(f"No long call found $15 above short strike {short_call.strike}")# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.

    def get_fomc_dates(self):
        return {
            "2025-06-18", "2025-05-07", "2025-03-19", "2025-01-29",
            "2024-12-18", "2024-11-07", "2024-09-18", "2024-07-31",
            "2024-06-12", "2024-05-01", "2024-03-20", "2024-01-31",
            "2023-12-13", "2023-11-01", "2023-09-20", "2023-07-26",
            "2023-06-14", "2023-05-03", "2023-03-22", "2023-02-01",
            "2022-12-14", "2022-11-02", "2022-09-21", "2022-07-27",
            "2022-06-15", "2022-05-04", "2022-03-16", "2021-12-15",
            "2021-11-03", "2021-09-22", "2021-07-28", "2021-06-16",
            "2021-04-28", "2021-03-17", "2021-01-27", "2020-12-16",
            "2020-11-05", "2020-09-16", "2020-07-29", "2020-06-10",
            "2020-04-29", "2020-03-31", "2020-03-23", "2020-03-19",
            "2020-03-15", "2020-03-03", "2019-10-30", "2019-09-18",
            "2019-07-31", "2018-12-19", "2018-09-26", "2018-06-13",
            "2018-03-21", "2017-12-13", "2017-06-14", "2017-03-15",
            "2016-12-14", "2015-12-16", "2011-06-22", "2008-12-16",
            "2008-10-29", "2008-10-08", "2008-09-16", "2008-08-05",
            "2008-04-30", "2008-03-18", "2008-03-16", "2008-01-30",
            "2008-01-22", "2007-12-11", "2007-10-31", "2007-09-18",
            "2007-08-17", "2007-08-07", "2007-06-28", "2007-05-09",
            "2007-03-21", "2007-01-31", "2006-12-12", "2006-10-25",
            "2006-09-20", "2006-08-08", "2006-06-29", "2006-05-10",
            "2006-03-28", "2006-01-31", "2005-12-13", "2005-08-09",
            "2005-06-30", "2005-05-03", "2005-03-22", "2005-02-02",
            "2004-12-14", "2004-11-10", "2004-09-21", "2004-08-10",
            "2004-06-30", "2004-05-04", "2004-03-16", "2004-01-28",
            "2003-12-09", "2003-10-28", "2003-09-16", "2003-08-12",
            "2003-06-25", "2003-05-06", "2003-03-18", "2003-01-29",
            "2002-12-10", "2002-11-06", "2002-09-24", "2002-08-13",
            "2002-06-26", "2002-05-07", "2002-03-19", "2002-01-30",
            "2001-12-11", "2001-11-06", "2001-10-02", "2001-09-17",
            "2001-08-21", "2001-06-27", "2001-05-15", "2001-04-18",
            "2001-03-20", "2001-01-31", "2001-01-03", "2000-12-19",
            "2000-11-15", "2000-10-03", "2000-08-22", "2000-06-28",
            "2000-05-16", "2000-03-21", "2000-02-02"
        }