Overall Statistics |
Total Trades 133 Average Win 3.70% Average Loss -1.92% Compounding Annual Return -44.872% Drawdown 22.100% Expectancy -0.156 Net Profit -19.348% Sharpe Ratio -0.864 Probabilistic Sharpe Ratio 7.807% Loss Rate 71% Win Rate 29% Profit-Loss Ratio 1.93 Alpha -0.236 Beta -0.37 Annual Standard Deviation 0.346 Annual Variance 0.119 Information Ratio -1.216 Tracking Error 0.385 Treynor Ratio 0.808 Total Fees $542.00 Estimated Strategy Capacity $360000.00 Lowest Capacity Asset SPY 326V9O0OPYZJA|SPY R735QTJ8XC9X Portfolio Turnover 3.68% |
# QuantConnect specific imports from AlgorithmImports import * # import QuantConnect as qc ############################################################################### class CustomExerciseModel(DefaultExerciseModel): # How top prevent the excercise from being triggered? def OptionExercise(self, option: Option, order: OptionExerciseOrder): order_event = OrderEvent( order.Id, option.Symbol, Extensions.ConvertToUtc(option.LocalTime, option.Exchange.TimeZone), OrderStatus.Filled, Extensions.GetOrderDirection(order.Quantity), 0.0, order.Quantity, OrderFee.Zero, "Tag" ) order_event.IsAssignment = False return # return [ order_event ]
############################################################################### # Standard library imports import datetime as DT # from dateutil.parser import parse # import decimal # import numpy as np # import pandas as pd # import pytz # from System.Drawing import Color # import traceback # QuantConnect specific imports # import QuantConnect as qc from AlgorithmImports import * # Import from files from notes_and_inputs import * from symbol_data import SymbolData ############################################################################### class CustomTradingStrategy(QCAlgorithm): def Initialize(self): """Initialize algorithm.""" # Set backtest details self.SetBacktestDetails() # Add strategy variables required for the algo self.AddStrategyVariables() # Add instrument data to the algo self.AddInstrumentData() # Schedule functions self.ScheduleFunctions() #------------------------------------------------------------------------------ def SetBacktestDetails(self): """Set the backtest details.""" self.SetStartDate(START_DT.year, START_DT.month, START_DT.day) if END_DATE: self.SetEndDate(END_DT.year, END_DT.month, END_DT.day) self.SetCash(CASH) self.SetTimeZone(TIMEZONE) # Setup trading framework # Transaction and submit/execution rules will use IB models # brokerages: '''https://github.com/QuantConnect/Lean/blob/master/Common/Brokerages /BrokerageName.cs''' # account types: AccountType.Margin, AccountType.Cash self.SetBrokerageModel( BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin ) # Configure all universe securities # This sets the data normalization mode # You can also set custom fee, slippage, fill, and buying power models self.SetSecurityInitializer( CustomSecurityInitializer( self.BrokerageModel, FuncSecuritySeeder(self.GetLastKnownPrices) ) ) #------------------------------------------------------------------------------ def AddStrategyVariables(self): """Create required strategy variables.""" try: self.RISK_PER_TRADE = float(self.GetParameter("RISK_PER_TRADE"))/100.0 except: self.RISK_PER_TRADE = RISK_PER_TRADE try: self.MIN_DAYS_TO_EXPIRY = \ int(self.GetParameter("MIN_DAYS_TO_EXPIRY")) except: self.MIN_DAYS_TO_EXPIRY = MIN_DAYS_TO_EXPIRY try: i = int(self.GetParameter("BAR_MINUTES")) self.BAR_MINUTES = BAR_MINUTES_OPTIONS[i] except: self.BAR_MINUTES = BAR_MINUTES try: self.START_TRADING_MINUTES_AFTER_OPEN = \ int(self.GetParameter("START_TRADING_MINUTES_AFTER_OPEN")) except: self.START_TRADING_MINUTES_AFTER_OPEN = \ START_TRADING_MINUTES_AFTER_OPEN # try: # self.MA_FAST_PERIOD = int(self.GetParameter("MA_FAST_PERIOD")) # except: # self.MA_FAST_PERIOD = MA_FAST_PERIOD # try: # self.MA_SLOW_PERIOD = int(self.GetParameter("MA_SLOW_PERIOD")) # except: # self.MA_SLOW_PERIOD = MA_SLOW_PERIOD try: self.OTM_STRIKES = int(self.GetParameter("OTM_STRIKES")) except: self.OTM_STRIKES = OTM_STRIKES try: self.MIN_TAKE_PROFIT_AMOUNT = \ float(self.GetParameter("MIN_TAKE_PROFIT_AMOUNT")) except: self.MIN_TAKE_PROFIT_AMOUNT = MIN_TAKE_PROFIT_AMOUNT try: self.STOP_LOSS_PERCENT = \ float(self.GetParameter("STOP_LOSS_PERCENT")) except: self.STOP_LOSS_PERCENT = STOP_LOSS_PERCENT try: self.TAKE_PROFIT_PERCENT = \ float(self.GetParameter("TAKE_PROFIT_PERCENT")) except: self.TAKE_PROFIT_PERCENT = TAKE_PROFIT_PERCENT try: self.TRAILING_STOP_PROFIT_TRIGGER = \ float(self.GetParameter("TRAILING_STOP_PROFIT_TRIGGER")) except: self.TRAILING_STOP_PROFIT_TRIGGER = TRAILING_STOP_PROFIT_TRIGGER try: self.TRAILING_STOP_PERCENT = \ float(self.GetParameter("TRAILING_STOP_PERCENT")) except: self.TRAILING_STOP_PERCENT = TRAILING_STOP_PERCENT # # Verify MA periods - slow must be 2x or more fast # if self.MA_SLOW_PERIOD < 2*self.MA_FAST_PERIOD: # raise ValueError( # f"Invalid MA Periods: fast={self.MA_FAST_PERIOD}, " # f"slow={self.MA_SLOW_PERIOD}" # ) #------------------------------------------------------------------------------ def AddInstrumentData(self): """Add instrument data to the algo.""" # Set data resolution based on input if DATA_RESOLUTION == 'SECOND': self.resolution = Resolution.Second self.option_resolution = Resolution.Minute elif DATA_RESOLUTION == 'MINUTE': self.resolution = Resolution.Minute self.option_resolution = Resolution.Minute elif DATA_RESOLUTION == 'HOUR': self.resolution = Resolution.Hour self.option_resolution = Resolution.Hour # Add data for the TICKER self.ticker = self.AddEquity( TICKER, self.resolution, extendedMarketHours=True ).Symbol # Create and save the SymbolData instance self.symbol_data = SymbolData(self, self.ticker) # Save a link to the benchmark and set benchmark self.bm = self.ticker self.SetBenchmark(self.bm) #------------------------------------------------------------------------------ def ScheduleFunctions(self): """Scheduling the functions required by the algo.""" # Schedule benchmark end of day event 5 minutes after the close # Used to plot the benchmark on the equity curve self.Schedule.On( self.DateRules.EveryDay(self.bm), self.TimeRules.BeforeMarketClose(self.bm, -5), self.BenchmarkOnEndOfDay ) #------------------------------------------------------------------------------- def MyLog(self, message): """Add algo time to log if live trading. Otherwise just log message.""" if self.LiveMode: self.Log(f'{self.Time}: {message}') else: self.Log(message) #------------------------------------------------------------------------------ def OnData(self, data): """Event handler for new data pumped into the algo.""" # Check for new dividends or splits for the active securities if data.Dividends.Count > 0 or data.Splits.Count > 0: # Check for a dividend symbol_object = self.symbol_data.symbol_object if data.Dividends.get(symbol_object): # Get the dividend info dividend = data.Dividends[symbol_object].Distribution # Get last 2 daily prices hist = self.History([symbol_object], 2, Resolution.Daily) price = hist.iloc[-1]['close'] # [-1] for last previous_close = hist.iloc[0]['close'] # [0] for first # Calculate the dividend adjustment factor af = (previous_close-dividend)/previous_close # Adjust the SymbolData class indicators self.symbol_data.adjust_indicators(af) ticker = symbol_object.Value self.MyLog( f"Adjusted {ticker} indicators for dividend={dividend}, " f"with an adjustment factor={af}" ) # Check for a split if data.Splits.get(symbol_object): # Make sure the split has occured and not just the warning # split.Type == 0 for warning # split.Type == 1 for split occured if data.Splits[symbol_object].Type == 1: split = data.Splits[symbol_object].SplitFactor # Adjust the SymbolData class indicators self.symbol_data.adjust_indicators(split, split=True) ticker = symbol_object.Value self.MyLog( f"Adjusted {ticker} indicators for split={split}" ) # Check if we are constantly looking for signals if CONSTANT_SIGNAL_CHECK: # Check for trading signals once the algo is no longer warming up # trading is allowed and the indicators are all ready to be used if not self.IsWarmingUp and self.symbol_data.indicators_ready \ and self.symbol_data.trading and data.get(TICKER) is not None: self.symbol_data.check_for_signals() #------------------------------------------------------------------------------ def BenchmarkOnEndOfDay(self): """Event handler for end of trading day for the benchmark.""" self.PlotBenchmarkOnEquityCurve() #------------------------------------------------------------------------------ def OnEndOfAlgorithm(self): """Built-in event handler for end of the backtest.""" # Plot the benchmark buy and hold value on the equity curve chart self.PlotBenchmarkOnEquityCurve(force_plot=True) self.MyLog(f"End of Backtest") #------------------------------------------------------------------------------- def OnOrderEvent(self, orderEvent): """Built-in event handler for orders.""" # Catch option exercise order if 'Automatic Exercise' in orderEvent.Message: # Get the option option = orderEvent.Symbol.Value raise ValueError(f"QC Model automatically exercised {option}") # Skip if not filled if orderEvent.Status != OrderStatus.Filled: if self.LiveMode: self.MyLog(f"New order event: {orderEvent}") return # Call on_order_event for the SymbolData class self.symbol_data.on_order_event(orderEvent) #------------------------------------------------------------------------------ def PlotBenchmarkOnEquityCurve(self, force_plot=False): """Plot the benchmark buy & hold value on the strategy equity chart.""" # Initially set percent change to zero pct_change = 0 # Get today's daily prices # history algo on QC github shows different formats that can be used: '''https://github.com/QuantConnect/Lean/blob/master/Algorithm.Python/ HistoryAlgorithm.py''' hist = self.History([self.bm], timedelta(1), Resolution.Daily) # Make sure hist df is not empty if not hist.empty: # Get today's closing price price = hist.iloc[-1]['close'] try: # Calculate the percent change since the first price pct_change = (price-self.bm_first_price)/self.bm_first_price except: # We have not created the first price variable yet # Get today's open and save as the first price self.bm_first_price = hist.iloc[-1]['open'] # Log the benchmark's first price for reference self.MyLog(f"Benchmark first price = {self.bm_first_price}") # Calculate the percent change since the first price pct_change = (price-self.bm_first_price)/self.bm_first_price # Calculate today's ending value if we have the % change from the start if pct_change != 0: bm_value = round(CASH*(1+pct_change),2) # Plot every PLOT_EVERY_DAYS days try: # We've previously created the counter, so increment it by 1 self.bm_plot_counter += 1 # same as: self.bm_plot_counter = self.bm_plot_counter + 1 except: # We've not created the counter, so set it to 1 self.bm_plot_counter = 1 # Check if it's time to plot the benchmark value if self.bm_plot_counter == PLOT_EVERY_DAYS or force_plot: # Plot the benchmark's value to the Strategy Equity chart # Plot function requires passing the chart name, series name, # then the value to plot self.Plot('Strategy Equity', 'Benchmark', bm_value) # Plot the account leverage account_leverage = self.Portfolio.TotalHoldingsValue \ / self.Portfolio.TotalPortfolioValue self.Plot('Leverage', 'Leverge', account_leverage) # Reset counter to 0 self.bm_plot_counter = 0 # Log benchmark's ending price for reference if force_plot: self.MyLog( f"Benchmark's first price = {self.bm_first_price}" ) self.MyLog(f"Benchmark's final price = {price}") self.MyLog(f"Benchmark buy & hold value = {bm_value}") ############################################################################### class CustomSecurityInitializer(BrokerageModelSecurityInitializer): def __init__( self, brokerage_model: IBrokerageModel, security_seeder: ISecuritySeeder) -> None: super().__init__(brokerage_model, security_seeder) def Initialize(self, security: Security) -> None: """ Define models to be used for securities as they are added to the algorithm's universe. """ # First, call the superclass definition # This method sets the reality models of each security using the # default reality models of the brokerage model super().Initialize(security) # Define the data normalization mode if DATA_MODE == 'RAW': security.SetDataNormalizationMode(DataNormalizationMode.Raw) else: raise ValueError(f"Invalid DATA_MODE: ({DATA_MODE})") # Define the fee model to use for the security # security.SetFeeModel() # Define the slippage model to use for the security # security.SetSlippageModel() # Define the fill model to use for the security # security.SetFillModel() # Define the buying power model to use for the security # security.SetBuyingPowerModel()
from AlgorithmImports import * """ SPY Option Scalping Strategy Using Stochastics Version 1.0.3 Platform: QuantConnect By: Aaron Eller www.excelintrading.com aaron@excelintrading.com Revision Notes: 1.0.0 (04/25/2023) - Initial. 1.0.1 (04/28/2023) - Added take profit inputs and logic. - Added trailing stop inputs and logic. - Added entry and entry filter inputs and logic. - Added special entry at open inputs and logic. - Added SR_TOLERANCE_PERCENT input and logic. - Added ENTRY_AT_EXTREMES input and logic. - Added LATE_ENTRY_HOLD_OVERNIGHT input and logic. 1.0.2 (05/02/2023) - Updated to handle in the money strike selection. - Added logic to not enter if not on a new bar. - Added ENTRY_EXTREME_FILTER_OVERBOUGHT and ENTRY_EXTREME_FILTER_OVERSOLD inputs. 1.0.3 (05/05/2023) - Removed USE_STOP_LOSS as input. Now always True. - Added RISK_PER_TRADE to replace PS_PCT. - Changed ENTRY_CROSSES_AFTER_EXTREME_ZONES to allow entry at the open (9:30) based on the last pre-market session bar. - Added INDICATOR_UPDATE_START_TIME and INDICATOR_UPDATE_STOP_TIME inputs and logic. - Added EXIT_ON_OPEN input and logic. - Added NullOptionAssignmentModel required for holding overnight with options that become deep in the money. Notes: - How to determine / handle "trending" days? - Uses extended market hours for indicators. - Only trades during the regular session. - Consider a max number of trades (or losing trades) per day Issue: -Option excerise model -> makes difficult to trade options deep ITM https://www.quantconnect.com/forum/discussion/14806/avoid-options-automatic-exercise/p1 https://github.com/QuantConnect/Lean/issues/6390 References: -QC (Lean) Class List https://lean-api-docs.netlify.app/annotated.html """ # Standard library imports import datetime as DT ############################################################################### # Backtest inputs START_DATE = "01-01-2023" # must be in "MM-DD-YYYY" format END_DATE = None # must be in "MM-DD-YYYY" format or None CASH = 100000 # starting portfolio value TIMEZONE = "US/Eastern" # e.g. "US/Eastern", "US/Central", "US/Pacific" #------------------------------------------------------------------------------- # DATA INPUTS # Define the data resolution to be fed to the algorithm # Must be "SECOND", "MINUTE", or "HOUR" # NOTE: Will use 'MINUTE' for options if 'SECOND' is selected (for underlying) DATA_RESOLUTION = 'MINUTE' # Model uses raw data normalization mode which is required for options # Set the underlying ticker symbol TICKER = "SPY" # Define the bar to trade (number of minutes) BAR_MINUTES = 30 # can be algorithm parameter / focus on the 15m or larger bars as they look like the most promising # BAR_MINUTES_OPTIONS = [1,2,3,4,5,6,10,12,13,15,20,30] # 12 options # Turn on checking for signals constantly (not every BAR) # If False, will only check for signals once per BAR_MINUTES close # This mostly affects the trailing stop logic CONSTANT_SIGNAL_CHECK = True # Define the higher timeframe bar used for entry filtering # HIGHER_BAR_MINUTES = 60 # 2, 3, 4, 5, 6, 10, 13, 15, 20, 30, 60 # Start trading minutes after the market open START_TRADING_MINUTES_AFTER_OPEN = 0 # can be algorithm parameter # Number of minutes prior to the close to stop trading STOP_TRADING_MINUTES_BEFORE_CLOSE = 0 # Turn on exiting positions at the end of the day USE_EOD_EXIT = False # Set the number of minutes prior to the close to exit positions EOD_EXIT_MINUTES_BEFORE_CLOSE = 5 # Turn on exiting positions at the start of the day (if held overnight) EXIT_ON_OPEN = True # Set the number of minutes prior to the close to exit positions on expiry day EXPIRY_DAY_EXIT_MINUTES_BEFORE_CLOSE = 5 #------------------------------------------------------------------------------- # INDICATOR INPUTS # Set the time to start allowing indicators to update (premarket session) # TradingView premarket starts at 7am ET [DT.time(7,0)] # QuantConnnect premarket data starts at 4am ET [DT.time(4,0)] INDICATOR_UPDATE_START_TIME = DT.time(7,0) # Set the time to stop allowing indicators to update (after hours session) # TradingView after hours session ends at 6pm ET [DT.time(18,0)] # QuantConnnect after hours session data ends at 8pm ET [DT.time(20,0)] INDICATOR_UPDATE_STOP_TIME = DT.time(18,0) # Define the Stochastic STOCHASTIC_PERIOD = 14 STOCHASTIC_KPERIOD = 3 STOCHASTIC_DPERIOD = 3 # Define the oversold and overbought levels OVERSOLD = 20 OVERBOUGHT = 80 # Salty Support/Resistance levels # Uses daily bars ATR_PERIOD = 14 # Define the support/resistance levels to use from previous daily close SR_LEVELS = [ 0.382, 0.5, 0.618, 0.786, 1.0, 1.236, 1.382, 1.5, 1.618, 1.786, 2.0, 2.236, 2.382, 2.5, 2.618, 2.786, 3.0 ] #------------------------------------------------------------------------------- # OPTION INPUTS # Set the target (minimum) number of days to the expiry MIN_DAYS_TO_EXPIRY = 2 # can be algorithm parameter (1 as minimum) # Set the number of strikes above and below from the underlying price # for the options selected # Negative value will be in the money instead of out of the money OTM_STRIKES = -1 # can be algorithm parameter # Turn on/off allowing long trades LONGS_ALLOWED = True # Turn on/off allowing short trades SHORTS_ALLOWED = True #------------------------------------------------------------------------------- # POSITION SIZING INPUTS # Set risk per trade RISK_PER_TRADE = 0.02 # decimal percent, e.g. 0.02=2.0% #------------------------------------------------------------------------------- # ENTRY FILTERS # Turn on/off filtering out entries after extreme moves # Crosses with %K already beyond these levels show price has already # made the initial profitable move. # Require longs to only be valid if %K <= ENTRY_STOCHASTIC_K_LONG_MAX # Require shorts to only be valid if %K >= ENTRY_STOCHASTIC_K_SHORT_MIN ENTRY_NOT_AFTER_EXTREME_MOVE_FILTER = True ENTRY_STOCHASTIC_K_LONG_MAX = 50 ENTRY_STOCHASTIC_K_SHORT_MIN = 50 # Only enter if outside of the oversold/overbought zones? # For long entry, this requires %K to be outside of oversold zone # For short entry, this requires %K to be outside of the overbought zone ENTRY_OUTSIDE_EXTREME_ZONE_FILTER = False ENTRY_EXTREME_FILTER_OVERBOUGHT = 80 ENTRY_EXTREME_FILTER_OVERSOLD = 20 # Do not exit position at end of day if triggered within set minutes of close LATE_ENTRY_HOLD_OVERNIGHT = False LATE_ENTRY_MINUTES_BEFORE_CLOSE = 90 #------------------------------------------------------------------------------- # ENTRY SIGNALS # Turn on/off entries on crossovers after oversold (long) and crossunders after # overbought (short) ENTRY_CROSSES_AFTER_EXTREME_ZONES = True # Turn on/off entering on bar following crosses if previous bar cross entry # was not allowed because %K was still in extreme zone # ENTRY_AFTER_CROSS_IN_EXTREME_ZONE = True # Turn on/off entry at open # enter long if in premarket %K drops to oversold zone, then crosses over %D # enter short if in premarket %K enters overbought zone, then crosses under %D ENTRY_AT_OPEN = False # Turn on/off entering immediately at extreme levels ENTRY_AT_EXTREMES = False # Enter long (calls) at extreme oversold level ENTRY_LONG_OVERSOLD = 8 # Enter short (puts) at extreme overbought level ENTRY_SHORT_OVERBOUGHT = 92 #------------------------------------------------------------------------------- # EXIT INPUTS # Exit long trade if Stochastic %K is >= overbought level # Exit short trade if Stochastic %D is <= oversold level EXIT_AT_STOCHASTIC_EXTREME = False EXIT_STOCHASTIC_OVERBOUGHT = 88 EXIT_STOCHASTIC_OVERSOLD = 12 # Exit based on SR levels (for profit) TAKE_PROFIT_AT_SR = False # Set the minimum distance required to allow take profit exit MIN_TAKE_PROFIT_AMOUNT = 0.5 # can be algorithm parameter # Set the percentage difference allowed to trigger at levels SR_TOLERANCE_PERCENT = 0.01 # as decimal percentage, e.g. 0.01=1.0% # NOTE: percents below are all decimal percents # e.g. 0.50=50.0%, 1.0=100.0% # Set stop loss exit STOP_LOSS_PERCENT = 0.5 # can be algorithm parameter # Set take profit USE_TAKE_PROFIT = True TAKE_PROFIT_PERCENT = 2.0 # can be algorithm parameter # Set trailing stop USE_TRAILING_STOP = False TRAILING_STOP_PROFIT_TRIGGER = 1.0 # can be algorithm parameter TRAILING_STOP_PERCENT = 0.10 # can be algorithm parameter #------------------------------------------------------------------------------- # LOGGING DETAILS # What logs to print? PRINT_SIGNALS = True # print signals info PRINT_ORDERS = True # print new orders ################################################################################ ############################ END OF ALL USER INPUTS ############################ ################################################################################ # VALIDATE USER INPUTS - DO NOT CHANGE BELOW!!! #------------------------------------------------------------------------------- # Verify start date try: START_DT = DT.datetime.strptime(START_DATE, '%m-%d-%Y') except: raise ValueError( f"Invalid START_DATE format ({START_DATE}). Must be in MM-DD-YYYY " "format." ) # Verify end date try: if END_DATE: END_DT = DT.datetime.strptime(END_DATE, '%m-%d-%Y') except: raise ValueError( f"Invalid END_DATE format ({END_DATE}). Must be in MM-DD-YYYY " "format or set to None to run to date." ) #------------------------------------------------------------------------------- # Verify DATA_RESOLUTION input DATA_RESOLUTION = DATA_RESOLUTION.upper() if DATA_RESOLUTION not in ['SECOND', 'MINUTE', 'HOUR']: raise ValueError( f"Invalid DATA_RESOLUTION ({DATA_RESOLUTION}). Must be 'SECOND', " f"'MINUTE' or 'HOUR'." ) #------------------------------------------------------------------------------- # Set the data normalizaton mode for price data # either 'ADJUSTED' (backtesting), or 'RAW' (necessary for live trading) # Raw is always required for options! DATA_MODE = 'RAW' #------------------------------------------------------------------------------- # Define benchmark equity # Also used for scheduling functions, so make sure it has same trading hours # as instruments traded. BENCHMARK = TICKER #------------------------------------------------------------------------------- # Calculate the number of days in the backtest PLOT_LIMIT = 4000 if not END_DATE: today = DT.datetime.today() BT_DAYS = (today-START_DT).days else: BT_DAYS = (END_DT-START_DT).days # Convert calendar days to estimated market days # Round up to the nearest integer # This uses // for integer division which rounds down # Take division for negative number to round up, then negate the negative BT_DAYS = -(-BT_DAYS*252//365) # Calculate the frequency of days that we can create a new plot # Use the same approach as above to round up to the nearest integer PLOT_EVERY_DAYS = -(-BT_DAYS//PLOT_LIMIT)
# Standard library imports import datetime as DT # from datetime import timedelta # from datetime import date # from dateutil.parser import parse # import decimal import math # import numpy as np # import pandas as pd import pytz from System.Drawing import Color # QuantConnect specific imports from AlgorithmImports import * # import QuantConnect as qc # Import from files from custom_models import CustomExerciseModel from notes_and_inputs import * ################################################################################ class SymbolData(object): """Class to store data for a specific symbol.""" def __init__(self, algo, symbol_object): """Initialize SymbolData object.""" # Save a reference to the QCAlgorithm class self.algo = algo # Save the string symbol and the .Symbol object self.symbol = symbol_object.Value self.symbol_object = symbol_object # Get the symbol's exchange market info self.get_exchange_info() # Add strategy variables self.add_strategy_variables() # Add the bars and indicators required self.add_bars() self.add_indicators() # Schedule functions self.schedule_functions() #------------------------------------------------------------------------------- def get_exchange_info(self): """Get the security's exchange info.""" # Get the SecurityExchangeHours Class object for the symbol self.exchange_hours = self.algo.Securities[self.symbol].Exchange.Hours # Create a datetime I know the market was open for the full day dt = DT.datetime(2021, 1, 4) # Get the next open datetime from the SecurityExchangeHours Class mkt_open_dt = self.exchange_hours.GetNextMarketOpen(dt, False) # Save the typical market open and close times self.mkt_open = mkt_open_dt.time() mkt_close_dt = self.exchange_hours.GetNextMarketClose(dt, False) self.mkt_close = mkt_close_dt.time() # Get the exchange timezone self.mkt_tz = pytz.timezone(str(self.exchange_hours.TimeZone)) # Create pytz timezone objects for the exchange tz and local tz exchange_tz = self.mkt_tz local_tz = pytz.timezone(TIMEZONE) # Get the difference in the timezones # REF: http://pytz.sourceforge.net/#tzinfo-api # for pytz timezone.utcoffset() method # 3600 seconds/hour exchange_utc_offset_hrs = int(exchange_tz.utcoffset(dt).seconds/3600) local_utc_offset_hrs = int(local_tz.utcoffset(dt).seconds/3600) self.offset_hrs = exchange_utc_offset_hrs-local_utc_offset_hrs # NOTE: offset hours are very helpful if you want to schedule functions # around market open/close times # Get the market close time for the local time zone self.mkt_close_local_tz = \ (mkt_close_dt-DT.timedelta(hours=self.offset_hrs)).time() #------------------------------------------------------------------------------- def add_bars(self): """Add bars required.""" # Create the desired custom bar consolidator for the symbol consolidator = TradeBarConsolidator(timedelta(minutes=self.algo.BAR_MINUTES)) # Create an event handler to be called on each new consolidated bar consolidator.DataConsolidated += self.on_data_consolidated # Link the consolidator with our symbol and add it to the algo manager self.algo.SubscriptionManager.AddConsolidator(self.symbol, consolidator) # Save the consolidator self.consolidator = consolidator # Create the daily bar consolidator (used for S/R levels) daily_consolidator = TradeBarConsolidator(self.daily_calendar) # Create an event handler to be called on each new consolidated bar daily_consolidator.DataConsolidated += self.on_daily_consolidated # Link daily_consolidator with our symbol and add it to the algo manager self.algo.SubscriptionManager.AddConsolidator( self.symbol, daily_consolidator ) # Save daily_consolidator link so we can remove it when necessary self.daily_consolidator = daily_consolidator #------------------------------------------------------------------------------- def daily_calendar(self, dt): """ Set up daily consolidator calendar info for the US equity market. This should return a start datetime object that is timezone unaware with a valid date/time for the desired securities' exchange's time zone. """ # Need to handle case where algo initializes and this function is called # for the first time. if not self.calendar_initialized: # Since this doesn't matter, we'll pass dt as start and one day # as the timedelta until end_dt start_dt = dt end_dt = start_dt + DT.timedelta(1) self.calendar_initialized = True return CalendarInfo(start_dt, end_dt-start_dt) # Create a datetime.datetime object to represent the market open for the # **EXCHANGE** timezone start = dt.replace( hour=self.mkt_open.hour, minute=self.mkt_open.minute, second=0, microsecond=0 ) # Get today's end time from the SecurityExchangeHours Class object end = self.exchange_hours.GetNextMarketClose(start, False) # Catch when start is after the passed dt # QC now throws an error in this case if start > dt: # To handle the QC error, pass period for no data # Set the end to be the next desired start end = start # And set start to dt to avoid QC throwing error start = dt # This will result in the next dt being the desired start time # Return the start datetime and the consolidation period return CalendarInfo(start, end-start) #------------------------------------------------------------------------------- def add_indicators(self): """Add indicators required.""" # Create an empty list to hold all indicators # Will add (indicator, update_method) tuples # where update_method is either 'high', 'low', 'close' or 'bar' self.indicators = [] # Create the Stochastic self.stochastic = Stochastic( STOCHASTIC_PERIOD, STOCHASTIC_KPERIOD, STOCHASTIC_DPERIOD ) self.indicators.append((self.stochastic, 'bar')) # Keep track of the previous stochastic %K value self.previous_stochastic_k = None # Keep track of %K crossing %D self.k_gt_d = RollingWindow[bool](2) self.k_lt_d = RollingWindow[bool](2) # Create a chart to plot the Stochastic self.chart_name = f'{self.symbol} Stochastics Plot' chart = Chart(self.chart_name) # Add series to the chart for the cmf chart.AddSeries( Series('%K', SeriesType.Line, '$', Color.Blue, ScatterMarkerSymbol.Circle) ) chart.AddSeries( Series('%D', SeriesType.Line, '$', Color.Orange, ScatterMarkerSymbol.Circle) ) # Create a list to hold the daily indicators self.daily_indicators = [] # Create the Average True Range self.atr = AverageTrueRange(ATR_PERIOD) self.daily_indicators.append((self.atr, 'bar')) # Initialize the S/R levels self.support = {} self.resistance = {} for percentage in SR_LEVELS: self.support[percentage] = None self.resistance[percentage] = None # Get the min number of bars required indicator_bars = [ int(3.0*(STOCHASTIC_PERIOD+STOCHASTIC_KPERIOD+STOCHASTIC_DPERIOD)) ] self.min_bars = max(indicator_bars) # Keep a rolling window of bars self.bar_window = RollingWindow[TradeBar](self.min_bars) # Custom rolling windows self.ma_fast_gt_slow = RollingWindow[bool](2) self.ma_fast_lt_slow = RollingWindow[bool](2) # Get the min number of daily bars required daily_indicator_bars = [int(3.0*ATR_PERIOD)] self.min_daily_bars = max(daily_indicator_bars) # Keep a rolling window of daily bars self.daily_bar_window = RollingWindow[TradeBar](self.min_daily_bars) # Warm up the indicators with historical data self.warmup_indicators() #------------------------------------------------------------------------------- def add_strategy_variables(self): """Add strategy specific variables.""" self.option_expiry = None self.option_data_subcriptions = [] self.reset_trade_variables() self.trading = False self.end_of_day_exit_ignore = False self.previous_bar_crossover = False self.previous_bar_crossunder = False # Keep track of calendar initialized self.calendar_initialized = False # Keep track of premarket variables self.reset_premarket_variables() #------------------------------------------------------------------------------- def reset_premarket_variables(self): """Reset variables used for premarket tracking.""" self.premarket_overbought = False self.premarket_oversold = False self.premarket_crossover = False self.premarket_crossunder = False #------------------------------------------------------------------------------- def reset_trade_variables(self): """Reset trade specific variables.""" self.stop_loss_price = None self.stop_loss_order = None self.trailing_stop_activation_price = None self.trade_best_price = None self.trailing_stop_activated = False self.take_profit_order = None self.call = None self.put = None self.position = None self.price_at_entry = None #------------------------------------------------------------------------------- def schedule_functions(self): """Schedule functions required for the class.""" # Start trading self.algo.Schedule.On( self.algo.DateRules.EveryDay(self.symbol_object), self.algo.TimeRules.AfterMarketOpen( self.symbol_object, self.algo.START_TRADING_MINUTES_AFTER_OPEN ), self.start_trading ) # Stop trading self.algo.Schedule.On( self.algo.DateRules.EveryDay(self.symbol_object), self.algo.TimeRules.BeforeMarketClose( self.symbol_object, STOP_TRADING_MINUTES_BEFORE_CLOSE ), self.stop_trading ) # End of day exit if USE_EOD_EXIT: self.algo.Schedule.On( self.algo.DateRules.EveryDay(self.symbol_object), self.algo.TimeRules.BeforeMarketClose( self.symbol_object, EOD_EXIT_MINUTES_BEFORE_CLOSE ), self.end_of_day_exit ) # On expiry day exit self.algo.Schedule.On( self.algo.DateRules.EveryDay(self.symbol_object), self.algo.TimeRules.BeforeMarketClose( self.symbol_object, EXPIRY_DAY_EXIT_MINUTES_BEFORE_CLOSE ), self.expiry_day_end_of_day_exit ) # On market open exit if EXIT_ON_OPEN: self.algo.Schedule.On( self.algo.DateRules.EveryDay(self.symbol_object), self.algo.TimeRules.AfterMarketOpen(self.symbol_object, -10), self.go_flat ) # After market close self.algo.Schedule.On( self.algo.DateRules.EveryDay(self.symbol_object), self.algo.TimeRules.BeforeMarketClose(self.symbol_object, -30), self.after_market_close ) #------------------------------------------------------------------------------- def start_trading(self): """Time to start trading.""" self.trading = True #------------------------------------------------------------------------------- def stop_trading(self): """Time to stop trading.""" if self.trading: self.trading = False # Reset premarket variables self.reset_premarket_variables() #------------------------------------------------------------------------------- def end_of_day_exit(self): """Exit open trades at the end of the day and stop trading.""" self.stop_trading() if not self.end_of_day_exit_ignore: self.go_flat() #------------------------------------------------------------------------------- def expiry_day_end_of_day_exit(self): """ Exit open trades at the end of the day if expiry day and stop trading. """ # Catch if the current expiration is today if self.option_expiry == self.algo.Time.date(): # Trigger the end of day exit logic self.end_of_day_exit() #------------------------------------------------------------------------------- def after_market_close(self): """Function called 30minutes after the market close.""" # Catch if the current expiration is today # if self.option_expiry == self.algo.Time.date(): # Set it back to None self.option_expiry = None #------------------------------------------------------------------------------- def on_data_consolidated(self, sender, bar): """Event handler for desired custom bars.""" # Manually update all of the indicators # Ignore if bar start time is before the start time if bar.Time.time() < INDICATOR_UPDATE_START_TIME: return # Ignore if bar end time is after stop time elif bar.EndTime.time() > INDICATOR_UPDATE_STOP_TIME: return self.update_indicators(bar) # Keep track of potential options to trade if not self.algo.IsWarmingUp and not self.warming_up: self.track_options() # Check for trading signals once the algo is no longer warming up, # indicators are ready, and trading is allowed if not self.algo.IsWarmingUp and not self.warming_up \ and self.indicators_ready and self.trading: self.check_for_signals(new_bar=True) # Update self.previous_bar_crossover and self.previous_bar_crossunder self.previous_bar_crossover = self.stochastic_crossover self.previous_bar_crossunder = self.stochastic_crossunder #------------------------------------------------------------------------------- def on_daily_consolidated(self, sender, bar): """Event handler for daily bars.""" # Manually update all of the daily indicators self.update_daily_indicators(bar) #------------------------------------------------------------------------------- def track_options(self): """Track options to consider trading.""" # Get a list of the available options options = self.algo.OptionChainProvider.GetOptionContractList( self.symbol, self.algo.Time ) # Get the new expiration date if self.option_expiry is None: # Get the possible expirations that meet MIN_DAYS_TO_EXPIRY min_dt = self.algo.Time.date() + DT.timedelta(days=self.algo.MIN_DAYS_TO_EXPIRY) expiries = [ x.ID.Date.date() for x in options if x.ID.Date.date() >= min_dt ] # Get unique expiries as list expiries = list(set(expiries)) # Sort list earliest to latest expiries.sort() # Use the first valid expiry self.option_expiry = expiries[0] # Check if we have the desired expiration if self.option_expiry: expiry = self.option_expiry # Get a list of strikes for only the desired expiry call_strikes = [ x.ID.StrikePrice for x in options \ if x.ID.Date.date() == expiry and x.ID.OptionRight == 0 ] put_strikes = [ x.ID.StrikePrice for x in options \ if x.ID.Date.date() == expiry and x.ID.OptionRight == 1 ] # Get a list of matching strikes - valid for calls and puts both # This is an intersection call_strikes = set(call_strikes) put_strikes = set(put_strikes) strikes = list(call_strikes.intersection(put_strikes)) # Return if no strikes if len(strikes) == 0: return # Otherwise sort the list strikes.sort() # Get the underlying price and differences to it for all strikes price = self.price strike_differences = [(x, price-x) for x in strikes] # Sort the lists by smallest difference from underlying price strike_differences.sort(key=lambda x:x[1]) # Get the ATM strike atm_strike_difference = min([abs(x[1]) for x in strike_differences]) try: atm_strike_index = [x[1] for x in strike_differences].index( atm_strike_difference ) except: try: atm_strike_index = [x[1] for x in strike_differences].index( -atm_strike_difference ) except: self.MyLog(f"Error getting the ATM strike!") return atm_strike = strike_differences[atm_strike_index][0] atm_strike_index = strikes.index(atm_strike) # Get the possible OTM call and put strikes otm_strike_int = max(2, abs(self.algo.OTM_STRIKES)) # use this as a minimum # We'll go otm_strike_int on either side of the current target # just in case the underlying moves significantly before we # enter the trade. diff = int(otm_strike_int*2) possible_call_strikes = strikes[ atm_strike_index-diff:atm_strike_index+diff+1 ] possible_call_strikes.sort() possible_put_strikes = strikes[ atm_strike_index-diff:atm_strike_index+diff+1 ] possible_put_strikes.sort() # Create empty lists for possible calls and puts self.possible_calls = [] self.possible_puts = [] # Subscribe to the possible options to buy for strike in possible_call_strikes: # Get the target call and subscribe to it's data calls = [ x for x in options \ if x.ID.Date.date() == expiry and x.ID.OptionRight == 0 \ and x.ID.StrikePrice == strike ] if len(calls) > 0: call = calls[0] # Add option contract, if it's not currently subscribed to if call not in self.option_data_subcriptions: self.algo.AddOptionContract( call, self.algo.option_resolution ) self.option_data_subcriptions.append(call) # below doesn't seem to be working... security = self.algo.Securities[call] security.SetOptionAssignmentModel( NullOptionAssignmentModel() ) security.SetOptionExerciseModel(CustomExerciseModel()) # option = self.AddOption("SPY") self.possible_calls.append(call) for strike in possible_put_strikes: # Get the target put and subscribe to it's data puts = [ x for x in options \ if x.ID.Date.date() == expiry and x.ID.OptionRight == 1 \ and x.ID.StrikePrice == strike ] if len(puts) > 0: put = puts[0] # Add option contract, if it's not currently subscribed to if put not in self.option_data_subcriptions: self.algo.AddOptionContract( put, self.algo.option_resolution ) self.option_data_subcriptions.append(put) # below doesn't seem to be working... security = self.algo.Securities[put] security.SetOptionAssignmentModel( NullOptionAssignmentModel() ) security.SetOptionExerciseModel(CustomExerciseModel()) self.possible_puts.append(put) #------------------------------------------------------------------------------- @property def indicators_ready(self): """Check if all of the indicators used are ready (warmed up).""" # Loop through all indicators for indicator, update_method in self.indicators: # Return False if the indicator is not ready if not indicator.IsReady: return False # for indicator, update_method in self.higher_tf_indicators: # # Return False if the indicator is not ready # if not indicator.IsReady: # return False for indicator, update_method in self.daily_indicators: # Return False if the indicator is not ready if not indicator.IsReady: return False # Check any RollingWindows if not self.bar_window.IsReady: return False # Check any custom RollingWindows if not self.k_gt_d.IsReady: return False if not self.k_lt_d.IsReady: return False # Otherwise all indicators are ready, so return True return True #------------------------------------------------------------------------------- def update_indicators(self, bar): """Manually update all of the symbol's indicators.""" # Keep track of self.previous_stochastic_k if self.stochastic.IsReady: self.previous_stochastic_k = self.stochastic.StochK.Current.Value # Plot if not warming up if not self.warming_up: k = self.stochastic.StochK.Current.Value d = self.stochastic.StochD.Current.Value self.algo.Plot(self.chart_name, '%K', k) self.algo.Plot(self.chart_name, '%D', d) # If the indicator only needs a single value (like close), pass the time # and the value to the Update() method. # e.g. indicator.Update(bar.EndTime, bar.Close) # If the indicator needs multiple parts of a bar like the open, high, # low, and close, only pass the bar to the Update() method. # e.g. indicator.Update(bar) # Loop through the indicators for indicator, update_method in self.indicators: if update_method == 'close': indicator.Update(bar.EndTime, bar.Close) elif update_method == 'high': indicator.Update(bar.EndTime, bar.High) elif update_method == 'low': indicator.Update(bar.EndTime, bar.Low) elif update_method == 'bar': indicator.Update(bar) # Handle any RollingWindows below # Update the bar window self.bar_window.Add(bar) # Handle custom RollingWindows if self.stochastic.IsReady: k = self.stochastic.StochK.Current.Value d = self.stochastic.StochD.Current.Value self.k_gt_d.Add(k > d) self.k_lt_d.Add(k < d) # Handle entry at open variables if not self.warming_up and ENTRY_AT_OPEN \ and bar.EndTime.time() <= DT.time(9,30): # Bar is still premarket # Get the %K and %D values k = self.stochastic.StochK.Current.Value d = self.stochastic.StochD.Current.Value # Check for crosses if self.stochastic_crossover: self.premarket_crossover = True elif self.stochastic_crossunder: self.premarket_crossunder = True # Check for k overbought or oversold if k >= OVERBOUGHT: self.premarket_overbought = True self.premarket_oversold = False # Also reset crossunder self.premarket_crossunder = False elif k <= OVERSOLD: self.premarket_overbought = False self.premarket_oversold = True # Also reset crossover self.premarket_crossover = False #------------------------------------------------------------------------------- def update_daily_indicators(self, bar): """Manually update all of the symbol's daily indicators.""" # Loop through the indicators for indicator, update_method in self.daily_indicators: if update_method == 'close': indicator.Update(bar.EndTime, bar.Close) elif update_method == 'high': indicator.Update(bar.EndTime, bar.High) elif update_method == 'low': indicator.Update(bar.EndTime, bar.Low) elif update_method == 'bar': indicator.Update(bar) # Handle any RollingWindows below # Update the bar window self.daily_bar_window.Add(bar) # Check if the ATR is ready if self.atr.IsReady: atr = self.atr.Current.Value # Calculate the support/resistance levels close = bar.Close for percentage in SR_LEVELS: self.support[percentage] = close - atr*percentage self.resistance[percentage] = close + atr*percentage #------------------------------------------------------------------------------- def adjust_indicators(self, adjustment, split=False): """Adjust all indicators for splits or dividends.""" # Need to update to handle split -> volume handled differently # Get a list of the current bars bars = list(self.bar_window) # Current order is newest to oldest (default for rolling window) # Reverse the list to be oldest to newest bars.reverse() # Reset all indicators self.reset_indicators() # Loop through the bars from oldest to newest for bar in bars: # Adjust the bar by the adjustment factor bar.Open *= adjustment bar.High *= adjustment bar.Low *= adjustment bar.Close *= adjustment # Use the bar to update the indicators # This also adds the bar to the rolling window self.update_indicators(bar) # Get a list of the current daily bars daily_bars = list(self.daily_bar_window) # Current order is newest to oldest (default for rolling window) # Reverse the list to be oldest to newest daily_bars.reverse() # Reset all indicators self.reset_daily_indicators() # Loop through the bars from oldest to newest for bar in daily_bars: # Adjust the bar by the adjustment factor bar.Open *= adjustment bar.High *= adjustment bar.Low *= adjustment bar.Close *= adjustment # Use the bar to update the indicators # This also adds the bar to the rolling window self.update_daily_indicators(bar) #------------------------------------------------------------------------------- def reset_indicators(self): """Manually reset all of the indicators.""" # Loop through all indicators and reset them for indicator, update_method in self.indicators: indicator.Reset() # Reset the bar window self.bar_window.Reset() #------------------------------------------------------------------------------- def reset_daily_indicators(self): """Manually reset all of the daily indicators.""" # Loop through all indicators and reset them for indicator, update_method in self.daily_indicators: indicator.Reset() # Reset the bar window self.daily_bar_window.Reset() #------------------------------------------------------------------------------- def warmup_indicators(self): """Warm up indicators using historical data.""" self.warming_up = True # Get a list of historical minute trade bars # num_bars = int(2.0*self.min_bars*HIGHER_BAR_MINUTES) num_bars = int(2.0*self.min_bars*self.algo.BAR_MINUTES) bars = self.algo.History[TradeBar]( self.symbol_object, num_bars, Resolution.Minute ) # Loop through the bars and update the consolidator for bar in bars: self.consolidator.Update(bar) # self.higher_consolidator.Update(bar) # Get a list of historical daily trade bars daily_bars = self.algo.History[TradeBar]( self.symbol_object, self.min_daily_bars, Resolution.Daily ) # Loop through the bars and update the consolidator for bar in daily_bars: # Instead of passing to consolidator, pass directly to event handler self.on_daily_consolidated(None, bar) self.warming_up = False # Throw error if indicators are not ready when backtesting if not self.indicators_ready and not self.algo.LiveMode: raise #------------------------------------------------------------------------------- def check_trailing_stop(self): """Check if we need to updated the trailing stop.""" # Get the current option price option_price = self.algo.Securities[self.option].Price # Wait until we initialize the trade best price if self.trade_best_price is None: self.trade_best_price = option_price # Check for new best trade price if option_price > self.trade_best_price: self.trade_best_price = option_price # Check if the trailing stop has not been activated if not self.trailing_stop_activated: # Check if it's now triggered if option_price >= self.trailing_stop_activation_price: self.trailing_stop_activated = True # Log message when desired if PRINT_SIGNALS: self.algo.MyLog( f"{self.option} trailing stop activated. Price " f"({option_price}) >= activation price " f"({self.trailing_stop_activation_price})" ) # Skip if the trailing stop is not activated if not self.trailing_stop_activated: return # Otherwise check for new trailing stop price trailing_stop_price = round( self.trade_best_price*(1-self.algo.TRAILING_STOP_PERCENT),2 ) # Update the stop loss order on a higher trailing stop price if trailing_stop_price > self.stop_loss_price: # Update the order self.update_stop_order(trailing_stop_price) #------------------------------------------------------------------------------- def check_for_signals(self, new_bar=False): """Check for new trading signals.""" # Debug # if self.algo.Time.month == 2 and self.algo.Time.day >= 3: # print('debug') # Get the current price. Return if not valid. price = self.price if price is None: return # Check for trailing stop and an active position if USE_TRAILING_STOP and self.position != None: self.check_trailing_stop() # Check for long position if self.position == 'long': # Check for long exit signal if self.long_exit_signal(price): self.go_flat() # Check for short position elif self.position == 'short': # Check for short exit signal if self.short_exit_signal(price): self.go_flat() # Otherwise flat # not using else here to catch after exit triggered # but only on a new bar! if self.position is None and new_bar: # Check for long entry signal if self.long_entry_signal(price): # Save price at entry self.price_at_entry = price self.go_long() # Check for short entry signal elif self.short_entry_signal(price): # Save price at entry self.price_at_entry = price self.go_short() #------------------------------------------------------------------------------- def long_entry_signal(self, price): """Check if there is a valid long entry signal.""" # First apply filters to disqualify entry # Require longs allowed if not LONGS_ALLOWED: return False # Require the current k to be <= ENTRY_STOCHASTIC_K_LONG_MAX k = self.stochastic.StochK.Current.Value d = self.stochastic.StochD.Current.Value if ENTRY_NOT_AFTER_EXTREME_MOVE_FILTER: if k > ENTRY_STOCHASTIC_K_LONG_MAX: return False # Require the current k to be > oversold zone if ENTRY_OUTSIDE_EXTREME_ZONE_FILTER: if k <= ENTRY_EXTREME_FILTER_OVERSOLD: return False # Working here... # # Next look for specific entry criteria # if ENTRY_AFTER_CROSS_IN_EXTREME_ZONE: # # Check if previous bar was a crossover # # and Stochastic %K still > %D # if self.previous_bar_crossover and (k>d): # # if ENTRY_CROSSES_AFTER_EXTREME_ZONES \ and self.algo.Time.time() >= DT.time(9,30): # Require previous Stochastic %K below OVERSOLD # and Stochastic %K crossing over Stochastic %D if (self.previous_stochastic_k < OVERSOLD) \ and self.stochastic_crossover: # Log message when desired if PRINT_SIGNALS: self.algo.MyLog( f"{self.symbol} LONG ENTRY SIGNAL: Stochastic %K " f"({k:.2f}) crossing over %D ({d:.2f}) and previous %K=" f"{self.previous_stochastic_k:.2f}, price={price}" ) return True if ENTRY_AT_OPEN and self.algo.Time.time() < DT.time(9,31): # Vaild if premarket oversold followed by crossover # Also make sure that K is still over D if self.premarket_oversold and self.premarket_crossover and (k>d): # Log message when desired if PRINT_SIGNALS: self.algo.MyLog( f"{self.symbol} LONG ENTRY SIGNAL: Stochastic %K " f"oversold in premarket then crossed over %D. K " f"({k:.2f}) still > %D ({d:.2f}), price={price}" ) return True if ENTRY_AT_EXTREMES: # Enter if k drops <= ENTRY_LONG_OVERSOLD if k <= ENTRY_LONG_OVERSOLD: # Log message when desired if PRINT_SIGNALS: self.algo.MyLog( f"{self.symbol} LONG ENTRY SIGNAL: Stochastic %K " f"extremely oversold ({k:.2f}) <= {ENTRY_LONG_OVERSOLD}," f" price={price}" ) return True # If not triggered above, then entry not valid return False #------------------------------------------------------------------------------- def short_entry_signal(self, price): """Check if there is a valid short entry signal.""" # # Debug # if self.algo.Time.month == 2 and self.algo.Time.day >= 9: # print('debug') # First apply filters to disqualify entry # Require shorts allowed if not SHORTS_ALLOWED: return False # Require the current k to be >= ENTRY_STOCHASTIC_K_SHORT_MIN k = self.stochastic.StochK.Current.Value d = self.stochastic.StochD.Current.Value if ENTRY_NOT_AFTER_EXTREME_MOVE_FILTER: if k < ENTRY_STOCHASTIC_K_SHORT_MIN: return False # Require the current k to be < overbought zone if ENTRY_OUTSIDE_EXTREME_ZONE_FILTER: if k >= ENTRY_EXTREME_FILTER_OVERBOUGHT: return False # Next look for specific entry criteria if ENTRY_CROSSES_AFTER_EXTREME_ZONES \ and self.algo.Time.time() >= DT.time(9,30): # Require previous Stochastic %K above OVERBOUGHT # and Stochastic %K crossing over Stochastic %D if (self.previous_stochastic_k > OVERBOUGHT) \ and self.stochastic_crossunder: # Log message when desired if PRINT_SIGNALS: self.algo.MyLog( f"{self.symbol} SHORT ENTRY SIGNAL: Stochastic %K " f"({k:.2f}) crossing under %D ({d:.2f}) and previous " f"%K={self.previous_stochastic_k:.2f}, price={price}" ) return True if ENTRY_AT_OPEN and self.algo.Time.time() < DT.time(9,31): # Vaild if premarket overbought followed by crossunder # Also make sure that K is still below D if self.premarket_overbought and self.premarket_crossunder and (k<d): # Log message when desired if PRINT_SIGNALS: self.algo.MyLog( f"{self.symbol} SHORT ENTRY SIGNAL: Stochastic %K " f"overbought in premarket then crossed under %D. K " f"({k:.2f}) still < %D ({d:.2f}), price={price}" ) return True if ENTRY_AT_EXTREMES: # Enter if k rises >= ENTRY_SHORT_OVERBOUGHT if k >= ENTRY_SHORT_OVERBOUGHT: # Log message when desired if PRINT_SIGNALS: self.algo.MyLog( f"{self.symbol} LONG ENTRY SIGNAL: Stochastic %K " f"extremely overbought ({k:.2f}) >= " f"{ENTRY_SHORT_OVERBOUGHT}, price={price}" ) return True # If not triggered above, then entry not valid return False #------------------------------------------------------------------------------- def long_exit_signal(self, price): """Check if there is a valid long exit signal.""" # Valid if the Stochastic %K is >= EXIT_STOCHASTIC_OVERBOUGHT k = self.stochastic.StochK.Current.Value if k >= EXIT_STOCHASTIC_OVERBOUGHT and EXIT_AT_STOCHASTIC_EXTREME: # Log message when desired if PRINT_SIGNALS: self.algo.MyLog( f"{self.symbol} LONG EXIT SIGNAL: Stochastic %K ({k:.2f}) " f">= {EXIT_STOCHASTIC_OVERBOUGHT}" ) return True # Valid on profitable move to support/resistance level if TAKE_PROFIT_AT_SR: # Loop through SR levels for percentage in SR_LEVELS: # Ignore if the profit distance is too small support = self.support[percentage]*(1-SR_TOLERANCE_PERCENT) diff = support-self.price_at_entry if diff > self.algo.MIN_TAKE_PROFIT_AMOUNT: # Check if we've triggered at this level if price >= support: # Log message when desired if PRINT_SIGNALS: pct = round(percentage*100.0,3) self.algo.MyLog( f"{self.symbol} LONG EXIT SIGNAL: close " f"({price}) >= S[{pct}] ({support})" ) return True resistance = self.resistance[percentage]*(1-SR_TOLERANCE_PERCENT) diff = resistance-self.price_at_entry if diff > self.algo.MIN_TAKE_PROFIT_AMOUNT: # Check if we've triggered at this level if price >= resistance: # Log message when desired if PRINT_SIGNALS: pct = round(percentage*100.0,3) self.algo.MyLog( f"{self.symbol} LONG EXIT SIGNAL: close " f"({price}) >= R[{pct}] ({resistance})" ) return True # Otherwise False return False #------------------------------------------------------------------------------- def short_exit_signal(self, price): """Check if there is a valid short exit signal.""" # Valid if the Stochastic %K is <= EXIT_STOCHASTIC_OVERSOLD k = self.stochastic.StochK.Current.Value if k <= EXIT_STOCHASTIC_OVERSOLD and EXIT_AT_STOCHASTIC_EXTREME: # Log message when desired if PRINT_SIGNALS: self.algo.MyLog( f"{self.symbol} SHORT EXIT SIGNAL: Stochastic %K ({k:.2f}) " f"<= {EXIT_STOCHASTIC_OVERSOLD}" ) return True # Valid on profitable move to support/resistance level if TAKE_PROFIT_AT_SR: # Loop through SR levels for percentage in SR_LEVELS: # Ignore if the profit distance is too small support = self.support[percentage]*(1+SR_TOLERANCE_PERCENT) diff = self.price_at_entry-support if diff > self.algo.MIN_TAKE_PROFIT_AMOUNT: # Check if we've triggered at this level if price <= support: # Log message when desired if PRINT_SIGNALS: pct = round(percentage*100.0,3) self.algo.MyLog( f"{self.symbol} SHORT EXIT SIGNAL: close " f"({price}) <= S[{pct}] ({support})" ) return True resistance = self.resistance[percentage]*(1+SR_TOLERANCE_PERCENT) diff = self.price_at_entry-resistance if diff > self.algo.MIN_TAKE_PROFIT_AMOUNT: # Check if we've triggered at this level if price <= resistance: # Log message when desired if PRINT_SIGNALS: pct = round(percentage*100.0,3) self.algo.MyLog( f"{self.symbol} SHORT EXIT SIGNAL: close " f"({price}) <= R[{pct}] ({resistance})" ) return True # Otherwise False return False #------------------------------------------------------------------------------- def select_call(self): """Select the call option to trade.""" # Get the list of strikes strikes = [x.ID.StrikePrice for x in self.possible_calls] strikes.sort() # Get the underlying price and differences to it for all strikes price = self.price strike_differences = [(x, abs(price-x)) for x in strikes] # Sort the list by smallest difference from underlying price strike_differences.sort(key=lambda x:x[1]) # Get the ATM strike atm_strike_difference = min([abs(x[1]) for x in strike_differences]) atm_strike_index = [x[1] for x in strike_differences].index( atm_strike_difference ) atm_strike = strike_differences[atm_strike_index][0] atm_strike_index = strikes.index(atm_strike) # Get the target OTM strike for the trade if len(strikes) > atm_strike_index+self.algo.OTM_STRIKES: # Catch if we don't have the strike (index < 0) if (atm_strike_index+self.algo.OTM_STRIKES) >= 0: target_strike = strikes[atm_strike_index+self.algo.OTM_STRIKES] else: # Best we can do now is use the lowest possible strike target_strike = strikes[0] else: target_strike = strikes[-1] strike_differences = [ (x, abs(target_strike-x)) for x in strikes ] strike_differences.sort(key=lambda x:x[1]) # Get the target OTM call option for strike, difference in strike_differences: if strike in strikes: target_calls = [ x for x in self.possible_calls \ if x.ID.StrikePrice == strike ] if len(target_calls) > 0: target_call = target_calls[0] return target_call #------------------------------------------------------------------------------- def select_put(self): """Select the put option to trade.""" # Get the list of strikes strikes = [x.ID.StrikePrice for x in self.possible_puts] strikes.sort() # Get the underlying price and differences to it for all strikes price = self.price strike_differences = [(x, abs(price-x)) for x in strikes] # Sort the list by smallest difference from underlying price strike_differences.sort(key=lambda x:x[1]) # Get the ATM strike atm_strike_difference = min([abs(x[1]) for x in strike_differences]) atm_strike_index = [x[1] for x in strike_differences].index( atm_strike_difference ) atm_strike = strike_differences[atm_strike_index][0] atm_strike_index = strikes.index(atm_strike) # Get the target OTM strike for the trade try: # Catch if we don't have the strike (index < 0) if (atm_strike_index-self.algo.OTM_STRIKES) >= 0: target_strike = strikes[atm_strike_index-self.algo.OTM_STRIKES] else: # Best we can do now is use the highest possible strike target_strike = strikes[-1] except: target_strike = strikes[0] strike_differences = [ (x, abs(target_strike-x)) for x in strikes ] strike_differences.sort(key=lambda x:x[1]) # Get the target OTM put option for strike, difference in strike_differences: if strike in strikes: target_puts = [ x for x in self.possible_puts \ if x.ID.StrikePrice == strike ] if len(target_puts) > 0: target_put = target_puts[0] return target_put #------------------------------------------------------------------------------- def go_long(self): """Take a long position.""" # self.algo.SetHoldings(self.symbol_object, 1.0) option = self.select_call() if option is None: return # Get the option bid/ask # bid = self.algo.Securities[option].BidPrice ask = self.algo.Securities[option].AskPrice # Ignore if ask is 0 if ask == 0: # Log message when desired if PRINT_SIGNALS: self.algo.MyLog( f"Ignoring (long) buy {option} signal because cannot get " f"price info." ) return # Get the target number of options to buy target_risk = self.algo.RISK_PER_TRADE*self.algo.Portfolio.TotalPortfolioValue risk_per_contract = 100.0*self.algo.STOP_LOSS_PERCENT*ask target_qty = int(target_risk/risk_per_contract) if target_qty != 0: # Log message when desired if PRINT_ORDERS: self.algo.MyLog(f"Buying {target_qty} of {option}. Ask={ask:.2f}.") order = self.algo.MarketOrder(option, target_qty, tag='call entry') # Catch invalid order if order.Status != OrderStatus.Invalid: self.call = option self.option = self.call self.position = 'long' # else: # raise ValueError(f"Error trying to buy {option}") self.late_entry_check() #------------------------------------------------------------------------------- def go_short(self): """Take a short position.""" # self.algo.SetHoldings(self.symbol_object, -1.0) option = self.select_put() if option is None: return # Get the option bid/ask # bid = self.algo.Securities[option].BidPrice ask = self.algo.Securities[option].AskPrice # Ignore if ask is 0 if ask == 0: # Log message when desired if PRINT_SIGNALS: self.algo.MyLog( f"Ignoring (short) buy {option} signal because cannot get " f"price info." ) return # Get the target number of options to buy target_risk = self.algo.RISK_PER_TRADE*self.algo.Portfolio.TotalPortfolioValue risk_per_contract = 100.0*self.algo.STOP_LOSS_PERCENT*ask target_qty = int(target_risk/risk_per_contract) if target_qty != 0: # Log message when desired if PRINT_ORDERS: self.algo.MyLog(f"Buying {target_qty} of {option}. Ask={ask:.2f}.") order = self.algo.MarketOrder(option, target_qty, tag='put entry') # Catch invalid order if order.Status != OrderStatus.Invalid: self.put = option self.option = self.put self.position = 'short' # else: # raise ValueError(f"Error trying to buy {option}") self.late_entry_check() #------------------------------------------------------------------------------- def late_entry_check(self): """Check for late entry to possibly ignore the end of day exit.""" # Check if a late in the day entry should be checked # Ignore if day of expiry self.end_of_day_exit_ignore = False if LATE_ENTRY_HOLD_OVERNIGHT and \ self.option_expiry != self.algo.Time.date(): # Get the number of minutes until the close close_dt = self.algo.Time.replace( hour=self.mkt_close_local_tz.hour, minute=self.mkt_close_local_tz.minute ) minutes_to_close = (close_dt-self.algo.Time).seconds/60.0 if minutes_to_close <= LATE_ENTRY_MINUTES_BEFORE_CLOSE: # Log message when desired if PRINT_SIGNALS: self.algo.MyLog( f"{minutes_to_close} minutes to close, so the new " f"position will NOT be exited at the end of the day!" ) self.end_of_day_exit_ignore = True #------------------------------------------------------------------------------- def go_flat(self): """Flatten the open position.""" # self.algo.SetHoldings(self.symbol_object, 0) if self.call: # Get the current qty order_qty = -self.algo.Portfolio[self.call].Quantity # Log message when desired if PRINT_ORDERS: self.algo.MyLog(f"Selling {-order_qty} of {self.call}.") self.algo.MarketOrder(self.call, order_qty, tag='call exit') self.call = None if self.put: # Get the current qty order_qty = -self.algo.Portfolio[self.put].Quantity # Log message when desired if PRINT_ORDERS: self.algo.MyLog(f"Selling {-order_qty} of {self.put}.") self.algo.MarketOrder(self.put, order_qty, tag='put exit') self.put = None self.position = None #------------------------------------------------------------------------------- @property def price(self): """Return the current price.""" try: return self.algo.Portfolio[self.symbol_object].Price except: try: return self.algo.Securities[self.symbol_object].Price except: return None #------------------------------------------------------------------------------- @property def stochastic_crossover(self): """Return if there is a Stochastic crossover.""" # Check if the rolling window is ready if self.k_gt_d.IsReady: return self.k_gt_d[0] and not self.k_gt_d[1] # Otherwise return False return False #------------------------------------------------------------------------------- @property def stochastic_crossunder(self): """Return if there is a Stochastic crossunder.""" # Check if the rolling window is ready if self.k_lt_d.IsReady: return self.k_lt_d[0] and not self.k_lt_d[1] # Otherwise return False return False #------------------------------------------------------------------------------- def cancel_open_exit_orders(self): """Cancel any open exit orders.""" # Cancel stop loss order, if one if self.stop_loss_order: response = self.stop_loss_order.Cancel() if response.IsSuccess: self.stop_loss_order = None elif not self.algo.LiveMode: raise ValueError(f"Error trying to cancel stop loss order!") # Cancel take profit order, if one if self.take_profit_order: response = self.take_profit_order.Cancel() if response.IsSuccess: self.take_profit_order = None elif not self.algo.LiveMode: raise ValueError(f"Error trying to cancel take profit order!") #------------------------------------------------------------------------------- def update_stop_order(self, price): """Update the desired stop order.""" # Get the stop order ticket ticket = self.stop_loss_order if ticket is None: return # Update the price ticket.UpdateStopPrice(price) self.stop_loss_price = price # Print details when desired if PRINT_SIGNALS: self.algo.MyLog( f"{self.option} trailing stop price updated to {price}" ) #------------------------------------------------------------------------------- def on_order_event(self, order_event): """New order event.""" # Get the order details order = self.algo.Transactions.GetOrderById(order_event.OrderId) order_qty = int(order.Quantity) avg_fill = order_event.FillPrice tag = order.Tag symbol = order.Symbol # Log message when desired if PRINT_ORDERS: self.algo.MyLog( f"{symbol} {tag} order filled: {order_qty} @ {avg_fill}" ) # Check for entry order if 'entry' in tag: # Check if we use a stop loss order # if USE_STOP_LOSS: # Check if also using trailing stop and profit trigger is 0 if USE_TRAILING_STOP \ and self.algo.TRAILING_STOP_PROFIT_TRIGGER == 0: # Use the closest stop stop_percent = min( self.algo.TRAILING_STOP_PERCENT, self.algo.STOP_LOSS_PERCENT ) else: stop_percent = self.algo.STOP_LOSS_PERCENT # Calulate the stop loss price stop_loss = round(avg_fill*(1-stop_percent),2) # Create the stop loss order self.stop_loss_price = stop_loss self.stop_loss_order = self.algo.StopMarketOrder( symbol, -order_qty, stop_loss, tag='stop loss exit' ) # Check if the trailing stop is used if USE_TRAILING_STOP: # Set trade best price to the filled entry price self.trade_best_price = avg_fill # Get the activation price activation = round( avg_fill*(1+self.algo.TRAILING_STOP_PROFIT_TRIGGER),2 ) self.trailing_stop_activation_price = activation # Check if the profit trigger is 0 if self.algo.TRAILING_STOP_PROFIT_TRIGGER == 0: # Set trailing stop activated to be true self.trailing_stop_activated = True # # See if the stop loss is not used # if not USE_STOP_LOSS: # # Calulate the stop loss price # stop_percent = self.algo.TRAILING_STOP_PERCENT # stop_loss = round(avg_fill*(1-stop_percent),2) # # Create the stop loss order # self.stop_loss_price = stop_loss # self.stop_loss_order = self.algo.StopMarketOrder( # symbol, -order_qty, stop_loss, tag='stop loss exit' # ) # Check if we use a take profit order if USE_TAKE_PROFIT: # Calulate the take profit price tp_price = round(avg_fill*(1+self.algo.TAKE_PROFIT_PERCENT),2) # Create the take profit order self.take_profit_price = tp_price self.take_profit_order = self.algo.LimitOrder( symbol, -order_qty, tp_price, tag='take profit exit' ) # Check for exit order elif 'exit' in tag: # Check for stop loss order filled if 'stop loss' in tag: self.stop_loss_order = None # Check for take profit order filled elif 'take profit' in tag: self.take_profit_order = None # Cancel any open exit orders self.cancel_open_exit_orders() # Reset trade variables self.reset_trade_variables()