Overall Statistics |
Total Trades 0 Average Win 0% Average Loss 0% Compounding Annual Return 0% Drawdown 0% Expectancy 0 Net Profit 0% Sharpe Ratio 0 Probabilistic Sharpe Ratio 0% Loss Rate 0% Win Rate 0% Profit-Loss Ratio 0 Alpha 0 Beta 0 Annual Standard Deviation 0 Annual Variance 0 Information Ratio 2.898 Tracking Error 0.12 Treynor Ratio 0 Total Fees $0.00 Estimated Strategy Capacity $0 Lowest Capacity Asset |
#region imports from AlgorithmImports import * import datetime as dt #endregion CASH = 100000 START_DATE = '01-01-2022' #'DD-MM-YYYY' END_DATE = '05-01-2022' # 'DD-MM-YYYY', Can be set as None FREE_PORTFOLIO_VALUE_PCT = 0.025 BULL_NUMBER = 4 BEAR_NUMBER = 1 FIXED_SL = 1.5 FIXED_TARGET = 0.15 DAYS_TO_EXPIRY = 45 STRIKE_DISTANCE = 0.05 BUY_LOTS = 1 SELL_LOTS = 1 HV_LOOKBACK = 600 # Universe filters PRICE_FLOOR = 20 PRICE_CEIL = 40 VOLUME_FLOOR = 500000 NUMBER_FROM_UNIVERSE = 100 IV_FLOOR = 0.4 IV_CEIL = 0.6 ############### Parsing Logic ############# ######## Do not edit anything below ###### START_DATE = dt.datetime.strptime(START_DATE, '%d-%m-%Y') if END_DATE: END_DATE = dt.datetime.strptime(END_DATE, '%d-%m-%Y')
''' Portfolio construction ---------------------- First, we define a start day when we want to perform analysis (for example 19 March 2022) and then run it continuously until a certain day (1 September 2022). The broker fee structure is set to Interactive Brokers. We set the resolution to hourly. Next, we allocate some capital (for example, we set the portfolio to 30 000$ ) and define how many assets that to be invested: 6 bull and 4 bear options spreads. A bull spread is when we buy a call (ITM) and sell the call (OTM) with a higher strike. Our bull spread will be Long leg= market price -0.5%; Short leg = market price + 0.5% A bear spread is when we buy a call and sell the call with a lower strike. Our bear spread will be: Long leg= market price +0.5%; Short leg = market price - 0.5% Universe Selection ------------------ We define the universe of equities that we would like to scan using the following criteria : totalVolume price more than 500,000 (average daily trading volume) stock price more than 10 and less than 90; IV more than 40 after that, we take 6 top performers for bull and 4 worst performers for bear using MACD (confirm current trend) sorted by 10 days exponential Moving Average (EMA) removing outliers (35%+) once we know the equities we will be investing in, we buy appropriate call spreads using the definition above. when we purchase our options, we capture (i.e. log file) IV, greeks (Delta, Gamma, Vega, Vomma,T heta, Rho) as well as a spread (Bid/Ask) as well as the price we got our order filled at. On Liquidation of any asset, get another equity from current selection and enter that one. Do not reenter an equity on same day. Portfolio monitoring -------------------- we set up a "stop loss" rule: we sell our spread if it loses 25% of its initial value we set up a "sell-off" rule: we sell if our spread increases in price by 15% when one option spread is liquidated (be it due to "stop loss" or "sell-off"), we replace it with another contract (for example: if we sell a bull spread, then we have 5 bull spreads left, so we add one more bull spread). the portfolio should be fully invested at all times. spread call strikes can be rounded, but have to be equally spaced. for example stock at $51.89, then we choose a medium point to be $52 then our differential is $52*0.005=0.26; if there are no options with strikes 51.74 and 52.26, then we take the nearest pair (51.75 and 52.25) or (51.50 and 52.50) or (51 and 53) ''' # region imports from AlgorithmImports import * from risk_management import FixedStopRMModel import pandas as pd from constants import * import pickle from numpy import sqrt,mean,log,diff from scipy import stats # endregion class VerticalSpread(QCAlgorithm): def Initialize(self): self.SetBacktestDetails() # setup state storage in initialize method self.stateData = {} self.option_symbols = {} self.new_day = True self.AddUniverse(self.CoarseFilterFunction) self.equity_store = pd.DataFrame(columns=['equities']) #------------------------------------------------------------------------------- def SetBacktestDetails(self): """Set the backtest details.""" self.SetStartDate(START_DATE) if END_DATE: self.SetEndDate(END_DATE) self.SetCash(CASH) self.SetWarmup(30, Resolution.Daily) #--------------------------------------------------------------------------------- def OnData(self, slice): if not self.IsWarmingUp and self.Time.hour==10 and self.new_day: # Filters coarse selected equities based on Expiry, Right and IV # Sets self.bulls as top equities on this criteria self.bulls = pd.DataFrame(columns=['IV','HV','HV_Percentile']) for symbol in self.selected: slice = self.CurrentSlice chain = slice.OptionChains.get(self.option_symbols[symbol]) if not chain: continue # sorted the optionchain by expiration date and choose the furthest date for the given dates to expiry expiry = sorted(chain, key=lambda x: abs((x.Expiry - self.Time).days - DAYS_TO_EXPIRY))[0].Expiry # filter the call options from the contracts expires on that date target_calls = [i for i in chain if i.Expiry == expiry and i.Right == OptionRight.Call] calls = [i for i in target_calls if IV_CEIL > i.ImpliedVolatility > IV_FLOOR] calls = sorted(calls,key=lambda x: x.ImpliedVolatility,reverse=True) if not calls: continue self.bulls.loc[symbol,'IV'] = float(calls[0].ImpliedVolatility) self.bulls.loc[symbol,'HV'],self.bulls.loc[symbol,'HV_Percentile'] = self.stateData[symbol].calcHV(self,HV_LOOKBACK) self.bulls = self.bulls.astype(float) self.bulls = self.bulls.loc[self.bulls['HV'] > (self.bulls['IV']*1.2)] self.bulls = self.bulls.loc[self.bulls['HV_Percentile'].lt(35)] self.bulls = self.bulls.nlargest(BULL_NUMBER,'IV') if not self.bulls.empty: self.Log(f'Top {BULL_NUMBER} bulls for {self.Time.date()} are {self.bulls.index.to_list()}') self.equity_store.loc[self.Time.date()] = [[x.Value for x in self.bulls.index.to_list()]] self.new_day = False else: self.Log(f'No bulls passed the criteria for {self.Time.date()}') def OnEndOfAlgorithm(self): # serializedString = pickle.dumps( self.ObjectStore.Save("DKAVS", self.equity_store.reset_index().to_json(date_unit='ns')) # self.ObjectStore.Save("DKAVS", serializedString) #--------------------------------------------------------------------------------- def OnSecuritiesChanged (self, changes): for x in changes.AddedSecurities: if (x.Symbol.SecurityType != SecurityType.Equity) or (x.Symbol.Value=='SPY'): continue option = self.AddOption(x.Symbol.Value, Resolution.Minute) option.SetFilter(-1, +1, timedelta(DAYS_TO_EXPIRY-15), timedelta(DAYS_TO_EXPIRY+15)) option.PriceModel = OptionPriceModels.CrankNicolsonFD() self.option_symbols[x.Symbol] = option.Symbol for x in changes.RemovedSecurities: if (x.Symbol.SecurityType != SecurityType.Equity) or (x.Symbol.Value=='SPY'): continue self.RemoveOptionContract(self.option_symbols[x.Symbol]) #--------------------------------------------------------------------------------- def CoarseFilterFunction(self, coarse: List[CoarseFundamental]) -> List[Symbol]: # We are going to use a dictionary to refer the object that will keep the moving averages onlyEquities = [x for x in coarse if x.HasFundamentalData] for c in onlyEquities: if c.Symbol not in self.stateData: self.stateData[c.Symbol] = SelectionData(c.Symbol, 10) # Updates the SymbolData object with current EOD price avg = self.stateData[c.Symbol] avg.update(c.EndTime, c.AdjustedPrice, c.DollarVolume) if self.IsWarmingUp: return [] # Filter the values of the dict to those above EMA and more than $500000 vol. values = [x for x in self.stateData.values() if (PRICE_FLOOR < x.price < PRICE_CEIL) and x.volume > VOLUME_FLOOR] values = [x for x in values if (x.macd.Current.Value > x.macd.Signal.Current.Value) and (x.macd.Current.Value > 0)] # sort by the largest in ema. values.sort(key=lambda x: x.ema, reverse=True) self.selected = [x.symbol for x in values] invested = [x.Key for x in self.Portfolio if x.Value.Invested] # we need to return only the symbol objects return self.selected + invested #----------------------------------------------------------------------------------------- # def OnOrderEvent(self, orderEvent: OrderEvent) -> None: # order = self.Transactions.GetOrderById(orderEvent.OrderId) # if orderEvent.Status == OrderStatus.Filled: # self.Debug(f"{self.Time}: {order.Type}: {orderEvent}: {orderEvent.Symbol} Filled at {orderEvent.FillPrice}") #---------------------------------------------------------------------------------------- def OnEndOfDay(self): self.new_day = True self.Log(f'Total Securities in Universe: {len(self.ActiveSecurities)}') #-------------------------------------------------------------------------------------- class SelectionData(object): def __init__(self, symbol, period): self.symbol = symbol self.ema = ExponentialMovingAverage(period) self.macd = MovingAverageConvergenceDivergence(12, 26, 9) self.is_above_ema = False self.volume = 0 def update(self, time, price, volume): self.price = price self.volume = volume self.ema.Update(time, price) self.macd.Update(time, price) def calcHV(self, algo, HVLookback): closes = algo.History(self.symbol, HVLookback, Resolution.Daily).close diffs = diff(log(closes)) diffs_mean = mean(diffs) diff_square = [(diffs[i]-diffs_mean)**2 for i in range(0,len(diffs))] sigma = sqrt(sum(diff_square)*(1.0/(len(diffs)-1))) self.HV = round(sigma*sqrt(252),2) self.HVP = round(stats.percentileofscore(closes.iloc[-252:], closes.iloc[-1]),2) return self.HV,self.HVP
''' Portfolio construction ---------------------- First, we define a start day when we want to perform analysis (for example 19 March 2022) and then run it continuously until a certain day (1 September 2022). The broker fee structure is set to Interactive Brokers. We set the resolution to hourly. Next, we allocate some capital (for example, we set the portfolio to 30 000$ ) and define how many assets that to be invested: 6 bull and 4 bear options spreads. A bull spread is when we buy a call (ITM) and sell the call (OTM) with a higher strike. Our bull spread will be Long leg= market price -0.5%; Short leg = market price + 0.5% A bear spread is when we buy a call and sell the call with a lower strike. Our bear spread will be: Long leg= market price +0.5%; Short leg = market price - 0.5% Universe Selection ------------------ We define the universe of equities that we would like to scan using the following criteria : totalVolume price more than 500,000 (average daily trading volume) stock price more than 10 and less than 90; IV more than 40 after that, we take 6 top performers for bull and 4 worst performers for bear using MACD (confirm current trend) sorted by 10 days exponential Moving Average (EMA) removing outliers (35%+) once we know the equities we will be investing in, we buy appropriate call spreads using the definition above. when we purchase our options, we capture (i.e. log file) IV, greeks (Delta, Gamma, Vega, Vomma,T heta, Rho) as well as a spread (Bid/Ask) as well as the price we got our order filled at. On Liquidation of any asset, get another equity from current selection and enter that one. Do not reenter an equity on same day. Portfolio monitoring -------------------- we set up a "stop loss" rule: we sell our spread if it loses 25% of its initial value we set up a "sell-off" rule: we sell if our spread increases in price by 15% when one option spread is liquidated (be it due to "stop loss" or "sell-off"), we replace it with another contract (for example: if we sell a bull spread, then we have 5 bull spreads left, so we add one more bull spread). the portfolio should be fully invested at all times. spread call strikes can be rounded, but have to be equally spaced. for example stock at $51.89, then we choose a medium point to be $52 then our differential is $52*0.005=0.26; if there are no options with strikes 51.74 and 52.26, then we take the nearest pair (51.75 and 52.25) or (51.50 and 52.50) or (51 and 53) ''' # region imports from AlgorithmImports import * from risk_management import FixedStopRMModel import pandas as pd from constants import * import pickle # endregion class VerticalSpread(QCAlgorithm): def Initialize(self): self.SetBacktestDetails() # setup state storage in initialize method self.stateData = {} self.option_symbols = {} self.SetWarmup(30,Resolution.Daily) self.LoadEquityStore() self.AddUniverse(self.CoarseFilterFunction) self.AddRiskManagement(FixedStopRMModel(self)) self.Schedule.On(self.DateRules.EveryDay(),self.TimeRules.At(15,0),self.ExitBeforeExpiry) self.open_bulls = {} self.open_bears = {} self.closed_today = [] def LoadEquityStore(self): self.equity_store = pd.read_json(self.ObjectStore.Read("DKAVS")) self.equity_store['index'] = pd.to_datetime(self.equity_store['index']).dt.date self.equity_store.set_index('index',inplace=True) #------------------------------------------------------------------------------- def SetBacktestDetails(self): """Set the backtest details.""" self.SetStartDate(START_DATE) if END_DATE: self.SetEndDate(END_DATE) self.SetCash(CASH) self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin) self.SetWarmup(30, Resolution.Daily) # Adjust the cash buffer from the default 2.5% to custom setting self.Settings.FreePortfolioValuePercentage = FREE_PORTFOLIO_VALUE_PCT # self.Settings.DataSubscriptionLimit = 500 #--------------------------------------------------------------------------------- def OnData(self, slice): if not self.IsWarmingUp: if len(self.open_bulls) < BULL_NUMBER: # We'll filter equities based on their options IV in the first minute self.bulls = pd.DataFrame(columns=['IV','Filtered_Calls']) self.GetBulls() if self.bulls.empty: return # for bull in self.bulls.index: # sd = self.stateData[bull] # self.Log(f'Bull: {bull.Value}; with ema {sd.ema.Current.Value}') # self.Log(f'Bull: {bull.Value}; with macd signal {sd.macd.Signal.Current.Value}') self.OpenBullCallSpreads(self.bulls) #--------------------------------------------------------------------------------- def GetBulls(self): # Filters coarse selected equities based on Expiry, Right and IV # Sets self.bulls as top equities on this criteria for symbol in self.selected: slice = self.CurrentSlice chain = slice.OptionChains.get(self.option_symbols[symbol]) if not chain: continue # sorted the optionchain by expiration date and choose the furthest date for the given dates to expiry expiry = sorted(chain, key=lambda x: abs((x.Expiry - self.Time).days - DAYS_TO_EXPIRY))[0].Expiry # filter the call options from the contracts expires on that date target_calls = [i for i in chain if i.Expiry == expiry and i.Right == OptionRight.Call] calls = [i for i in target_calls if IV_CEIL > i.ImpliedVolatility > IV_FLOOR] calls = sorted(calls,key=lambda x: x.ImpliedVolatility,reverse=True) if not calls: continue self.bulls.loc[symbol,'IV'] = float(calls[0].ImpliedVolatility) self.bulls.loc[symbol,'Filtered_Calls'] = target_calls exclusions = self.GetExclusions() # Exclude already invested and same day liquidated equities self.bulls = self.bulls[~self.bulls.index.isin(exclusions)] self.bulls['IV'] = self.bulls['IV'].astype(float) self.bulls = self.bulls.nlargest(BULL_NUMBER - len(self.open_bulls),'IV') #------------------------------------------------------------------------------------------ def GetExclusions(self): portfolio_syms = [x[0].Underlying for x in self.open_bulls.keys()] today_liquidated_syms = [x[0].Underlying for x in self.closed_today] return portfolio_syms + today_liquidated_syms #------------------------------------------------------------------------------------------ def GetOptionsForSymbol(self,symbol,calls): slice = self.CurrentSlice underlying_price = slice[symbol].Price itm_calls = [x for x in calls if x.Strike < underlying_price] itm_calls = sorted(itm_calls, key=lambda x: abs(x.Strike - (underlying_price*(1-STRIKE_DISTANCE)))) otm_calls = [x for x in calls if x.Strike > underlying_price] otm_calls = sorted(otm_calls, key=lambda x: abs(x.Strike - (underlying_price*(1+STRIKE_DISTANCE)))) return itm_calls, otm_calls, underlying_price #--------------------------------------------------------------------------------- def OpenBullCallSpreads(self, bulls): for symbol,row in bulls.iterrows(): itm_calls,otm_calls,uprice = self.GetOptionsForSymbol(symbol,row['Filtered_Calls']) if (not itm_calls) or (not otm_calls): return self.Log(f'Underlying Price: {uprice}') # Buy call option contract with lower strike self.Buy(itm_calls[0].Symbol, BUY_LOTS) self.Log(f'Bought {itm_calls[0].Symbol} with attributes-> Expiry: {itm_calls[0].Expiry}, \ Strike: {itm_calls[0].Strike}, Ask: {itm_calls[0].AskPrice}, \ Delta: {itm_calls[0].Greeks.Delta}, Gamma: {itm_calls[0].Greeks.Gamma}, \ Vega: {itm_calls[0].Greeks.Vega}, Theta: {itm_calls[0].Greeks.Theta}, Rho: {itm_calls[0].Greeks.Rho}') # Sell call option contract with higher strike self.Sell(otm_calls[0].Symbol, SELL_LOTS) self.Log(f'Sold {otm_calls[0].Symbol} with attributes-> Expiry: {otm_calls[0].Expiry}, \ Strike: {otm_calls[0].Strike}, Bid: {otm_calls[0].BidPrice}, \ Delta: {otm_calls[0].Greeks.Delta}, Gamma: {otm_calls[0].Greeks.Gamma}, \ Vega: {otm_calls[0].Greeks.Vega}, Theta: {otm_calls[0].Greeks.Theta}, Rho: {otm_calls[0].Greeks.Rho}') self.open_bulls[(itm_calls[0].Symbol,otm_calls[0].Symbol)] = self.Time #--------------------------------------------------------------------------------- def OnSecuritiesChanged (self, changes): for x in changes.AddedSecurities: x.SetFeeModel(CustomFeeModel()) if (x.Symbol.SecurityType != SecurityType.Equity) or (x.Symbol.Value=='SPY'): continue option = self.AddOption(x.Symbol.Value, Resolution.Minute) option.SetFilter(-20, +20, timedelta(DAYS_TO_EXPIRY-15), timedelta(DAYS_TO_EXPIRY+15)) option.PriceModel = OptionPriceModels.CrankNicolsonFD() self.option_symbols[x.Symbol] = option.Symbol for x in changes.RemovedSecurities: if (x.Symbol.SecurityType != SecurityType.Equity) or (x.Symbol.Value=='SPY'): continue self.RemoveOptionContract(self.option_symbols[x.Symbol]) #--------------------------------------------------------------------------------- def CoarseFilterFunction(self, coarse: List[CoarseFundamental]) -> List[Symbol]: # We are going to use a dictionary to refer the object that will keep the moving averages onlyEquities = [x for x in coarse if x.HasFundamentalData] if self.IsWarmingUp: return [] if (self.Time.date() not in self.equity_store.index): return [] today_equities = self.equity_store.loc[self.Time.date()].iloc[0] self.selected = [x.Symbol for x in onlyEquities if x.Symbol.Value in today_equities] invested = [x.Key for x in self.Portfolio if x.Value.Invested] # we need to return only the symbol objects return self.selected + invested #----------------------------------------------------------------------------------------- # def OnOrderEvent(self, orderEvent: OrderEvent) -> None: # order = self.Transactions.GetOrderById(orderEvent.OrderId) # if orderEvent.Status == OrderStatus.Filled: # self.Debug(f"{self.Time}: {order.Type}: {orderEvent}: {orderEvent.Symbol} Filled at {orderEvent.FillPrice}") #---------------------------------------------------------------------------------------- def OnEndOfDay(self): self.closed_today = [] self.Log(f'Total Securities in Universe: {len(self.ActiveSecurities)}') #--------------------- Exit before Expiry and Dividend -------------------------------------- def ExitBeforeExpiry(self): to_be_removed = [] for (contract1,contract2) in self.open_bulls.keys(): slice = self.CurrentSlice security1 = self.Securities[contract1] security2 = self.Securities[contract2] if (security1.Expiry.date()==self.Time.date()) or (slice.Dividends.get(contract1.Underlying)): self.ExitOption(contract1.Underlying,contract1,'ExitBeforeExpiry') self.ExitOption(contract2.Underlying,contract2,'ExitBeforeExpiry') to_be_removed.append((contract1,contract2)) self.Log(f'{self.Time}--> Exited before Expiry {security1.Expiry} of {contract1}') self.Log(f'{self.Time}--> Exited before Expiry {security2.Expiry} of {contract2}') for contracts in to_be_removed: del self.open_bulls[contracts] #------------------------------------------------------------------------------------------ def ExitOption(self,symbol,contract,msg): self.Liquidate(contract, msg) #-------------------------------------------------------------------------------------- class CustomFeeModel: def GetOrderFee(self, parameters): fee = 0 return OrderFee(CashAmount(fee, 'USD'))
#region imports from AlgorithmImports import * from constants import * #endregion # Your New Python File class FixedStopRMModel(RiskManagementModel): '''Provides an implementation of IRiskManagementModel that limits the maximum possible loss measured from the highest unrealized profit - Uses Fixed Stop ''' def __init__(self, algo): '''Initializes a new instance of the FixedStopRMModel class Args: maximumDrawdownPercent: The maximum percentage drawdown allowed for algorithm portfolio compared with the highest unrealized profit, defaults to 5% drawdown''' self.algo = algo def ManageRisk(self, algorithm, targets): '''Manages the algorithm's risk at each time step Args: algorithm: The algorithm instance targets: The current portfolio targets to be assessed for risk ''' riskAdjustedTargets = list() bulls = [x for x in self.algo.open_bulls.keys()] for (self.sym1,self.sym2) in bulls: if algorithm.Time == self.algo.open_bulls[(self.sym1,self.sym2)]: continue self.security1 = algorithm.Securities[self.sym1] self.security2 = algorithm.Securities[self.sym2] # Remove if not invested if not self.security1.Invested or not self.security2.Invested: continue profit1 = self.security1.Holdings.UnrealizedProfit value1 = self.security1.Holdings.HoldingsCost profit2 = self.security2.Holdings.UnrealizedProfit value2 = self.security2.Holdings.HoldingsCost self.profitPercent = (profit1 + profit2)/(value1+value2) stop = FIXED_SL*-1 target = FIXED_TARGET if (self.profitPercent <= stop): riskAdjustedTargets = self.ExitOptions(algorithm, riskAdjustedTargets,criteria='stop') elif (self.profitPercent >= target): riskAdjustedTargets = self.ExitOptions(algorithm, riskAdjustedTargets,criteria='target') return riskAdjustedTargets def ExitOptions(self, algorithm, riskAdjustedTargets,criteria): algorithm.Log(f'''Liquidated as spread for {(self.sym1.Value,self.sym2.Value)} reached {criteria} at PnL%: {self.profitPercent} at Underlying: {self.security1.Underlying.Price}''') riskAdjustedTargets.append(PortfolioTarget(self.sym1, 0)) riskAdjustedTargets.append(PortfolioTarget(self.sym2, 0)) del self.algo.open_bulls[(self.sym1,self.sym2)] self.algo.closed_today.append((self.sym1,self.sym2)) return riskAdjustedTargets