Overall Statistics |
Total Trades 60 Average Win 0.25% Average Loss -0.25% Compounding Annual Return 22.720% Drawdown 18.200% Expectancy 0.889 Net Profit 29.144% Sharpe Ratio 1.303 Probabilistic Sharpe Ratio 56.814% Loss Rate 4% Win Rate 96% Profit-Loss Ratio 0.98 Alpha 0.01 Beta 1.705 Annual Standard Deviation 0.193 Annual Variance 0.037 Information Ratio 1.276 Tracking Error 0.086 Treynor Ratio 0.147 Total Fees $38.00 |
class QuantumVerticalInterceptor(QCAlgorithm): def Initialize(self): self.SetStartDate(2020, 1, 1) self.SetEndDate(2020, 1, 7) self.SetCash(10000) self.DELTA_TARGET=0.5 self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Cash) self.df_calls = None #set a risk limit of 1% of portfolio value self.investment_limit = self.Portfolio.TotalPortfolioValue * 0.01 # Add the option option = self.AddOption("SPY") self.optionSymbol = option.Symbol # Add the initial contract filter option.SetFilter(-5, +5, 5, 10) # Define the Option Price Model option.PriceModel = OptionPriceModels.CrankNicolsonFD() #option.PriceModel = OptionPriceModels.BlackScholes() #option.PriceModel = OptionPriceModels.AdditiveEquiprobabilities() #option.PriceModel = OptionPriceModels.BaroneAdesiWhaley() #option.PriceModel = OptionPriceModels.BinomialCoxRossRubinstein() #option.PriceModel = OptionPriceModels.BinomialJarrowRudd() #option.PriceModel = OptionPriceModels.BinomialJoshi() #option.PriceModel = OptionPriceModels.BinomialLeisenReimer() #option.PriceModel = OptionPriceModels.BinomialTian() #option.PriceModel = OptionPriceModels.BinomialTrigeorgis() #option.PriceModel = OptionPriceModels.BjerksundStensland() #option.PriceModel = OptionPriceModels.Integral() # Set warm up with 30 trading days to warm up the underlying volatility model self.SetWarmUp(30, Resolution.Daily) def OnData(self,slice): self.Plot("Portfolio", "Margin Remaining", self.Portfolio.MarginRemaining) # Remaining margin on the account self.Plot("Portfolio", "Margin Used", self.Portfolio.TotalMarginUsed) # Sum of margin used across all securities if self.IsWarmingUp or not slice.OptionChains.ContainsKey(self.optionSymbol): return chain = slice.OptionChains[self.optionSymbol] #set float format so delta displays correctly pd.set_option('display.float_format', lambda x: '%.5f' % x) #put the relevant data into the dataframe df = pd.DataFrame([[x.Right,float(x.Strike),x.Expiry,float(x.BidPrice),float(x.AskPrice),x.Greeks.Delta,x.UnderlyingLastPrice] for x in chain], index=[x.Symbol.Value for x in chain], columns=['type', 'strike', 'expiry', 'ask price', 'bid price', 'delta','underlyinglast']) #ensure expiry column is in datetime format df['expiry'] = pd.to_datetime(df['expiry']) # sort by expiry, descending df.sort_values(by=['expiry'],ascending=False) # get the most future date furthest_date = df['expiry'].iloc[0] # keep only those rows which have that furthest date df = df[df.expiry == furthest_date] #split the dataframe into calls and puts (calls are 0, puts are 1) self.df_calls = df[df.type==0] #sort by delta self.df_calls.sort_values(by=['delta'],ascending=False) #select the closest two records to the DELTA TARGET #try: uppercall_ind = self.df_calls[self.df_calls.delta<self.DELTA_TARGET].delta.idxmax() lowercall_ind = self.df_calls[self.df_calls.delta>self.DELTA_TARGET].delta.idxmin() self.df_calls = self.df_calls[self.df_calls.index.isin([lowercall_ind,uppercall_ind])] spread_value = self.df_calls.at[lowercall_ind,'bid price'] - self.df_calls.at[uppercall_ind,'ask price'] max_risk = self.df_calls.at[uppercall_ind,'strike'] - self.df_calls.at[lowercall_ind,'strike'] max_risk_contract = max_risk * 100 max_investment = math.trunc(self.investment_limit / max_risk_contract) self.Sell(OptionStrategies.BearCallSpread(self.optionSymbol, self.df_calls.at[lowercall_ind,'strike'], self.df_calls.at[uppercall_ind,'strike'] , self.df_calls.at[uppercall_ind,'expiry']), max_investment) #except: # return
from QuantConnect.Securities.Option import OptionPriceModels from QuantConnect.Securities import GetMaximumOrderQuantityForTargetBuyingPowerParameters from datetime import timedelta import decimal as d from my_calendar import last_trading_day import pandas as pd class DeltaHedgedStraddleAlgo(QCAlgorithm): def Initialize(self): self.SetStartDate(2017, 1, 6) self.SetEndDate(2018, 4, 6) self.SetCash(100000) #self.Benchmark("SPY") self.Log("PERIOD: 2017-2019") # ---------------------------------------------------------------------- # Algo params # ---------------------------------------------------------------------- self.MIN_EXPIRY = 17 #Shortest DTE to consider self.MAX_EXPIRY = 37 #Longest DTE to consider self.MIN_DELTA = .37 #lowest delta = lower risk = less returns self.MAX_DELTA = .49 #highest delta = higher risk = more returns self.k_filter_low = -25 self.k_filter_high = 1 self.resol = Resolution.Minute # Resolution.Minute .Hour .Daily self.tkr = "SPY" # "SPY", "GOOG", ... self.Lev = d.Decimal(1.0) self.single_trade_allocation = .4 #max portfolio value to place one single trade self.Settings.FreePortfolioValuePercentage = 0.07 #default is .025 # self.Ntnl_perc = d.Decimal( round( 1. / (2. * self.MAX_EXPIRY/7.), 2) ) # notional percentage, e.g. 0.08 self.select_flag, self.hedge_flag = False, False self.previous_delta, self.delta_threshold = d.Decimal(0.0), d.Decimal(0.05) # ---------------------------------------------------------------------- # add underlying Equity equity = self.AddEquity(self.tkr, self.resol) equity.SetDataNormalizationMode(DataNormalizationMode.Raw) # IMPORTANT: default self.equity_symbol = equity.Symbol #self.SetSecurityInitializer(self.CustomSecurityInitializer) # Add options option = self.AddOption(self.tkr, self.resol) option.SetDataNormalizationMode(DataNormalizationMode.Raw) # IMPORTANT: default #option.SetLeverage(2.0) self.option_symbol = option.Symbol # set our strike/expiry filter for this option chain option.SetFilter(self.UniverseFunc) # option.SetFilter(-2, +2, 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 # this is needed for Greeks calcs self.SetWarmUp(TimeSpan.FromDays(7)) # timedelta(7) self._assignedOption = False self.call, self.put = None, None # ----------------------------------------------------------------------------- # scheduled functions # ----------------------------------------------------------------------------- #self.Schedule.On(self.DateRules.EveryDay(self.equity_symbol), # self.TimeRules.BeforeMarketClose(self.equity_symbol, 10), # Action(self.close_options)) def close_options(self): """ Liquidate opts (with some value) and underlying """ #return # check this is the last trading day #if self.last_trading_day != self.Time.date(): return #self.Log("On last trading day: liquidate options with value and underlying ") # liquidate options (if invested and in the money [otherwise their price is min of $0.01) #for x in self.Portfolio: # symbol = x.Key; security = x.Value ## but also symbol = x.Value.Symbol # if x.Value.Invested: # self.Portfolio[opt].Invested, but no need for self.Securities.ContainsKey(opt) # only liquidate valuable options, otherwise let them quietly expiry # if self.Securities[x.Key].AskPrice > 0.05: self.Liquidate(x.Key) # CHECK if this necessary (incorporated above) if self.Portfolio[self.equity_symbol].Invested: self.Liquidate(self.equity.Symbol) def OnData(self, slice): if self.IsWarmingUp: return if not self.HourMinuteIs(10, 1): return ##trade once a day max # choose ITM contracts #contracts = [x for x in call if call.UnderlyingLastPrice - x.Strike > 0] # or choose ATM contracts #contracts = sorted(optionchain, key = lambda x: abs(optionchain.Underlying.Price - x.Strike))[0] # or choose OTM contracts #contracts = [x for x in call if call.UnderlyingLastPrice - x.Strike < 0] # sort the contracts by their expiration dates #contracts = sorted(contracts, key = lambda x:x.Expiry, reverse = True) # 1. deal with any early assignments #if self._assignedOption: # close everything # for x in self.Portfolio: # if x.Value.Invested: self.Liquidate(x.Key) self._assignedOption = False # self.call, self.put = None, None # stop getting Greeks # 2. sell options, if none if not self.Portfolio.Invested or self.Portfolio.GetBuyingPower(self.tkr) > self.Portfolio.TotalPortfolioValue * self.single_trade_allocation: # select contract #self.Log("get contracts") self.get_contracts(slice) if not self.put: return # trade #self.Securities[self.tkr].Holdings.MarginRemaining unit_price = self.put.Strike * d.Decimal(100.0) #unit_price2 = self.put.UnderlyingLastPrice * d.Decimal(100.0) #unit_price = self.Securities[self.equity_symbol].Price * d.Decimal(100.0) # share price x 100 qnty = int((self.Portfolio.GetBuyingPower(self.tkr) * self.single_trade_allocation) / unit_price) #q2 = int((self.Portfolio.GetBuyingPower(self.tkr) * .5) / unit_price) #qnty = min(q1, q2) #never use more than 50% of remaining buy power #bp = str(self.Portfolio.GetBuyingPower(self.tkr)) #tv = str(self.Portfolio.TotalPortfolioValue) #mr = str(self.Portfolio.MarginRemaining) #mu = str(self.Portfolio.TotalMarginUsed) #cash = str(self.Portfolio.Cash) #settled only #u_cash = str(self.Portfolio.UnsettledCash) #unsettled only #blah = str(self.CalculateOrderQuantity(self.tkr, .2)) ##fail #maxBpParam = GetMaximumOrderQuantityForTargetBuyingPowerParameters(self.Portfolio, self.Securities[self.equity_symbol], d.Decimal(20), False) #maxq = int(self.Portfolio.GetMaximumOrderQuantityForTargetBuyingPower(maxBpParam).Quantity) # #self.Log("unit_price " + str(unit_price)) #self.Log("qty " + str(qnty)) # call_exists, put_exists = self.call is not None, self.put is not None if self.call is not None and self.Portfolio[self.tkr].Invested: ccnt = self.Portfolio[self.tkr].Invested.Quantity / 100 self.Debug("Selling call " + str(ccnt) + "@" + str(self.call.Strike)) self.Sell(self.call.Symbol, ccnt) # self.MarketOrder(self.call.Symbol, -qnty) if self.put is not None and qnty > 0: self.Debug("Selling puts " + str(qnty) + "@" + str(unit_price)) order = self.MarketOrder(self.put.Symbol, -qnty) # 3. delta-hedged any existing option #if self.Portfolio.Invested and self.HourMinuteIs(10, 1): # self.get_greeks(slice) # if abs(self.previous_delta - self.Delta) > self.delta_threshold: # self.Log("delta_hedging: self.call {}, self.put {}, self.Delta {}" .format(self.call, self.put, self.Delta)) # self.SetHoldings(self.equity_symbol, self.Delta) # self.previous_delta = self.Delta def get_contracts(self, slice): """ Get ATM call and put """ for kvp in slice.OptionChains: if kvp.Key != self.option_symbol: continue if not self.HourMinuteIs(10, 1): continue optionchain = kvp.Value # option contracts for each 'subscribed' symbol/key calls = [x for x in optionchain if x.Right == 0] self.call = calls[0] if calls else None # self.Log("delta call {}, self.call type {}" .format(self.call.Greeks.Delta, type(self.call))) # self.Log("implied vol {} " .format(self.call.ImpliedVolatility)) #self.Debug(str(calls)) #self.Debug(str(self.call)) #self.Log("min delt " + str(self.MIN_DELTA)) #self.Log("max delt " + str(self.MAX_DELTA)) puts = [x for x in optionchain if x.Right == 1 and abs(x.Greeks.Delta) >= self.MIN_DELTA and abs(x.Greeks.Delta) <= self.MAX_DELTA] #df = pd.DataFrame([[x.Right, float(x.Strike), x.Expiry, float(x.BidPrice), float(x.AskPrice), x.Greeks.Delta, x.Greeks.Theta] for x in optionchain], # index=[x.Symbol.Value for x in optionchain], # columns=['type(call 0, put 1)', 'strike', 'expiry', 'ask', 'bid', 'delta', 'theta']) #put_frame = pd.DataFrame([[float(x.Strike), x.Expiry, float(x.BidPrice), float(x.AskPrice), x.Greeks.Delta, x.Greeks.Theta] for x in puts], # index=[x.Symbol.Value for x in puts], # columns=[ 'strike', 'expiry', 'ask', 'bid', 'delta', 'theta']) #self.Log("spy close " + str(self.Securities[self.tkr].Close)) #self.Log(str(put_frame)) #for kvp in slice.OptionChains: # if kvp.Key != self.option_symbol: continue # chain = kvp.Value # option contracts for each 'subscribed' symbol/key # spot_price = chain.Underlying.Price # self.Log("spot_price {}" .format(spot_price)) # prefer to do in steps, rather than a nested sorted ## ##TEN / DEL *AKA* PRE / (DTE * DEL) ## # 1. get shortest expiry contracts_by_T = sorted(puts, key = lambda x: x.Expiry, reverse = True) if not contracts_by_T: return self.expiry = contracts_by_T[-1].Expiry.date() # shortest expiry self.last_trading_day = last_trading_day(self.expiry) # get contracts with shortest expiry and sort them by strike slice_T = [i for i in puts if i.Expiry.date() == self.expiry] sorted_contracts = sorted(slice_T, key = lambda x: x.Strike, reverse = False) # self.Log("Expiry used: {} and shortest {}" .format(self.expiry, contracts_by_T[-1].Expiry.date()) ) # 2b. get the ATM closest put to short #puts = [i for i in sorted_contracts \ # if i.Right == OptionRight.Put and i.Strike <= spot_price] self.put = puts[0] if puts else None #self.Log("found contract: " + str(self.put)) def get_greeks(self, slice): """ Get greeks for invested option: self.call and self.put """ if (self.call is None) or (self.put is None): return for kvp in slice.OptionChains: if kvp.Key != self.option_symbol: continue chain = kvp.Value # option contracts for each 'subscribed' symbol/key traded_contracts = filter(lambda x: x.Symbol == self.call.Symbol or x.Symbol == self.put.Symbol, chain) if not traded_contracts: self.Log("No traded cointracts"); return deltas = [i.Greeks.Delta for i in traded_contracts] # self.Log("Delta: {}" .format(deltas)) self.Delta=sum(deltas) # self.Log("Vega: " + str([i.Greeks.Vega for i in contracts])) # self.Log("Gamma: " + str([i.Greeks.Gamma for i in contracts])) #.IncludeWeeklys() def UniverseFunc(self, universe): return universe.IncludeWeeklys()\ .Strikes(self.k_filter_low, self.k_filter_high)\ .Expiration(timedelta(self.MIN_EXPIRY), timedelta(self.MAX_EXPIRY)) # ---------------------------------------------------------------------- # Other ancillary fncts # ---------------------------------------------------------------------- def OnOrderEvent(self, orderEvent): # self.Log("Order Event -> {}" .format(orderEvent)) pass def TimeIs(self, day, hour, minute): return self.Time.day == day and self.Time.hour == hour and self.Time.minute == minute def HourMinuteIs(self, hour, minute): return self.Time.hour == hour and self.Time.minute == minute def CustomSecurityInitializer(self, security): if security.Type == SecurityType.Option: security.MarginModel = OptionMarginModel() class MyPCM(InsightWeightingPortfolioConstructionModel): def CreateTargets(self, algorithm, insights): targets = super().CreateTargets(algorithm, insights) return [PortfolioTarget(x.Symbol, x.Quantity*algorithm.Securities[x.Symbol].Leverage) for x in targets] class OptionMarginModel: def __init__(self): optionMarginRequirement = 1; nakedPositionMarginRequirement = 0.1 nakedPositionMarginRequirementOTM = 0.2 def Initialize(self): pass def GetLeverage(security): return 1 def SetLeverage(security, leverage): return # ---------------------------------------------------------------------- # all_symbols = [ x.Value for x in self.Portfolio.Keys ] # all_invested = [x.Symbol.Value for x in self.Portfolio.Values if x.Invested ] # for kvp in self.Securities: symbol = kvp.Key; security = kvp.Value # # orders = self.Transactions.GetOrders(None) # for order in orders: self.Log("order symbol {}" .format(order.Symbol)) # # volatility = self.Securities[self.equity_symbol].VolatilityModel.Volatility # self.Log("Volatility: {}" .format(volatility)) # set our strike/expiry filter for this option chain
# ------------------------------------------------------------------------------ # Business days # ------------------------------------------------------------------------------ from datetime import timedelta #, date from pandas.tseries.holiday import (AbstractHolidayCalendar, # inherit from this to create your calendar Holiday, nearest_workday, # to custom some holidays # USMartinLutherKingJr, # already defined holidays USPresidentsDay, # " " " " " " GoodFriday, USMemorialDay, # " " " " " " USLaborDay, USThanksgivingDay # " " " " " " ) class USTradingCalendar(AbstractHolidayCalendar): rules = [ Holiday('NewYearsDay', month=1, day=1, observance=nearest_workday), USMartinLutherKingJr, USPresidentsDay, GoodFriday, USMemorialDay, Holiday('USIndependenceDay', month=7, day=4, observance=nearest_workday), USLaborDay, USThanksgivingDay, Holiday('Christmas', month=12, day=25, observance=nearest_workday) ] # TODO: to be tested def last_trading_day(expiry): # American options cease trading on the third Friday, at the close of business # - Weekly options expire the same day as their last trading day, which will usually be a Friday (PM-settled), [or Mondays? & Wednesdays?] # # SPX cash index options (and other cash index options) expire on the Saturday following the third Friday of the expiration month. # However, the last trading day is the Thursday before that third Friday. Settlement price Friday morning opening (AM-settled). # http://www.daytradingbias.com/?p=84847 dd = expiry # option.ID.Date.date() # if expiry on a Saturday (standard options), then last trading day is 1d earlier if dd.weekday() == 5: dd -= timedelta(days=1) # dd -= 1 * BDay() # check that Friday is not an holiday (e.g. Good Friday) and loop back while USTradingCalendar().holidays(dd, dd).tolist(): # if list empty (dd is not an holiday) -> False dd -= timedelta(days=1) return dd