Overall Statistics
Total Orders
170
Average Win
1.92%
Average Loss
-1.88%
Compounding Annual Return
0.843%
Drawdown
14.200%
Expectancy
0.059
Start Equity
1000000
End Equity
1068748.82
Net Profit
6.875%
Sharpe Ratio
-0.282
Sortino Ratio
-0.281
Probabilistic Sharpe Ratio
0.266%
Loss Rate
48%
Win Rate
52%
Profit-Loss Ratio
1.02
Alpha
-0.008
Beta
0.172
Annual Standard Deviation
0.036
Annual Variance
0.001
Information Ratio
0.028
Tracking Error
0.057
Treynor Ratio
-0.06
Total Fees
$716.30
Estimated Strategy Capacity
$4000000.00
Lowest Capacity Asset
GC Y9O6T2ED3VRX
Portfolio Turnover
2.65%
from AlgorithmImports import *

class PredictionOnFuturesContango(QCAlgorithm):
    def initialize(self):
        self.set_start_date(2015, 8, 1)
        self.set_end_date(2023, 7, 1)
        self.set_cash(1000000)

        # Subscribe and set our expiry filter for the futures chain
        self.future_g_o_l_d = self.add_future(
            Futures.Metals.GOLD, 
            resolution = Resolution.MINUTE,
            data_normalization_mode = DataNormalizationMode.BACKWARDS_RATIO,
            data_mapping_mode = DataMappingMode.OPEN_INTEREST,
            contract_depth_offset = 0
        )
        # expiry between 0 and 90 days to avoid naked position stays for too long to tie up fund
        self.future_g_o_l_d.set_filter(0, 90)

        # 20-day SMA on return as the basis mean-reversion predictor
        self.roc = self.ROC(self.future_g_o_l_d.symbol, 1, Resolution.DAILY)
        self.sma = IndicatorExtensions.of(SimpleMovingAverage(20), self.roc)
        self.set_warm_up(21, Resolution.DAILY)

        ief = self.add_equity("IEF").symbol
        self.set_benchmark(ief)

    def on_data(self, slice):
        if not self.portfolio.invested and not self.is_warming_up:
            # We only trade during last-day return is lower than average return
            if not self.roc.is_ready or not self.sma.is_ready or self.sma.current.value < self.roc.current.value:
                return

            spreads = {}

            for chain in slice.future_chains:
                contracts = list(chain.value)

                # if there is less than or equal 1 contracts, we cannot compare the spot price
                if len(contracts) < 2: continue

                # sort the contracts by expiry
                sorted_contracts = sorted(contracts, key=lambda x: x.expiry)
                # compare the spot price
                for i, contract in enumerate(sorted_contracts):
                    if i == 0: continue

                    # compare the ask price for each contract having nearer term
                    for j in range(i):
                        near_contract = sorted_contracts[j]

                        # get the spread and total cost (price of contracts and commission fee $1 x 2)
                        horizontal_spread = contract.bid_price - near_contract.ask_price
                        total_price = contract.bid_price + near_contract.ask_price + 2
                        spreads[(contract.symbol, near_contract.symbol)] = (horizontal_spread, total_price)

            # Select the pair with the lowest spread to trade for maximum potential contango
            if spreads:
                min_spread_pair = sorted(spreads.items(), key=lambda x: x[1][0])[0]
                far_contract, near_contract = min_spread_pair[0]

                # subscribe to the contracts to avoid removing from the universe
                self.add_future_contract(far_contract, Resolution.MINUTE)
                self.add_future_contract(near_contract, Resolution.MINUTE)

                num_of_contract = max((self.portfolio.total_portfolio_value / min_spread_pair[1][1]) // self.future_g_o_l_d.symbol_properties.contract_multiplier, 1)
                self.market_order(far_contract, num_of_contract)
                self.market_order(near_contract, -num_of_contract)