The strategy trades Chinese commodity futures using a basis-momentum signal, constructing a long-short, equally-weighted, monthly rebalanced portfolio while ensuring liquidity through volume thresholds and gradual contract rolling.

I. STRATEGY IN A NUTSHELL

The strategy trades Chinese commodity futures using 12-month basis-momentum, constructing long-short portfolios, applying gradual contract rolls, and rebalancing monthly to capture liquidity-adjusted momentum opportunities.

II. ECONOMIC RATIONALE

Basis-momentum exploits speculators’ market-clearing roles and term structure dynamics, with rolling methods enhancing liquidity management and capacity, enabling consistent, momentum-driven returns in commodity futures markets.

III. SOURCE PAPER

Investable Commodity Premia in China [Click to Open PDF]

Robert J. Bianchi, Griffith University; John Hua Fan, Griffith University – Department of Accounting, Finance and Economics; Tingxi Zhang, Curtin University

<Abstract>

We investigate the investability of commodity risk premia in China. Previously documented standard momentum, carry and basis-momentum factors are not investable due to the unique liquidity patterns along the futures curves in China. However, dynamic rolling and strategic portfolio weights significantly boost the investment capacity of such premia without compromising its statistical and economic significance. Meanwhile, style integration delivers enhanced performance and improved opportunity sets. Furthermore, the observed investable premia are robust to execution lags, stop-loss, illiquidity, sub-period specifications, seasonality and transaction costs. They also offer portfolio diversification for investors. Finally, investable commodity premia in China reveal strong predictive ability with global real economic growth.

IV. BACKTEST PERFORMANCE

Annualised Return7.93%
Volatility10.06%
Beta0.019
Sharpe Ratio0.79
Sortino RatioN/A
Maximum Drawdown-23.53%
Win Rate45%

V. FULL PYTHON CODE

from AlgorithmImports import *
#endregion
class BasisMomentumCommodityPremiainChina(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2017, 1, 1)
        self.SetCash(100000)
        
        # NOTE: QC max cap of 100 custom symbols added => 50 commodities x2 contracts
        self.symbols:list[str] = [
            # 'ER','ME','RO','S','TC','WS','WT',    # empty
            
            'CU', 'A', 'AG', 'AL', 'AP', 'AU',
            'B', 'BB', 'BU', 'C', 'CF', 'CS',
            'CY', 'FB', 'FG', 'FU', 'HC', 'I',
            'IC', 'IF', 'IH', 'J', 'JD', 'JM', 
            'JR', 'L', 'LR', 'M', 'MA', 'NI',
            'OI', 'P', 'PB', 'PM', 'PP', 'RB',
            'RI', 'RM', 'RS', 'RU',  'SF', 'SM',
            'SN', 'SR', 'T', 'TF', 'V',
            'WR', 'ZN', 'Y'
            #  'ZC', 'TA', 'WH'
        ]
        
        self.period:int = 12*21
        self.SetWarmup(self.period, Resolution.Daily)
        self.price_data:dict = {}
        self.contract_range:list[int] = [3, 4]  # 3rd and 4th futures contract
        self.latest_update_date:dict = {}   # latest price data arrival time
        for symbol in self.symbols:
            # futures data
            for i in self.contract_range:
                sym = symbol + str(i)
                data = self.AddData(QuantpediaChineseFutures, sym, Resolution.Daily)
                data.SetLeverage(5)
                data.SetFeeModel(CustomFeeModel())
                self.price_data[sym] = RollingWindow[float](self.period)
            self.latest_update_date[symbol] = None
                
        self.recent_month = -1
    
    def OnData(self, data):
        rebalance_flag:bool = False
        basis_momentum_signal:dict[Symbol, float] = {}
        # store daily prices
        for symbol in self.symbols:
            # both contracts data points are available
            if all(symbol+str(i) in data and data[symbol+str(i)] and data[symbol+str(i)].Value != 0 for i in self.contract_range):
                near_c_symbol:str = symbol + str(self.contract_range[0])  # 3rd contract
                dist_c_symbol:str = symbol + str(self.contract_range[1])  # 4th contract
                self.price_data[near_c_symbol].Add(data[near_c_symbol].Value)  # 3rd contract price
                self.price_data[dist_c_symbol].Add(data[dist_c_symbol].Value)  # 4th contract price
                self.latest_update_date[symbol] = self.Time.date()
                if self.IsWarmingUp: continue
                # rebalance date
                if self.Time.month != self.recent_month:
                    rebalance_flag = True
                    # check data arrival time
                    if (self.Time.date() - self.latest_update_date[symbol]).days > 5:
                        continue
                    # price data for both contracts are ready
                    if self.price_data[near_c_symbol].IsReady and self.price_data[dist_c_symbol].IsReady:
                        # calculate momentum from forward ratio rolled contracts
                        near_momentum:float = self.price_data[near_c_symbol][0] / self.price_data[near_c_symbol][self.period-1] - 1
                        dist_momentum:float = self.price_data[dist_c_symbol][0] / self.price_data[dist_c_symbol][self.period-1] - 1
                        basis_momentum_signal[near_c_symbol] = near_momentum - dist_momentum
        # monthly rebalance
        if self.Time.month == self.recent_month:
            return
        self.recent_month = self.Time.month
        # buying the commodity futures with positive signal and selling commodity futures with a negative signal
        long:list[Symbol] = [x[0] for x in basis_momentum_signal.items() if x[1] > 0.]
        short:list[Symbol] = [x[0] for x in basis_momentum_signal.items() if x[1] < 0.]
        # trade execution
        invested = [x.Key for x in self.Portfolio if x.Value.Invested]
        for symbol in invested:
            if symbol not in long + short:
                self.Liquidate(symbol)
        for symbol in long:
            self.SetHoldings(symbol, 1 / len(long))
        
        for symbol in short:
            self.SetHoldings(symbol, -1 / len(short))
# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaChineseFutures(PythonData):
    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/china/forward_ratio_rolled/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
    def Reader(self, config, line, date, isLiveMode):
        data = QuantpediaChineseFutures()
        data.Symbol = config.Symbol
        
        if not line[0].isdigit(): 
            return None
        split = line.split(';')
        
        data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
        data['close'] = float(split[1]) if split[1] != '' else 0 # unadjusted close
        data['adj_close'] = float(split[2]) if split[2] != '' else 0
        data['last_trade_month'] = int(split[3])
        data.Value = float(split[2]) if split[2] != '' else 0
        return data
# Custom fee model.
class CustomFeeModel():
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading