Overall Statistics
Total Trades
911
Average Win
1.08%
Average Loss
-0.28%
Compounding Annual Return
34.424%
Drawdown
48.900%
Expectancy
1.380
Net Profit
1743.581%
Sharpe Ratio
0.963
Probabilistic Sharpe Ratio
28.661%
Loss Rate
51%
Win Rate
49%
Profit-Loss Ratio
3.88
Alpha
0.077
Beta
1.691
Annual Standard Deviation
0.288
Annual Variance
0.083
Information Ratio
0.793
Tracking Error
0.2
Treynor Ratio
0.164
Total Fees
$39391.44
Estimated Strategy Capacity
$15000.00
Lowest Capacity Asset
VXXB XT2IGBLOG6ZQ|VXXB WRBPJAJZ2Q91
## WHEREAS PREVIOUS VERSIONS OF ALGORITHM INVEST IN ONE HEDGE AT A TIME, THIS VERSION ALLOCATES TO MULTI-LEG SPREAD
## ACROSS MULTIPLE MATURITY DATES (DTE).

## Allocate % of portfolio each to SSO (2x leveraged SP500 etf) and % to VXX weekly call option.
## VXX option strike is set as a percentage premium (e.g., 2.0 means strike is 200% premium to current VIX level)
## Percentage premium method is significantly faster than calculating Greeks (see delta-based algorithm)


