Overall Statistics
Total Orders
507
Average Win
1.96%
Average Loss
-2.09%
Compounding Annual Return
-18.586%
Drawdown
79.300%
Expectancy
-0.135
Start Equity
10000000
End Equity
3574804.22
Net Profit
-64.252%
Sharpe Ratio
-0.271
Sortino Ratio
-0.249
Probabilistic Sharpe Ratio
0.093%
Loss Rate
55%
Win Rate
45%
Profit-Loss Ratio
0.94
Alpha
-0.102
Beta
0.002
Annual Standard Deviation
0.377
Annual Variance
0.142
Information Ratio
-0.433
Tracking Error
0.403
Treynor Ratio
-57.312
Total Fees
$50503.52
Estimated Strategy Capacity
$320000.00
Lowest Capacity Asset
PL Z0A8S1BLJXS1
Portfolio Turnover
7.77%
Drawdown Recovery
201
#region imports
from AlgorithmImports import *

from math import floor
#endregion
# https://quantpedia.com/Screener/Details/23


class CommodityMomentumCombinedWithTermStructureAlgorithm(QCAlgorithm):

    def initialize(self):
        self.set_start_date(self.end_date - timedelta(5*365))
        self.settings.seed_initial_prices = True
        self.set_cash(10_000_000)
        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.MICRO_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(90))
        self.schedule.on(self.date_rules.month_start("SPY"), self.time_rules.after_market_open("SPY", 30), self._rebalance)

    def _rebalance(self):
        roll_return_by_contract = {}
        for symbol, chain in self.current_slice.future_chains.items():
            if len(chain) < 2:
                continue
            contracts = sorted(chain, key=lambda x: x.expiry)
            expiry_nearest = contracts[0].expiry
            price_nearest = contracts[0].last_price or 0.5*(contracts[0].ask_price + contracts[0].bid_price)
            for c in contracts[1:]:
                if not c.open_interest:
                    continue
                security = self.securities[c.symbol]
                if not security.price:
                    continue
                roll_return_by_contract[security] = (price_nearest-c.last_price)*365 / (c.expiry-expiry_nearest).days
        
        sorted_by_roll_return = sorted(roll_return_by_contract, key=lambda c: roll_return_by_contract[c], reverse=True)
        tertile = floor(1/3*len(sorted_by_roll_return))

        targets = self._create_targets(sorted_by_roll_return[:tertile])
        targets += self._create_targets(sorted_by_roll_return[-tertile:], False, -1)        
        self.set_holdings(targets, True)
    
    def _create_targets(self, contracts, reverse=True, direction=1):
        selected_contracts = sorted(
            contracts, 
            key=lambda contract: contract.mean_return.current.value, 
            reverse=reverse
        )[:int(len(contracts)*0.5)]
        weight = direction*0.05/len(selected_contracts)
        return [PortfolioTarget(contract.symbol, weight) for contract in selected_contracts]

    def on_securities_changed(self, changes):
        # Add an indicator to each contract to track its mean daily return.
        for security in changes.added_securities:
            roc = self.roc(security, 1, Resolution.DAILY)
            security.mean_return = IndicatorExtensions.sma(roc, 21)
            for bar in self.history[TradeBar](security, 22, Resolution.DAILY):
                roc.update(bar)