Overall Statistics
Total Trades
825
Average Win
1.34%
Average Loss
-2.92%
Compounding Annual Return
10.105%
Drawdown
74.600%
Expectancy
0.078
Net Profit
94.048%
Sharpe Ratio
0.43
Probabilistic Sharpe Ratio
4.245%
Loss Rate
26%
Win Rate
74%
Profit-Loss Ratio
0.46
Alpha
0.012
Beta
1.228
Annual Standard Deviation
0.348
Annual Variance
0.121
Information Ratio
0.13
Tracking Error
0.291
Treynor Ratio
0.122
Total Fees
$18745.91
### 2020_11_13 v3
### ----------------------------------------------------------------------------
### Added a cash instrument to invest the margin remaining from the two futures contracts
### ----------------------------------------------------------------------------

import numpy as np

class VixFuturesStrategyAlgorithm(QCAlgorithm):

    def Initialize(self):
        
        self.SetTimeZone("America/Chicago")
        
        self.SetStartDate(2014, 1, 1)
        #self.SetEndDate(2020, 11, 5)
        self.SetCash(1000000)
        
        # notional target as a percentage over total portfolio value
        self.notionalOverPortfolio = -0.5
        # allocations to roll every week for different expiration groups (keys are the weeks left to fully roll)
        self.allocationsToRoll = {1: [1], 2: [0.25, 0.75], 3: [0.25, 0.25, 0.5], 4: [0.2, 0.2, 0.2, 0.4]}
        
        # add a ticker (cashSymbol) for the margin remaining
        self.cashTicker = 'TLT'

        # add futures data and always get fron month contract
        self.vix = self.AddFuture(Futures.Indices.VIX, Resolution.Minute)
        self.vix.SetFilter(3, 90)
        # add data for cash symbol
        if self.cashTicker is not None:
            self.cashSymbol = self.AddEquity(self.cashTicker, Resolution.Minute).Symbol
        
        self.contractsSortedByExpiry = []
        self.previousFrontMonthContract = None
        self.allocationToRoll = 0
        self.day = 0
        
        # plot notional exposure by contract and total
        notionalExposurePlot = Chart('Chart Notional Exposure')
        self.AddChart(notionalExposurePlot)
    
    def OnData(self, data):
        
        # skip if the cash ticker has price of zero
        if self.cashTicker is not None and self.Securities[self.cashSymbol].Price == 0:
            return
        
        if self.Time.day == self.day:
            return
        
        # get relevant contracts once a day --------------------------------------------------------------
        
        for chain in data.FutureChains:
            contracts = [contract for contract in chain.Value]
            self.contractsSortedByExpiry = sorted(contracts, key = lambda x: x.Expiry, reverse = False)
        
        if len(self.contractsSortedByExpiry) < 2:
            return
        
        # define contracts
        frontContract = self.contractsSortedByExpiry[0]
        secondContract = self.contractsSortedByExpiry[1]
        
        self.day = self.Time.day

        # skip if not monday
        if self.Time.date().weekday() != 0:
            return
        
        # trade first front month contracts -----------------------------------------------------------
        if not self.Portfolio.Invested:
            self.RollFuturesContracts(frontContract, 0, 'front contract')
            # invest the margin remaining in the cash symbol
            if self.cashTicker is not None:
                self.RebalanceCashSymbol()
            # print a few logs
            self.PrintRelevantLogs(frontContract, secondContract)
        
        # when we have a new front month contract,
        # calculate the number of weeks left to expiration ---------------------------------------------
        if self.previousFrontMonthContract != frontContract.Symbol.Value or self.weeksToFrontContractExpiry == 0:
            self.weeksToFrontContractExpiry = self.WeekdaysBetweenDates(self.Time.date(), frontContract.Expiry.date(), 0) - 2
            
            self.Log(frontContract.Symbol.Value + '; ' + str(self.weeksToFrontContractExpiry) + ' weeks of rolling')
            self.previousFrontMonthContract = frontContract.Symbol.Value
            self.allocationToRoll = 0
            self.weeksCount = 0
            
            return
        
        if self.weeksToFrontContractExpiry == 0:
            return
        
        # rolling ----------------------------------------------------------------------------------------

        # update the allocation to roll ----------------------------------------
        self.allocationToRoll += self.allocationsToRoll[self.weeksToFrontContractExpiry][self.weeksCount]
        self.weeksCount += 1
        self.Log('allocationToRoll: ' + str(self.allocationToRoll))
        
        # trade front month contracts ------------------------------------------
        self.RollFuturesContracts(frontContract, self.allocationToRoll, 'front contract')
        
        # trade front month contracts ------------------------------------------
        self.RollFuturesContracts(secondContract, 1 - self.allocationToRoll, 'second contract')
        
        # invest the margin remaining in the cash symbol -----------------------
        if self.cashTicker is not None:
            self.RebalanceCashSymbol()
            
        # print a few logs -----------------------------------------------------
        self.PrintRelevantLogs(frontContract, secondContract)
        
        # plot notional exposure by contract and total -------------------------
        self.PlotNotionalExposure(frontContract, secondContract)
            
    def GetTargetNumberOfContracts(self, contract, adjustment):
        
        ''' Calculate the number of contracts to hold to achieve notional target '''
        
        numberOfContracts = 0
        
        notionalTarget = self.Portfolio.TotalPortfolioValue * self.notionalOverPortfolio * (1 - adjustment)
        #contractPrice = self.Securities[contract.Symbol].BidPrice if self.notionalOverPortfolio < 0 else self.Securities[contract.Symbol].AskPrice
        contractPrice = contract.BidPrice if self.notionalOverPortfolio < 0 else contract.AskPrice
        contractNotionalValue = contractPrice * self.vix.SymbolProperties.ContractMultiplier
        
        if contractNotionalValue == 0:
            return numberOfContracts
            
        numberOfContracts = int(notionalTarget / contractNotionalValue)
        
        return numberOfContracts
            
    def RollFuturesContracts(self, contract, adjustment, message):
    
        ''' Roll futures contracts '''

        targetNumberOfContracts = self.GetTargetNumberOfContracts(contract, adjustment)
        currentNumberOfContracts = self.Portfolio[contract.Symbol].Quantity
        quantityContracts = targetNumberOfContracts - currentNumberOfContracts
        self.MarketOrder(contract.Symbol, quantityContracts, False, message)
        self.Log('target contracts for ' + message + ': ' + str(targetNumberOfContracts))
            
    def RebalanceCashSymbol(self):
        
        ''' Rebalance cash symbol to use all the remaining margin '''
        
        targetCashShares = int(self.Portfolio.MarginRemaining / self.Securities[self.cashSymbol].Price)
        currentCashShares = self.Portfolio[self.cashSymbol].Quantity
        cashShares = targetCashShares - currentCashShares
        self.MarketOrder(self.cashSymbol, cashShares, False, 'cash rebalancing')
        
    def GetCurrentNotionalExposure(self, contract):
        
        ''' Calculate the current notional exposure of a given contract '''
        
        #contractPrice = self.Securities[contract.Symbol].BidPrice if self.notionalOverPortfolio < 0 else self.Securities[contract.Symbol].AskPrice
        contractPrice = contract.BidPrice if self.notionalOverPortfolio < 0 else contract.AskPrice
        currentNotionalValue = contractPrice * self.vix.SymbolProperties.ContractMultiplier * self.Portfolio[contract.Symbol].Quantity
        currentNotionalExposure = currentNotionalValue / self.Portfolio.TotalPortfolioValue
        
        return currentNotionalExposure
    
    def PlotNotionalExposure(self, frontContract, secondContract):
        
        ''' Plot the notional exposure by contract and in total '''
            
        notionalExposureFrontContract = self.GetCurrentNotionalExposure(frontContract)
        self.Plot('Chart Notional Exposure', 'Front Month Notional Exposure', notionalExposureFrontContract)
        notionalExposureSecondContract = self.GetCurrentNotionalExposure(secondContract)
        self.Plot('Chart Notional Exposure', 'Second Month Notional Exposure', notionalExposureSecondContract)
        totalNotionalExposure = notionalExposureFrontContract + notionalExposureSecondContract
        self.Plot('Chart Notional Exposure', 'Total Notional Exposure', totalNotionalExposure)
        
    def WeekdaysBetweenDates(self, fromDate, toDate, weekdayNumber):
        
        ''' Calculate the number of given weekday between two dates '''
        
        daysBetween = (toDate - fromDate).days
        weekdaysBetween = sum((fromDate + timedelta(i)).weekday() == weekdayNumber for i in range(0, daysBetween))
        
        return weekdaysBetween
        
    def PrintRelevantLogs(self, frontContract, secondContract):
        
        ''' Print a few relevant logs '''
        
        self.Log('current contracts: ' + str({frontContract.Symbol.Value: [frontContract.Expiry.date(), self.Portfolio[frontContract.Symbol].Quantity, self.Securities[frontContract.Symbol].BidPrice],
                                                secondContract.Symbol.Value: [secondContract.Expiry.date(), self.Portfolio[secondContract.Symbol].Quantity, self.Securities[secondContract.Symbol].BidPrice]}))
        if self.cashTicker is not None:
            self.Log('TotalPortfolioValue: ' + str(self.Portfolio.TotalPortfolioValue) + '; Cash: ' + str(self.Portfolio.Cash)
                        + '; MarginRemaining: ' + str(self.Portfolio.MarginRemaining) + '; cashSymbol: ' + str(self.Portfolio[self.cashSymbol].Quantity))
        else:
            self.Log('TotalPortfolioValue: ' + str(self.Portfolio.TotalPortfolioValue) + '; Cash: ' + str(self.Portfolio.Cash)
                    + '; MarginRemaining: ' + str(self.Portfolio.MarginRemaining))

    def OnOrderEvent(self, orderEvent):
        
        ticket = self.Transactions.GetOrderTicket(orderEvent.OrderId)
        if ticket.Tag == 'Liquidate from delisting':
            self.Log('liquidate due to contract delisting')
            
    def OnMarginCall(self, requests):
        self.Log('liquidate all holdings due to margin call')
        self.Liquidate()
        
        return requests