Overall Statistics |
Total Orders 250 Average Win 5.24% Average Loss -3.25% Compounding Annual Return 78.549% Drawdown 36.500% Expectancy 0.348 Start Equity 1000000 End Equity 4256445.14 Net Profit 325.645% Sharpe Ratio 1.614 Sortino Ratio 2.095 Probabilistic Sharpe Ratio 71.889% Loss Rate 48% Win Rate 52% Profit-Loss Ratio 1.61 Alpha 0.609 Beta -0.396 Annual Standard Deviation 0.356 Annual Variance 0.127 Information Ratio 1.283 Tracking Error 0.379 Treynor Ratio -1.452 Total Fees $12947.74 Estimated Strategy Capacity $0 Lowest Capacity Asset CL WVICXLISS69T Portfolio Turnover 22.40% |
#region imports from AlgorithmImports import * from math import floor #endregion # http://quantpedia.com/Screener/Details/22 class CommodityTermStructureAlgorithm(QCAlgorithm): def initialize(self): self.set_start_date(2016, 1, 1) self.set_end_date(2018, 7, 1) self.set_cash(1000000) tickers = [ Futures.Softs.COCOA, Futures.Softs.COFFEE, Futures.Grains.CORN, Futures.Softs.COTTON_2, Futures.Grains.OATS, Futures.Softs.ORANGE_JUICE, Futures.Grains.SOYBEAN_MEAL, Futures.Grains.SOYBEAN_OIL, Futures.Grains.SOYBEANS, Futures.Softs.SUGAR_11, Futures.Grains.WHEAT, Futures.Meats.FEEDER_CATTLE, Futures.Meats.LEAN_HOGS, Futures.Meats.LIVE_CATTLE, Futures.Energies.CRUDE_OIL_WTI, Futures.Energies.HEATING_OIL, Futures.Energies.NATURAL_GAS, Futures.Energies.GASOLINE, Futures.Metals.GOLD, Futures.Metals.PALLADIUM, Futures.Metals.PLATINUM, Futures.Metals.SILVER ] for ticker in tickers: future = self.add_future(ticker) future.set_filter(timedelta(0), timedelta(days=90)) self.add_equity("SPY", Resolution.MINUTE) self.schedule.on(self.date_rules.month_start("SPY"), self.time_rules.after_market_open("SPY", 30), self._rebalance) def _rebalance(self): self.liquidate() roll_returns = {} chains = {} for chain in self.current_slice.future_chains: if chain.value.contracts.count < 2: continue symbol = chain.value.symbol.value chain = [i for i in chain.value] chains[symbol] = chain 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.last_price if near_contract.last_price>0 else 0.5*float(near_contract.ask_price+near_contract.bid_price) price_distant = distant_contract.last_price if distant_contract.last_price>0 else 0.5*float(distant_contract.ask_price+distant_contract.bid_price) 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} quintile = floor(len(roll_returns)/5) 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: return for short_symbol in contango: sort = sorted(chains[short_symbol], key=lambda x: x.expiry) self.set_holdings(sort[0].symbol, -0.1/count) for long_symbol in backwardation: sort = sorted(chains[long_symbol], key=lambda x: x.expiry) self.set_holdings(sort[0].symbol, 0.1/count)