Overall Statistics |
Total Trades 126 Average Win 14.73% Average Loss -11.02% Compounding Annual Return -51.486% Drawdown 79.800% Expectancy 0.022 Net Profit -51.486% Sharpe Ratio 0.11 Sortino Ratio 0.129 Probabilistic Sharpe Ratio 14.731% Loss Rate 56% Win Rate 44% Profit-Loss Ratio 1.34 Alpha 0.169 Beta -0.252 Annual Standard Deviation 1.112 Annual Variance 1.236 Information Ratio -0.057 Tracking Error 1.119 Treynor Ratio -0.483 Total Fees $9425.52 Estimated Strategy Capacity $14000000.00 Lowest Capacity Asset GF XBMB8NZ7UL1D Portfolio Turnover 79.15% |
# http://quantpedia.com/Screener/Details/22 from datetime import timedelta from math import floor from decimal import Decimal import numpy as np from AlgorithmImports import * class CommodityTermStructureAlgorithm(QCAlgorithm): def Initialize(self): self.SetStartDate(2019, 1, 1) self.SetEndDate(2020, 1, 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.Daily) self.Schedule.On(self.DateRules.MonthStart("SPY"), self.TimeRules.AfterMarketOpen("SPY", 30), self.Rebalance) self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel(portfolioBias=PortfolioBias.Long)) self.AddRiskManagement(NullRiskManagementModel()) self.SetExecution(ImmediateExecutionModel()) self.SetWarmUp(60) self.Settings.FreePortfolioValuePercentage = 0.30 def OnData(self,slice): if self.IsWarmingUp: return # 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 = {} positive_roll_returns = [] negative_roll_returns = [] if self.chains.items() is None: return 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 } if positive_roll_returns is not None and negative_roll_returns is not None: 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 = {}