| 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