Overall Statistics |
Total Trades 217 Average Win 2.81% Average Loss -2.47% Compounding Annual Return -4.218% Drawdown 83.400% Expectancy -0.383 Net Profit -23.890% Sharpe Ratio 0.15 Probabilistic Sharpe Ratio 0.865% Loss Rate 71% Win Rate 29% Profit-Loss Ratio 1.14 Alpha 0.025 Beta 0.325 Annual Standard Deviation 0.449 Annual Variance 0.202 Information Ratio -0.135 Tracking Error 0.459 Treynor Ratio 0.207 Total Fees $641.25 Estimated Strategy Capacity $91000.00 |
""" [ekz] Added options to In & Out w/ROC, from the following thread thread:https://www.quantconnect.com/forum/discussion/10763/reproducing-in-out-with-roc-v1-9/p1/comment-31828 [/ekz] --- SEL(stock selection part) Qual Up Based on the 'Quality Companies in an Uptrand' strategy introduced by Chris Cain, 22 Nov 2019 adapted and recoded by Jonathon Tzu and Peter Guenther https://www.quantconnect.com/forum/discussion/9678/quality-companies-in-an-uptrend/p1 https://www.quantconnect.com/forum/discussion/9632/amazing-returns-superior-stock-selection-strategy-superior-in-amp-out-strategy/p2 I/O(in & out part) Option 1: The In & Out algo Based on the 'In & Out' strategy introduced by Peter Guenther, 4 Oct 2020 expanded/inspired by Tentor Testivis, Dan Whitnable (Quantopian), Vladimir, Thomas Chang, Mateusz Pulka, Derek Melchin (QuantConnect), Nathan Swenson, Goldie Yalamanchi, and Sudip Sil https://www.quantopian.com/posts/new-strategy-in-and-out https://www.quantconnect.com/forum/discussion/9597/the-in-amp-out-strategy-continued-from-quantopian/p1 Option 2: The Distilled Bear in & out algo based on Dan Whitnable's 22 Oct 2020 algo on Quantopian. Dan's original notes: "This is based on Peter Guenther great “In & Out” algo. Included Tentor Testivis recommendation to use volatility adaptive calculation of WAIT_DAYS and RET. Included Vladimir's ideas to eliminate fixed constants Help from Thomas Chang" https://www.quantopian.com/posts/new-strategy-in-and-out https://www.quantconnect.com/forum/discussion/9597/the-in-amp-out-strategy-continued-from-quantopian/ """ from OptionsManager import * from QuantConnect.Data.UniverseSelection import * import math import numpy as np import pandas as pd import scipy as sp class QualUp_inout(QCAlgorithm): def Initialize(self): distFromPrice = int(self.GetParameter('distFromPrice'))/100 daysTillExpiry = int(self.GetParameter('daysTillExpiry')) self.OptionsManager = OptionsManager(self, distFromPrice, daysTillExpiry) self.SetStartDate(2015, 1, 1) #Set Start Date #self.SetEndDate(2015, 3, 31) #Set End Date self.cap = 100000 self.SetCash(self.cap) res = Resolution.Hour # Holdings ### 'Out' holdings and weights self.BND1 = self.AddEquity('TLT', res).Symbol #TLT; TMF for 3xlev self.Securities["TLT"].SetDataNormalizationMode(DataNormalizationMode.Raw) # Choose in & out algo self.go_inout_vs_dbear = 0 # 1=In&Out, 0=DistilledBear ##### In & Out parameters ##### # Feed-in constants self.INI_WAIT_DAYS = 15 # out for 3 trading weeks self.wait_days = self.INI_WAIT_DAYS # Market and list of signals based on ETFs self.MRKT = self.AddEquity('SPY', res).Symbol # market self.PRDC = self.AddEquity('XLI', res).Symbol # production (industrials) self.METL = self.AddEquity('DBB', res).Symbol # input prices (metals) self.NRES = self.AddEquity('IGE', res).Symbol # input prices (natural res) self.DEBT = self.AddEquity('SHY', res).Symbol # cost of debt (bond yield) self.USDX = self.AddEquity('UUP', res).Symbol # safe haven (USD) self.GOLD = self.AddEquity('GLD', res).Symbol # gold self.SLVA = self.AddEquity('SLV', res).Symbol # vs silver #self.INFL = self.AddEquity('RINF', res).Symbol # disambiguate GPLD/SLVA pair via inflaction expectations self.TIPS = self.AddEquity('TIP', res).Symbol # disambiguate GPLD/SLVA pair via inflaction expectations; Treasury Yield = TIPS Yield + Expected Inflation self.UTIL = self.AddEquity('XLU', res).Symbol # utilities self.INDU = self.PRDC # vs industrials self.SHCU = self.AddEquity('FXF', res).Symbol # safe haven currency (CHF) self.RICU = self.AddEquity('FXA', res).Symbol # vs risk currency (AUD) self.FORPAIRS = [self.GOLD, self.SLVA, self.UTIL, self.SHCU, self.RICU, self.TIPS] #self.INFL self.SIGNALS = [self.PRDC, self.METL, self.NRES, self.DEBT, self.USDX] self.pairlist = ['G_S', 'U_I', 'C_A'] # Initialize variables ## 'In'/'out' indicator self.be_in = 1 #-1 #initially, set to an arbitrary value different from 1 (in) and 0 (out) self.be_in_prior = 0 #-1 #initially, set to an arbitrary value different from 1 (in) and 0 (out) ## Day count variables self.dcount = 0 # count of total days since start self.outday = (-self.INI_WAIT_DAYS+1) # setting ensures universe updating at algo start ## Flexi wait days self.WDadjvar = self.INI_WAIT_DAYS self.adjwaitdays = self.INI_WAIT_DAYS ## For inflation gauge self.debt1st = [] self.tips1st = [] ##### Distilled Bear parameters (note: some signals shared with In & Out) ##### self.DISTILLED_BEAR = 1 #-1 self.VOLA_LOOKBACK = 126 self.WAITD_CONSTANT = 85 ##### Qual Up parameters ##### self.UniverseSettings.Resolution = res self.AddUniverse(self.UniverseCoarseFilter, self.UniverseFundamentalsFilter) self.num_coarse = 500 self.num_screener = 250 self.num_stocks = 20 self.formation_days = 126 self.lowmom = False self.data = {} self.setrebalancefreq = int(self.GetParameter('reb_freq')) # X days, update universe and momentum calculation self.updatefinefilter = 0 self.symbols = None self.reb_count = 0 self.Schedule.On( self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen('SPY', 30), self.rebalance_when_out_of_the_market) self.Schedule.On( self.DateRules.EveryDay(), self.TimeRules.BeforeMarketClose('SPY', 0), self.record_vars) # Benchmarks self.QQQ = self.AddEquity('QQQ', res).Symbol self.benchmarks = [] self.year = self.Time.year #for resetting benchmarks annually if applicable # Setup daily consolidation symbols = [self.MRKT] + self.SIGNALS + self.FORPAIRS + [self.QQQ] for symbol in symbols: self.consolidator = TradeBarConsolidator(timedelta(days=1)) self.consolidator.DataConsolidated += self.consolidation_handler self.SubscriptionManager.AddConsolidator(symbol, self.consolidator) # Warm up history if self.go_inout_vs_dbear==1: self.lookback = 252 if self.go_inout_vs_dbear==0: self.lookback = 126 self.history = self.History(symbols, self.lookback, Resolution.Daily) if self.history.empty or 'close' not in self.history.columns: return self.history = self.history['close'].unstack(level=0).dropna() def UniverseCoarseFilter(self, coarse): if not (((self.dcount-self.reb_count)==self.setrebalancefreq) or (self.dcount == self.outday + self.adjwaitdays - 1)): self.updatefinefilter = 0 return Universe.Unchanged self.updatefinefilter = 1 # drop stocks which have no fundamental data or have too low prices selected = [x for x in coarse if (x.HasFundamentalData) and (float(x.Price) > 5)] # rank the stocks by dollar volume filtered = sorted(selected, key=lambda x: x.DollarVolume, reverse=True) return [x.Symbol for x in filtered[:self.num_coarse]] def UniverseFundamentalsFilter(self, fundamental): if self.updatefinefilter == 0: return Universe.Unchanged rank_cash_return = sorted(fundamental, key=lambda x: x.ValuationRatios.CashReturn, reverse=True) rank_fcf_yield = sorted(fundamental, key=lambda x: x.ValuationRatios.FCFYield, reverse=True) rank_roic = sorted(fundamental, key=lambda x: x.OperationRatios.ROIC.Value, reverse=True) rank_ltd_to_eq = sorted(fundamental, key=lambda x: x.OperationRatios.LongTermDebtEquityRatio.Value, reverse=True) combo_rank = {} for i,ele in enumerate(rank_cash_return): rank1 = i rank2 = rank_fcf_yield.index(ele) score = sum([rank1*0.5,rank2*0.5]) combo_rank[ele] = score rank_value = dict(sorted(combo_rank.items(), key=lambda item:item[1], reverse=False)) stock_dict = {} # assign a score to each stock, you can also change the rule of scoring here. for i,ele in enumerate(rank_roic): rank1 = i rank2 = rank_ltd_to_eq.index(ele) rank3 = list(rank_value.keys()).index(ele) score = sum([rank1*0.33,rank2*0.33,rank3*0.33]) stock_dict[ele] = score # sort the stocks by their scores self.sorted_stock = sorted(stock_dict.items(), key=lambda d:d[1],reverse=True) self.sorted_symbol = [self.sorted_stock[i][0] for i in range(len(self.sorted_stock))] top= self.sorted_symbol[:self.num_screener] self.symbols = [x.Symbol for x in top] #self.Log("100 fine-filtered stocks\n" + str(sorted([str(i.Value) for i in self.symbols]))) self.updatefinefilter = 0 self.reb_count = self.dcount return self.symbols def OnSecuritiesChanged(self, changes): addedSymbols = [] for security in changes.AddedSecurities: if( security.Symbol.HasUnderlying ): continue # if this is an options security, dont process it. skip it addedSymbols.append(security.Symbol) if security.Symbol not in self.data: self.data[security.Symbol] = SymbolData(security.Symbol, self.formation_days, self) if len(addedSymbols) > 0: history = self.History(addedSymbols, 1 + self.formation_days, Resolution.Daily).loc[addedSymbols] for symbol in addedSymbols: try: self.data[symbol].Warmup(history.loc[symbol]) except: self.Debug(str(symbol)) continue def consolidation_handler(self, sender, consolidated): self.history.loc[consolidated.EndTime, consolidated.Symbol] = consolidated.Close self.history = self.history.iloc[-self.lookback:] if self.go_inout_vs_dbear==1: self.update_history_shift() def update_history_shift(self): self.history_shift = self.history.rolling(11, center=True).mean().shift(60) def derive_vola_waitdays(self): volatility = 0.6 * np.log1p(self.history[[self.MRKT]].pct_change()).std() * np.sqrt(252) wait_days = int(volatility * self.WAITD_CONSTANT) returns_lookback = int((1.0 - volatility) * self.WAITD_CONSTANT) return wait_days, returns_lookback def signalcheck_inout(self): ##### In & Out signal check logic ##### # Returns sample to detect extreme observations returns_sample = (self.history / self.history_shift - 1) # Reverse code USDX: sort largest changes to bottom returns_sample[self.USDX] = returns_sample[self.USDX] * (-1) # For pairs, take returns differential, reverse coded returns_sample['G_S'] = -(returns_sample[self.GOLD] - returns_sample[self.SLVA]) returns_sample['U_I'] = -(returns_sample[self.UTIL] - returns_sample[self.INDU]) returns_sample['C_A'] = -(returns_sample[self.SHCU] - returns_sample[self.RICU]) # Extreme observations; statist. significance = 1% pctl_b = np.nanpercentile(returns_sample, 1, axis=0) extreme_b = returns_sample.iloc[-1] < pctl_b # Re-assess/disambiguate double-edged signals if self.dcount==0: self.debt1st = self.history[self.DEBT] self.tips1st = self.history[self.TIPS] self.history['INFL'] = (self.history[self.DEBT]/self.debt1st - self.history[self.TIPS]/self.tips1st) median = np.nanmedian(self.history, axis=0) abovemedian = self.history.iloc[-1] > median ### Interest rate expectations (cost of debt) may increase because the economic outlook improves (showing in rising input prices) = actually not a negative signal extreme_b.loc[[self.DEBT]] = np.where((extreme_b.loc[[self.DEBT]].any()) & (abovemedian[[self.METL, self.NRES]].any()), False, extreme_b.loc[[self.DEBT]]) ### GOLD/SLVA differential may increase due to inflation expectations which actually suggest an economic improvement = actually not a negative signal extreme_b.loc['G_S'] = np.where((extreme_b.loc[['G_S']].any()) & (abovemedian.loc[['INFL']].any()), False, extreme_b.loc['G_S']) # Determine waitdays empirically via safe haven excess returns, 50% decay self.WDadjvar = int( max(0.50 * self.WDadjvar, self.INI_WAIT_DAYS * max(1, np.where((returns_sample[self.GOLD].iloc[-1]>0) & (returns_sample[self.SLVA].iloc[-1]<0) & (returns_sample[self.SLVA].iloc[-2]>0), self.INI_WAIT_DAYS, 1), np.where((returns_sample[self.UTIL].iloc[-1]>0) & (returns_sample[self.INDU].iloc[-1]<0) & (returns_sample[self.INDU].iloc[-2]>0), self.INI_WAIT_DAYS, 1), np.where((returns_sample[self.SHCU].iloc[-1]>0) & (returns_sample[self.RICU].iloc[-1]<0) & (returns_sample[self.RICU].iloc[-2]>0), self.INI_WAIT_DAYS, 1) )) ) self.adjwaitdays = min(60, self.WDadjvar) return (extreme_b[self.SIGNALS + self.pairlist]).any() def signalcheck_dbear(self): ##### Distilled Bear signal check logic ##### self.adjwaitdays, returns_lookback = self.derive_vola_waitdays() ## Check for Bears returns = self.history.pct_change(returns_lookback).iloc[-1] silver_returns = returns[self.SLVA] gold_returns = returns[self.GOLD] industrials_returns = returns[self.INDU] utilities_returns = returns[self.UTIL] metals_returns = returns[self.METL] dollar_returns = returns[self.USDX] DISTILLED_BEAR = (((gold_returns > silver_returns) and (utilities_returns > industrials_returns)) and (metals_returns < dollar_returns) ) return DISTILLED_BEAR def rebalance_when_out_of_the_market(self): if self.go_inout_vs_dbear==1: out_signal = self.signalcheck_inout() if self.go_inout_vs_dbear==0: out_signal = self.signalcheck_dbear() ##### Determine whether 'in' or 'out' of the market. Perform out trading if applicable ##### if out_signal: self.be_in = False self.outday = self.dcount if self.no_position_held(self.BND1): # Liquidate all except BND1 for symbol, symbol_holding in self.Portfolio.items(): if symbol != self.BND1 and (symbol.Underlying != self.BND1): self.close_position(symbol) self.open_position(self.BND1, 0.85) if (self.dcount >= self.outday + self.adjwaitdays): self.be_in = True # Update stock ranking/holdings, when swithing from 'out' to 'in' plus every X days when 'in' (set rebalance frequency) if (self.be_in and not self.be_in_prior) or (self.be_in and (self.dcount==self.reb_count)): self.rebalance() self.be_in_prior = self.be_in self.dcount += 1 def rebalance(self): if self.symbols is None: return chosen_df = self.calc_return(self.symbols) chosen_df = chosen_df.iloc[:self.num_stocks] # Liquidate BND if not self.no_position_held(self.BND1): self.close_position(self.BND1) # Allocate equally to remaining securities weight = 0.85 / self.num_stocks for symbol, security in self.Securities.items(): if symbol == self.BND1: continue if not self.CurrentSlice.ContainsKey(symbol) or self.CurrentSlice[symbol] is None: continue if symbol not in chosen_df.index: self.close_position(symbol) else: self.open_position(symbol, weight) def calc_return(self, stocks): ret = {} for symbol in stocks: try: ret[symbol] = self.data[symbol].Roc.Current.Value except: self.Debug(str(symbol)) continue df_ret = pd.DataFrame.from_dict(ret, orient='index') df_ret.columns = ['return'] sort_return = df_ret.sort_values(by = ['return'], ascending = self.lowmom) return sort_return def record_vars(self): if self.dcount==1: self.benchmarks = [self.history[self.MRKT].iloc[-2], self.Portfolio.TotalPortfolioValue, self.history[self.QQQ].iloc[-2]] # reset portfolio value and qqq benchmark annually if self.Time.year!=self.year: self.benchmarks = [self.benchmarks[0], self.Portfolio.TotalPortfolioValue, self.history[self.QQQ].iloc[-2]] self.year = self.Time.year # SPY benchmark for main chart spy_perf = self.history[self.MRKT].iloc[-1] / self.benchmarks[0] * self.cap self.Plot('Strategy Equity', 'SPY', spy_perf) # Leverage gauge: cash level self.Plot('Cash level', 'cash', round(self.Portfolio.Cash+self.Portfolio.UnsettledCash, 0)) # Annual saw tooth return comparison: Portfolio VS QQQ saw_portfolio_return = self.Portfolio.TotalPortfolioValue / self.benchmarks[1] - 1 saw_qqq_return = self.history[self.QQQ].iloc[-1] / self.benchmarks[2] - 1 self.Plot('Annual Saw Tooth Returns: Portfolio VS QQQ', 'Annual portfolio return', round(saw_portfolio_return, 4)) self.Plot('Annual Saw Tooth Returns: Portfolio VS QQQ', 'Annual QQQ return', round(float(saw_qqq_return), 4)) ### IN/Out indicator and wait days self.Plot("In Out", "in_market", int(self.be_in)) self.Plot("Wait Days", "waitdays", self.adjwaitdays) ################################################################## def OnData(self, dataSlice): if bool(int(self.GetParameter('tradeOptions'))): pass #self.OptionsManager.DequeueOptionHoldings() def open_position(self, holdingSymbol, holdingWeight): if(self.no_position_held(holdingSymbol)): if bool(int(self.GetParameter('tradeOptions'))): self.OptionsManager.TradeOption(holdingSymbol, holdingWeight) #QueueOptionHoldings(holdingSymbol, holdingWeight) else: self.SetHoldings(holdingSymbol, holdingWeight) def close_position(self, holdingSymbol): if bool(int(self.GetParameter('tradeOptions'))): self.OptionsManager.LiquidateOptionsHoldings(holdingSymbol) else: self.Liquidate(holdingSymbol) def no_position_held(self, holdingSymbol): if bool(int(self.GetParameter('tradeOptions'))): return not self.OptionsManager.PortfolioHasOptionsForThisSymbol(holdingSymbol) else: return not self.Securities[holdingSymbol].Invested class SymbolData(object): def __init__(self, symbol, roc, algorithm): self.Symbol = symbol self.Roc = RateOfChange(roc) self.algorithm = algorithm self.consolidator = algorithm.ResolveConsolidator(symbol, Resolution.Daily) algorithm.RegisterIndicator(symbol, self.Roc, self.consolidator) def Warmup(self, history): for index, row in history.iterrows(): self.Roc.Update(index, row['close'])
from QuantConnect.Securities.Option import OptionStrategies from QuantConnect.Securities.Option import OptionPriceModels class OptionsManager(): def __init__(self, algo, distFromPrice = 0, daysTillExp = 120): self.algo = algo self.OptionOrderQueue = [] self.distFromPrice = distFromPrice # distance of the strike price from trading price (0.10 = 10%) self.daysTillExp = daysTillExp # how many days till expiration, for the contract algo.SetSecurityInitializer(lambda x: x.SetMarketPrice(algo.GetLastKnownPrice(x))) algo.UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw def TradeOption(self, holdingSymbol, holdingWeight): # Search OptionChainProvider for option to select callStrike = self.GetTargetStrikeByPctDist( holdingSymbol,self.distFromPrice ) putStrike = self.GetTargetStrikeByPctDist( holdingSymbol,(-1* self.distFromPrice)) expiryDate = self.GetTargetExpiryByDTE( self.daysTillExp ) contract = self.SelectContract(holdingSymbol, callStrike, expiryDate, OptionRight.Call) if contract is None: return option_contract = self.algo.AddOptionContract(contract, Resolution.Minute) # Purchase self.algo.SetHoldings(option_contract.Symbol, holdingWeight) # ============================================================== # Queue an options order # ============================================================== def QueueOptionHoldings(self, holdingSymbol, holdingWeight): #self.SubscribeToOptionFeed( holdingSymbol ) self.OptionOrderQueue.append((holdingSymbol, holdingWeight)) # ============================================================== # Dequeue a queued options order # ============================================================== def DequeueOptionHoldings(self): if (self.OptionOrderQueue is not None and \ len(self.OptionOrderQueue) > 0): # for holdingTuple in self.OptionsManager.OptionOrderQueue[:]: for index, holdingTuple in enumerate(self.OptionOrderQueue[:]): holdingSymbol = holdingTuple[0] holdingWeight = holdingTuple[1] if(self.SetOptionsHoldings(holdingSymbol, holdingWeight)): self.OptionOrderQueue.remove(holdingTuple) # ==================================================================== # Execute an options Order -- set options holdings for given stock # ==================================================================== def SetOptionsHoldings(self, holdingSymbol, holdingWeight): callStrike = self.GetTargetStrikeByPctDist( holdingSymbol,self.distFromPrice ) putStrike = self.GetTargetStrikeByPctDist( holdingSymbol,(-1* self.distFromPrice)) expiryDate = self.GetTargetExpiryByDTE( self.daysTillExp ) # Buy a single Call Option # ------------------------------------------- callContract, callOrderMsg = self.GetCallOrPut(holdingSymbol, callStrike, expiryDate, OptionRight.Call ) if( callContract is not None ): self.algo.SetHoldings(callContract, holdingWeight, False, callOrderMsg) return True else: self.algo.Debug(f"{self.algo.Time} - Failed to get buy calls for {holdingSymbol}") return False # ============================================================== # Liquidate an Options Holdings for the given symbol # ============================================================== def LiquidateOptionsHoldings(self, holdingsSymbol): self.LiquidateOptionsOfType(holdingsSymbol, OptionRight.Call) # ============================================================== # Get Target Strike Given Pct Distnace # ============================================================== def GetTargetStrikeByPctDist( self, symbolArg, pctDist): theCurrPrice = self.algo.Securities[symbolArg].Price theStrikePrice = theCurrPrice * (1 + (pctDist) ) return theStrikePrice # ============================================================== # Get Target Expiry Given DTE Days # ============================================================== def GetTargetExpiryByDTE( self, dteDays): theExpiration = self.algo.Time + timedelta(days=dteDays) return theExpiration # ============================================================== # Get Strike & Expiration, fiven # ============================================================== def GetStrikeAndExpiration( self, symbolArg, pctDist, dteDays ): theCurrPrice = self.algo.Securities[symbolArg].Price theExpiration = self.algo.Time + timedelta(days=dteDays) theStrikePrice = theCurrPrice * (1 + (pctDist) ) return theStrikePrice , theExpiration # ============================================================== # Get Call or Put # ============================================================== def GetCallOrPut(self, symbolArg, callStrike, expiryDTE, optionRightArg = OptionRight.Call ): # retrive closest call contracts # ------------------------------- selectedContract = self.SelectContract(symbolArg, callStrike, expiryDTE, optionRightArg) if( selectedContract is None): return None, None addedContract = self.algo.AddOptionContract(selectedContract, Resolution.Minute) # construct orer message # ------------------- underlyingPrice = self.algo.Securities[symbolArg].Price callOrPutString = "CALL" if (optionRightArg == OptionRight.Call) else "PUT" orderMsg = f"[BUY {callOrPutString}] {symbolArg}-{str(round(addedContract.StrikePrice,0))} | Stock @ $ {str(underlyingPrice)}" return (selectedContract, orderMsg) # ============================================================== # Subscribe to the Option feed # ============================================================== # todo: explore how to unsubscribe from the option order feed def SubscribeToOptionFeed(self, symbolArg): option = self.algo.AddOption(symbolArg, Resolution.Minute) # set our strike/expiry filter for this option chain # ---------------------------------------------------------- option.SetFilter(-5, +5, timedelta(0), timedelta(30)) # for greeks and pricer (needs some warmup?) # https://github.com/QuantConnect/Lean/blob/21cd972e99f70f007ce689bdaeeafe3cb4ea9c77/Common/Securities/Option/OptionPriceModels.cs#L81 # ------------------------------------------------ option.PriceModel = OptionPriceModels.CrankNicolsonFD() # both European & American, automatically # ============================================================== # Select a Contract, given a symbol, desired strike and expiration # ============================================================== def SelectContract(self, symbolArg, strikePriceArg, expirationArg, optionRightArg): contracts = self.algo.OptionChainProvider.GetOptionContractList(symbolArg, self.algo.Time) if len( contracts ) == 0: return # get all contracts that match type # ------------------------------------ filteredContracts = [symbol for symbol in contracts if symbol.ID.OptionRight == optionRightArg] # sort contracts by expiry dates and select expiration closest to desired expiration; check that it meets min time to expiry # -------------------------------------------- contractsSortedByExpiration = sorted(filteredContracts, key=lambda p: abs(p.ID.Date - expirationArg), reverse=False) min_expiry = int(self.algo.GetParameter('reb_freq')) contractsSortedByExpiration = [p for p in contractsSortedByExpiration if ((p.ID.Date - self.algo.Time).days > min_expiry+5)] if len(contractsSortedByExpiration) == 0: return closestExpirationDate = contractsSortedByExpiration[0].ID.Date # get all contracts for selected expiration # ------------------------------------------------ contractsFilteredByExpiration = [contract for contract in contractsSortedByExpiration if contract.ID.Date == closestExpirationDate] # sort contracts and select the one closest to desired strike # ----------------------------------------------------------- contractsSortedByStrike = sorted(contractsFilteredByExpiration, key=lambda p: abs(p.ID.StrikePrice - strikePriceArg), reverse=False) theOptionContract = contractsSortedByStrike[0] return theOptionContract # ================================================================== # Liquidate all positions of the given type,for the given symbol. # ================================================================== def LiquidateOptionsOfType(self, symbolArg, optionRightArg = OptionRight.Call, orderMsgArg="Liquidated"): for symbolKey in self.algo.Securities.Keys: portfolioPosition = self.algo.Securities[symbolKey] if portfolioPosition.Invested: # Manage Option positions # -------------------------------- if( portfolioPosition.Type == SecurityType.Option ) and \ ( portfolioPosition.Underlying.Symbol == symbolArg) and \ ( portfolioPosition.Right == optionRightArg): profitPct = round(portfolioPosition.Holdings.UnrealizedProfitPercent * 100,2) orderMsgArg = f"{orderMsgArg} | Profit: {profitPct}%" self.algo.Debug(f"{self.algo.Time} - {orderMsgArg}") self.algo.Liquidate(symbolKey, orderMsgArg) self.algo.RemoveSecurity(symbolKey) # Remove option contract subsubscription # ============================================================== # Return True if we are holding options for this underlying # ============================================================== def PortfolioHasOptionsForThisSymbol(self, underlyingSymbol): # cycle through holdings and check if there are options # positions open for this underlying.symbol. # ------------------------------------------------------ optionInPortfolioForThisSecurity = False for symbolKey in self.algo.Securities.Keys: currentHolding = self.algo.Securities[symbolKey] if currentHolding.Invested: try: if (currentHolding.Underlying.Symbol == underlyingSymbol): optionInPortfolioForThisSecurity = True break except: timestamp = self.algo.Time.strftime('%b %d %Y') # todo: Explore why this is ever hit. Happens a lot with NVDA in march 2018 self.algo.Debug(f"[{timestamp}] [ERROR] PortfolioHasOptionsForThisSymbol(): Could not get underlying for {symbolKey}") # self.Debug(f"[{timestamp}] [exception] It's possible that assignment has occured") return optionInPortfolioForThisSecurity
from bisect import bisect_left def GetIndexOfClosestValueFromList(myList, myNumber): """ Assumes myList is sorted. Returns closest value to myNumber. If two numbers are equally close, return the smallest number. Credit from: https://stackoverflow.com/questions/12141150/from-list-of-integers-get-number-closest-to-a-given-value/12141511#12141511 """ pos = bisect_left(myList, myNumber) if pos == 0: return 0 if pos == len(myList): return len(myList)-1 before = myList[pos - 1] after = myList[pos] if after - myNumber < myNumber - before: return pos else: return pos-1 def GetClosestContractFromList(myList, myNumber): """ Assumes myList is sorted. Returns closest value to myNumber. If two numbers are equally close, return the smallest number. Credit from: https://stackoverflow.com/questions/12141150/from-list-of-integers-get-number-closest-to-a-given-value/12141511#12141511 """ pos = bisect_left(myList, myNumber) if pos == 0: return myList[0] if pos == len(myList): return myList[-1] before = myList[pos - 1] after = myList[pos] if after - myNumber < myNumber - before: return after else: return before