Overall Statistics |
Total Trades 42 Average Win 30.59% Average Loss -20.13% Compounding Annual Return -47.379% Drawdown 28.900% Expectancy 0.200 Net Profit -18.640% Sharpe Ratio -1.55 Probabilistic Sharpe Ratio 1.545% Loss Rate 52% Win Rate 48% Profit-Loss Ratio 1.52 Alpha -0.694 Beta 0.709 Annual Standard Deviation 0.288 Annual Variance 0.083 Information Ratio -2.983 Tracking Error 0.267 Treynor Ratio -0.63 Total Fees $364.00 |
from QuantConnect.Securities.Option import OptionPriceModels from datetime import timedelta import decimal as d from my_calendar import last_trading_day class DeltaHedgedStraddleAlgo(QCAlgorithm): def Initialize(self): self.SetStartDate(2020, 7, 1) #self.SetEndDate(2020, 7, 30) self.SetCash(25000) # ---------------------------------------------------------------------- # Algo params # ---------------------------------------------------------------------- self.MAX_EXPIRY = 10 # max num of days to expiration => will select expiries upto 2 weeks out self.MIN_EXPIRY = 5 # max num of days to expiration => will select expiries at least 1 week out self._no_K = 50 # number of strikes below/above ATM for universe => No need to change this self.tkr = "SPY" # "SPY", "GOOG", ... self.quantity = 50 # number of credit spreads to trade consolidatedBar = TradeBarConsolidator(timedelta(hours=1)) # options data is minute by default, so hourly bar, change timedelta param for different bar lengths # bollinger band params self.bbLookback = 20 self.bbBand = 2 # profit targets self.profitTargetA,self.profitFactorA = 0.0,0.5 # profit factor the fraction of open to close i.e 0.5 = 50% self.profitTargetB = -0.05 self.hedgeRatio = 1 / 20 # set as fraction for hedges per spread; set zero for no hedging self.shortDelta, self.hedgeDelta = -0.2,-0.3 # target deltas for short leg and hedge # ---------------------------------------------------------------------- # add underlying Equity self.resol = Resolution.Minute # Resolution.Minute .Hour .Daily equity = self.AddEquity(self.tkr, self.resol) equity.SetDataNormalizationMode(DataNormalizationMode.Raw) # IMPORTANT: default self.equity_symbol = equity.Symbol # Consolidate to 1hr bars and create bollingber band indicator self.SubscriptionManager.AddConsolidator(self.tkr, consolidatedBar) consolidatedBar.DataConsolidated += self.OnconsolidatedBar self.bb = BollingerBands(self.bbLookback,2) self.RegisterIndicator(self.tkr, self.bb, consolidatedBar) # Add options option = self.AddOption(self.tkr, self.resol) 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)) self.SetSecurityInitializer(lambda x: x.SetBuyingPowerModel(CustomBuyingPowerModel(self))) # 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 # Set warm up with 30 trading days to warm up the underlying volatility model self.SetWarmUp(30, Resolution.Daily) # vars for option contract objects self.shortLeg,self.longLeg,self.hedgeLeg = None, None, None def OnconsolidatedBar(self,sender,bar): if self.IsWarmingUp: return #self.Debug(str(self.Time) + " " + str(bar)) # 2. sell puts if current price below bollinger band if not self.Portfolio.Invested: if self.bb.LowerBand.Current.Value > self.Securities[self.equity_symbol].Close: # select contracts self.get_contracts(self.CurrentSlice) if (not self.shortLeg) or (not self.longLeg) or (not self.hedgeLeg): return # get quantities for trading credit = self.Securities[self.shortLeg.Symbol].Price - self.Securities[self.longLeg.Symbol].Price qnty = self.quantity hedgeQnty = qnty * self.hedgeRatio # hedge (optional) self.Log(f"Thresh exceeded: Price {self.Securities[self.equity_symbol].Close} < {self.bb.LowerBand.Current.Value:.2f}. Credit sell to open value ${credit:.2f}") self.MarketOrder(self.shortLeg.Symbol,-qnty) self.MarketOrder(self.longLeg.Symbol,qnty) if hedgeQnty > 0: self.MarketOrder(self.hedgeLeg.Symbol,hedgeQnty) # set profit target credit value self.profitTargetA = - credit * self.profitFactorA def OnData(self, slice): if self.Portfolio.Invested: if (not self.shortLeg) or (not self.longLeg) or (not self.hedgeLeg): return # 3. Check exit conditions credit = self.Securities[self.shortLeg.Symbol].Price - self.Securities[self.longLeg.Symbol].Price #1. unrealized profit $ = arbitrary number if credit < self.profitTargetA: self.Liquidate() self.Log(f"Profit Target A hit {credit:.2f} < {self.profitTargetA:.2f}") #2. unrealized profit $ = -0.05 if credit < self.profitTargetB: self.Liquidate() self.Log(f"Profit Target B hit {credit:.2f} < {self.profitTargetB:.2f}") #3. 0.5 delta on the short leg shortDelta = self.get_delta(slice,self.shortLeg) if self.get_delta(slice,self.shortLeg) is not None else 0 if shortDelta < - 0.5: self.Liquidate() self.Log(f"Stop loss hit {shortDelta:.2f} < - 0.5 ( {credit:.2f} )") #4. Today is expiration day if self.Time.date() == self.last_trading_day and self.Time.hour == 15: self.Liquidate() self.Log(f"Liquidating on expiration ( {credit:.2f} )") def get_contracts(self, slice): 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)) # 1. get the contract series expiring on Friday contracts_by_T = [ i for i in chain if i.Expiry.date().weekday() == 4 ] #contracts_by_T = sorted(chain, key = lambda x: x.Expiry, reverse = False) #contracts_by_T = list(filter(lambda x: x.Expiry >= self.Time + timedelta(days = 30), chain)) if not contracts_by_T: return #self.Log(f"Chain {[i.Expiry.date() for i in contracts_by_T]}") self.expiry = contracts_by_T[-1].Expiry.date() # furthest self.last_trading_day = last_trading_day(self.expiry) # sort contracts by strike sorted_contracts = sorted(contracts_by_T, key = lambda x: x.Strike, reverse = False) #self.Log("Expiry used: {} and shortest {}" .format(self.expiry, contracts_by_T[-1].Expiry.date()) ) # get all puts below 50 delta puts = [i for i in sorted_contracts \ if i.Right == OptionRight.Put and i.Strike <= spot_price] # get short leg - the first strike below delta self.shortLeg = [ i for i in puts if i.Greeks.Delta >= self.shortDelta ][-1] if puts else None # get long leg - the strike $2 below short leg self.longLeg = [ i for i in puts if i.Strike <= self.shortLeg.Strike - 2 ][-1] if puts else None # get hedge leg - self.hedgeLeg = [ i for i in puts if i.Greeks.Delta >= self.hedgeDelta ][-1] if puts else None self.Log(f"Deltas: short {self.shortLeg.Greeks.Delta}, long {self.longLeg.Greeks.Delta}") def get_delta(self, slice,leg): """ Take a data slice and an option leg as args; return the delta of that leg in slice """ if not leg: 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 == leg.Symbol, chain) if not traded_contracts: self.Log("No traded cointracts"); return deltas = [i.Greeks.Delta for i in traded_contracts] return deltas[0] def UniverseFunc(self, universe): return universe.IncludeWeeklys()\ .Strikes(-self._no_K, 0)\ .Expiration(timedelta(self.MIN_EXPIRY), timedelta(self.MAX_EXPIRY)) # ---------------------------------------------------------------------- # Other ancillary fncts # ---------------------------------------------------------------------- def OnOrderEvent(self, orderEvent): # self.Log("Order Event -> {}" .format(orderEvent)) pass # necessitated by incorrect default margining of credit spreads in QC class CustomBuyingPowerModel(BuyingPowerModel): def __init__(self, algorithm): self.algorithm = algorithm def HasSufficientBuyingPowerForOrder(self, parameters): # custom behavior: this model will assume that there is always enough buying power hasSufficientBuyingPowerForOrderResult = HasSufficientBuyingPowerForOrderResult(True) #self.algorithm.Log(f"CustomBuyingPowerModel: {hasSufficientBuyingPowerForOrderResult.IsSufficient}") return hasSufficientBuyingPowerForOrderResult
# ------------------------------------------------------------------------------ # 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