| 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"
}