class VIXTailHedge(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2012, 1, 12)
        # self.SetEndDate(2020, 8, 1)
        self.cash = 1000000
        self.SetCash(self.cash)
        stock = self.AddEquity("SSO", Resolution.Minute) #portfolio holdings
        self.stock = stock.Symbol
        self.SetWarmUp(200)
        
        self.vxx_option = self.AddEquity("VXX", Resolution.Minute)
        self.vxx1_option = self.AddEquity("VXX.1", Resolution.Minute)
        self.vxx_option.SetDataNormalizationMode(DataNormalizationMode.Raw)
        self.vxx1_option.SetDataNormalizationMode(DataNormalizationMode.Raw)
        self.vix = self.AddIndex('VIX', Resolution.Minute).Symbol   #Used in charts only, not in strategy
        self.SetSecurityInitializer(lambda x: x.SetMarketPrice(self.GetLastKnownPrice(x)))
        self.hedge = None
        self.contracts = []
        
        #Plot hedge profits once per month
        self.Schedule.On(self.DateRules.MonthStart(self.stock), \
                        self.TimeRules.AfterMarketOpen(self.stock, 30), \
                        self.Monthlychart)
                        
        #schedule Plotting function 30 minutes after every market open
        self.Schedule.On(self.DateRules.EveryDay(self.stock), \
                        self.TimeRules.AfterMarketOpen(self.stock, 30), \
                        self.Plotting)
        
        ######### Hedge Paramaters #########
        #self.hedge_weight = 0.0025 # % of portfolio invested into hedge each month
        self.hedge_weight = float(self.GetParameter("hedge_weight")) # % of portfolio invested into hedge each month
        
        self.stock_weight = 1-self.hedge_weight # % of portfolio allocated to stock each month
        self.hedge_premium = 2 # % strike price premium on option call hedge
        self.target_DTE1 = 10 # target days to expiration for selected option contract
        self.target_DTE2 = 20 # target days to expiration for selected option contract
        self.target_DTE3 = 30 # target days to expiration for selected option contract
        
        #self.target_hedge_profit = 50 # percentage of portfolio covered in tail event
        self.target_hedge_profit = float(self.GetParameter("target_hedge_profit"))# percentage of portfolio covered in tail event
        ######### Charts / Plots #########
        self.mkt = []
        self.hedge_month_profit = 0
        self.hedge_ytd_profit = 0
        self.hedge_all_profit = 0
        
        #master container for chart:
        hedgeProfit = Chart("Hedge Profit/Loss")
        hedgeProfit.AddSeries(Series("Monthly Return", SeriesType.Bar, 0, "%"))
        hedgeProfit.AddSeries(Series("YTD Return", SeriesType.Bar, 0, "%"))
        hedgeProfit.AddSeries(Series("All-Time Return", SeriesType.Bar, 1, "%"))
        self.AddChart(hedgeProfit)
        
        
    def OnData(self, data):
        ## VXX expired and was relisted in 2019, requires switching tickers
        ## --> https://www.quantconnect.com/forum/discussion/6804/vxx-minute-the-issue-starts-from-jan-30th-2009-and-continues-until-jan-17th-2018/p1
        if self.Time.year < 2019:
            self.hedge = self.vxx1_option.Symbol
        else:
            self.hedge = self.vxx_option.Symbol
        
        if self.IsWarmingUp:
            return

        self.contracts = [x.Key for x in self.Portfolio if x.Value.Invested and x.Value.Type==SecurityType.Option]
        
        for contract in self.contracts:
            #Use this code block if testing 2020 covid event only, and not including 2015's august vix spike
            if self.Time.month == 8 and self.Time.year == 2015:
                return
            
            #check to see if hedge meets return requirement (ie, in tail event)
            if self.Portfolio[contract].UnrealizedProfitPercent*self.hedge_weight*100 >= self.target_hedge_profit:
                
                #liquidate hedge position
                self.MarketOrder(contract, -self.Portfolio[contract].Quantity, False, "Tail Event!")
        
        if self.Time.hour == 11: #Placing trades at 11am prevents filling orders at stale prices (fillforward)
            #Check if we're holding the ideal 3 contracts, if not purchase them
            if len(self.contracts) < 3:
                
                if len(self.contracts) == 0:
                    self.GetContract1()
                    self.GetContract2()
                    self.GetContract3()
                    
                elif len(self.contracts) == 1:
                    self.GetContract2()
                    self.GetContract3()
                    
                elif len(self.contracts) == 2:
                    self.GetContract3()
                
                for contract in self.contracts:
                    if not self.Portfolio[contract].Invested:
                        self.SetHoldings(self.stock, self.stock_weight)
                        
                        #Purchase hedge contract
                        self.SetHoldings(contract, self.hedge_weight/len(self.contracts))
                        
                        # #Set stop loss orders in the event of a tail event
                        # contract_quantity = self.Portfolio[contract].Quantity
                        # contract_price = self.Portfolio[contract].Price
                        # stoploss_price = self.contract_price*self.target_hedge_profit/(self.hedge_weight/len(self.contracts)*100)
                        # self.StopLimitOrder(contract, -contract_quantity/3, round(stoploss_price*0.6, 2), round(stoploss_price*0.6, 2))
                        # self.StopLimitOrder(contract, -contract_quantity/3, round(stoploss_price*1.0, 2), round(stoploss_price*1.0, 2))
                        # self.StopLimitOrder(contract, -contract_quantity/3, round(stoploss_price*1.4, 2), round(stoploss_price*1.4, 2))

            #Liquidate any options that are within 2 days of expiration    
            option_invested = [x.Key for x in self.Portfolio if x.Value.Invested and x.Value.Type==SecurityType.Option]
            if option_invested:
                for contract in option_invested:
                    expirydate = contract.ID.Date
                    if (expirydate - self.Time) < timedelta(2):
                        # self.Debug("option less than 2 days before expiry")
                        
                        #liquidate hedge before expiration
                        self.Liquidate(contract, "2 days before expiry")
    

    def Monthlychart(self):
        # self.Log("MonthlyChart() fired at : {0}".format(self.Time))
        
        #log number of hedge contracts currently held
        number_option_contracts = [x.Key for x in self.Portfolio if x.Value.Invested and x.Value.Type==SecurityType.Option]
        self.Debug(f"Number of hedge contracts held: {len(number_option_contracts)}")

        #log hedge profits in charts
        self.Plot("Hedge Profit/Loss", "Monthly Return", self.hedge_month_profit)
        self.Plot("Hedge Profit/Loss", "YTD Return", self.hedge_ytd_profit)
        self.Plot("Hedge Profit/Loss", "All-Time Return", self.hedge_all_profit)
        
        #reset monthly and yearly profit
        self.hedge_month_profit = 0
        if self.Time.month == 1:
            self.hedge_ytd_profit = 0
        
            
    def GetContract1(self):
        targetStrike = self.Securities[self.hedge].Price * self.hedge_premium
        contracts = self.OptionChainProvider.GetOptionContractList(self.hedge, self.Time)
        # self.Debug(f"VXX Total Contracts Found: {len(contracts)}")
        
        calls = [x for x in contracts if x.ID.OptionRight == OptionRight.Call]
        calls = [x for x in calls if abs(x.ID.StrikePrice - targetStrike) < 2]
        calls = [x for x in calls if 0 <= ((x.ID.Date - self.Time).days - self.target_DTE1) <= 14]
        # self.Debug(f"VXX Calls found: {len(calls)}")
        calls = sorted(sorted(calls, key = lambda x: x.ID.Date), key = lambda x: x.ID.StrikePrice)
        
        if len(calls) == 0:
            self.Debug(f"!!! no options available")
            return None
        
        # self.Debug(f"Selected option contract: {calls[0]}")    
        self.AddOptionContract(calls[0], Resolution.Minute)
        self.contracts.append(calls[0])
        return
        
        
    def GetContract2(self):
        targetStrike = self.Securities[self.hedge].Price * self.hedge_premium
        contracts = self.OptionChainProvider.GetOptionContractList(self.hedge, self.Time)
        # self.Debug(f"VXX Total Contracts Found: {len(contracts)}")
        
        calls = [x for x in contracts if x.ID.OptionRight == OptionRight.Call]
        calls = [x for x in calls if abs(x.ID.StrikePrice - targetStrike) < 2]
        calls = [x for x in calls if 0 <= ((x.ID.Date - self.Time).days - self.target_DTE2) <= 14]
        # self.Debug(f"VXX Calls found: {len(calls)}")
        calls = sorted(sorted(calls, key = lambda x: x.ID.Date), key = lambda x: x.ID.StrikePrice)
        
        if len(calls) == 0:
            self.Debug(f"!!! no options available")
            return None
        
        # self.Debug(f"Selected option contract: {calls[0]}")    
        self.AddOptionContract(calls[0], Resolution.Minute)
        self.contracts.append(calls[0])
        return
        
        
    def GetContract3(self):
        targetStrike = self.Securities[self.hedge].Price * self.hedge_premium
        contracts = self.OptionChainProvider.GetOptionContractList(self.hedge, self.Time)
        # self.Debug(f"VXX Total Contracts Found: {len(contracts)}")
        
        calls = [x for x in contracts if x.ID.OptionRight == OptionRight.Call]
        calls = [x for x in calls if abs(x.ID.StrikePrice - targetStrike) < 2]
        calls = [x for x in calls if 0 <= ((x.ID.Date - self.Time).days - self.target_DTE3) <= 14]
        # self.Debug(f"VXX Calls found: {len(calls)}")
        calls = sorted(sorted(calls, key = lambda x: x.ID.Date), key = lambda x: x.ID.StrikePrice)
        
        if len(calls) == 0:
            self.Debug(f"!!! no options available")
            return None
        
        # self.Debug(f"Selected option contract: {calls[0]}")    
        self.AddOptionContract(calls[0], Resolution.Minute)
        self.contracts.append(calls[0])
        return
        
    
    #Log hedge profits for charts
    def OnOrderEvent(self, orderEvent):
        if orderEvent.Status == OrderStatus.Filled and orderEvent.Symbol != self.stock and orderEvent.Direction == OrderDirection.Sell:
            # self.Log("{0}: {1}".format(self.Time, orderEvent))
            
            # self.Debug(f"{orderEvent.Symbol} Profit_$: {self.Portfolio[orderEvent.Symbol].Profit}, \
            #             Profit_%_Portfolio: {self.Portfolio[orderEvent.Symbol].Profit/self.Portfolio.TotalPortfolioValue}")
                        
            self.hedge_month_profit += self.Portfolio[orderEvent.Symbol].Profit/self.Portfolio.TotalPortfolioValue*100
            self.hedge_ytd_profit += self.Portfolio[orderEvent.Symbol].Profit/self.Portfolio.TotalPortfolioValue*100
            self.hedge_all_profit += self.Portfolio[orderEvent.Symbol].Profit/self.Portfolio.TotalPortfolioValue*100
        
    def Plotting (self):
        mkt_price = self.Securities[self.stock].Close
        self.mkt.append(mkt_price)
        mkt_perf = self.mkt[-1] / self.mkt[0] * self.cash
        self.Plot('Strategy Equity', 'SSO', mkt_perf)
        
        #plot strike price of option
        option_invested = [x.Key for x in self.Portfolio if x.Value.Invested and x.Value.Type==SecurityType.Option]
        if option_invested and self.contracts is not None:
            self.Plot("Hedge Strike", "Strike", option_invested[0].ID.StrikePrice)
            self.Plot("Hedge Strike", "VXX Price", self.Securities[self.hedge].Price)
            self.Plot("Hedge Strike", "VIX Price", self.Securities[self.vix].Price)
            self.Plot("Hedge Unrealized Profit", "VXX Price", self.Securities[self.hedge].Price)
            self.Plot("Hedge Unrealized Profit", "VIX Price", self.Securities[self.vix].Price)
            self.Plot("Hedge Unrealized Profit", "Unrealized Profit", self.Portfolio[option_invested[0]].UnrealizedProfitPercent)