| Overall Statistics |
|
Total Trades 286 Average Win 0.95% Average Loss -1.12% Compounding Annual Return -5.266% Drawdown 26.400% Expectancy -0.103 Net Profit -12.656% Sharpe Ratio -0.18 Loss Rate 51% Win Rate 49% Profit-Loss Ratio 0.85 Alpha -0.083 Beta 3.493 Annual Standard Deviation 0.161 Annual Variance 0.026 Information Ratio -0.276 Tracking Error 0.161 Treynor Ratio -0.008 Total Fees $2643.65 |
# http://quantpedia.com/Screener/Details/22
from datetime import timedelta
from math import floor
from decimal import Decimal
import numpy as np
class CommodityTermStructureAlgorithm(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2016, 1, 1)
self.SetEndDate(2018, 7, 1)
self.SetCash(1000000)
tickers = [
Futures.Softs.Cocoa,
Futures.Softs.Coffee,
Futures.Grains.Corn,
Futures.Softs.Cotton2,
Futures.Grains.Oats,
Futures.Softs.OrangeJuice,
Futures.Grains.SoybeanMeal,
Futures.Grains.SoybeanOil,
Futures.Grains.Soybeans,
Futures.Softs.Sugar11,
Futures.Grains.Wheat,
Futures.Meats.FeederCattle,
Futures.Meats.LeanHogs,
Futures.Meats.LiveCattle,
Futures.Energies.CrudeOilWTI,
Futures.Energies.HeatingOil,
Futures.Energies.NaturalGas,
Futures.Energies.Gasoline,
Futures.Metals.Gold,
Futures.Metals.Palladium,
Futures.Metals.Platinum,
Futures.Metals.Silver
]
for ticker in tickers:
future = self.AddFuture(ticker)
future.SetFilter(timedelta(0), timedelta(days = 90))
self.chains = {}
self.AddEquity("SPY", Resolution.Minute)
self.Schedule.On(self.DateRules.MonthStart("SPY"), self.TimeRules.AfterMarketOpen("SPY", 30), self.Rebalance)
def OnData(self,slice):
# Saves the Futures Chains
for chain in slice.FutureChains:
if chain.Value.Contracts.Count < 2:
continue
if chain.Value.Symbol.Value not in self.chains:
self.chains[chain.Value.Symbol.Value] = [i for i in chain.Value]
self.chains[chain.Value.Symbol.Value] = [i for i in chain.Value]
def Rebalance(self):
self.Liquidate()
quintile = floor(len(self.chains)/5)
roll_returns = {}
for symbol, chain in self.chains.items():
contracts = sorted(chain, key = lambda x: x.Expiry)
# R = (log(Pn) - log(Pd)) * 365 / (Td - Tn)
# R - Roll returns
# Pn - Nearest contract price
# Pd - Distant contract price
# Tn - Nearest contract expire date
# Pd - Distant contract expire date
near_contract = contracts[0]
distant_contract = contracts[-1]
price_near = near_contract.LastPrice if near_contract.LastPrice>0 else 0.5*float(near_contract.AskPrice+near_contract.BidPrice)
price_distant = distant_contract.LastPrice if distant_contract.LastPrice>0 else 0.5*float(distant_contract.AskPrice+distant_contract.BidPrice)
if distant_contract.Expiry == near_contract.Expiry:
self.Debug("ERROR: Near and distant contracts have the same expiry!" + str(near_contract))
return
expire_range = 365 / (distant_contract.Expiry - near_contract.Expiry).days
roll_returns[symbol] = (np.log(float(price_near)) - np.log(float(price_distant)))*expire_range
positive_roll_returns = { symbol: returns for symbol, returns in roll_returns.items() if returns > 0 }
negative_roll_returns = { symbol: returns for symbol, returns in roll_returns.items() if returns < 0 }
backwardation = sorted(positive_roll_returns , key = lambda x: positive_roll_returns[x], reverse = True)[:quintile]
contango = sorted(negative_roll_returns , key = lambda x: negative_roll_returns[x])[:quintile]
count = min(len(backwardation), len(contango))
if count != quintile:
backwardation = backwardation[:count]
contango = contango[:count]
# We cannot long-short if count is zero
if count == 0:
self.chains = {}
return
for short_symbol in contango:
sort = sorted(self.chains[short_symbol], key = lambda x: x.Expiry)
self.SetHoldings(sort[1].Symbol, -0.5/count)
for long_symbol in backwardation:
sort = sorted(self.chains[long_symbol], key = lambda x: x.Expiry)
self.SetHoldings(sort[1].Symbol, 0.5/count)
self.chains = {}