The strategy trades futures across asset classes, ranking contracts by notional value, creating weighted long/short portfolios, combining them via equal volatility weighting, and rebalancing daily for balanced risk exposure.

I. STRATEGY IN A NUTSHELL

Trade front-month futures across equities, bonds, rates, currencies, and commodities. Rank contracts by notional value within each asset class, creating long/short portfolios weighted by rank deviations, and combine them into an equal-volatility, daily-rebalanced multi-asset portfolio.

II. ECONOMIC RATIONALE

Lower notional value contracts earn premiums due to higher illiquidity and volatility. The strategy captures these risk-based returns, generating alpha largely unexplained by traditional factors.

III. SOURCE PAPER

Notional Value Effect in Futures Markets [Click to Open PDF]

Theodosios Athanasiadis, Erevna Capital Management

<Abstract>

We examine how the notional value of futures contracts predicts the cross-section of returns within the major asset classes tracking a large number of futures contracts. We find that low notional value contracts outperform high notional value contracts within government bonds, short-term rates, commodities, currencies, and equity indexes. A diversified portfolio of the strategy delivers abnormal returns after controlling for standard asset pricing factors. The strategy is related to value and reversal factors but their explanatory power is low. Differences in liquidity explain a large portion of the cross-section of notional value, where high notional value contracts are more liquid, and subsume the reversal and value factors. Volatility risk can be a partial explanation both cross-sectionally where low notional value contracts exhibit higher volatility and across time where shocks to market volatility decrease strategy returns.

IV. BACKTEST PERFORMANCE

Annualised Return3.38%
Volatility4.74%
Beta-0.017
Sharpe Ratio0.71
Sortino RatioN/A
Maximum DrawdownN/A
Win Rate46%

V. FULL PYTHON CODE

from AlgorithmImports import *
import numpy as np
import pandas as pd
#endregion
class NotionalValueEffectInFuturesMarkets(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetCash(100000)
        self.symbols = [
                        ("CME_S1", "Futures.Grains.Soybeans", 1), # Soybean Futures, Continuous Contract
                        ("CME_W1", "Futures.Grains.SRWWheat", 1), # Wheat Futures, Continuous Contract
                        ("CME_SM1", "Futures.Grains.SoybeanMeal", 1), # Soybean Meal Futures, Continuous Contract
                        ("CME_BO1", "Futures.Grains.SoybeanOil", 1), # Soybean Oil Futures, Continuous Contract
                        ("CME_C1", "Futures.Grains.Corn", 1), # Corn Futures, Continuous Contract
                        ("CME_O1", "Futures.Grains.Oats", 1), # Oats Futures, Continuous Contract
                        ("CME_LC1", "Futures.Meats.LiveCattle", 1), # Live Cattle Futures, Continuous Contract
                        ("CME_FC1", "Futures.Meats.FeederCattle", 1), # Feeder Cattle Futures, Continuous Contract
                        ("CME_GC1", "Futures.Metals.Gold", 1), # Gold Futures, Continuous Contract
                        ("CME_SI1", "Futures.Metals.Silver", 1), # Silver Futures, Continuous Contract
                        ("CME_PL1", "Futures.Metals.Platinum", 1), # Platinum Futures, Continuous Contract
                        ("CME_CL1", "Futures.Energies.CrudeOilWTI", 1), # Crude Oil Futures, Continuous Contract
                        ("CME_HG1", "Futures.Metals.Copper", 1), # Copper Futures, Continuous Contract
                        ("CME_LB1", "Futures.Forestry.RandomLengthLumber", 1), # Random Length Lumber Futures, Continuous Contract
                        ("CME_NG1", "Futures.Energies.NaturalGasHenryHubLastDayFinancial", 1), # Natural Gas (Henry Hub) Physical Futures, Continuous Contract
                        ("CME_PA1", "Futures.Metals.Palladium", 1), # Palladium Futures, Continuous Contract 
                        # ("CME_CU1", "Futures.Energies.ChicagoEthanolPlatts", 1), # Chicago Ethanol (Platts) Futures
                        ("CME_DA1", "Futures.Dairy.ClassIIIMilk", 1), # Class III Milk Futures
                        ("CME_RB2", "Futures.Energies.GulfCoastCBOBGasolineA2PlattsVsRBOBGasoline", 1), # Gasoline Futures, Continuous Contract
                        
                        ("ICE_CC1", "Futures.Softs.Cocoa", 1), # Cocoa Futures, Continuous Contract 
                        ("ICE_O1", "Futures.Energies.HeatingOil", 1), # Heating Oil Futures, Continuous Contract
                        ("ICE_GO1", "Futures.Energies.SingaporeGasoilPlattsVsLowSulphurGasoilFutures", 1),  # Gas Oil Futures, Continuous Contract
                        ("ICE_WT1", "Futures.Energies.WTIHoustonCrudeOil", 1), # WTI Crude Futures, Continuous Contract
                                                
                        ("CME_AD1", "Futures.Currencies.AUD", 2), # Australian Dollar Futures, Continuous Contract #1
                        ("CME_BP1", "Futures.Currencies.GBP", 2), # British Pound Futures, Continuous Contract #1
                        ("CME_CD1", "Futures.Currencies.CAD", 2), # Canadian Dollar Futures, Continuous Contract #1
                        ("CME_EC1", "Futures.Currencies.EUR", 2), # Euro FX Futures, Continuous Contract #1
                        ("CME_JY1", "Futures.Currencies.AUDJPY", 2), # Japanese Yen Futures, Continuous Contract #1
                        ("CME_MP1", "Futures.Currencies.MXN", 2), # Mexican Peso Futures, Continuous Contract #1
                        ("CME_NE1", "Futures.Currencies.AUDNZD", 2), # New Zealand Dollar Futures, Continuous Contract #1
                        ("CME_SF1", "Futures.Currencies.CHF", 2), # Swiss Franc Futures, Continuous Contract #1
                    
                        ("CME_NQ1", "Futures.Indices.NASDAQ100EMini", 3), # E-mini NASDAQ 100 Futures, Continuous Contract #1
                        ("CME_ES1", "Futures.Indices.SP500EMini", 3), # E-mini S&P 500 Futures, Continuous Contract #1
                        ("SGX_NK1", "Futures.Indices.Nikkei225Yen", 3), # SGX Nikkei 225 Index Futures, Continuous Contract #1
                        ("CME_MD1", "Futures.Indices.SP400MidCapEmini", 3), # E-mini S&P MidCap 400 Futures
                        
                        ("CME_TY1", "Futures.Financials.Y10TreasuryNote", 4), # 10 Yr Note Futures, Continuous Contract #1
                        ("CME_FV1", "Futures.Financials.Y5TreasuryNote", 4), # 5 Yr Note Futures, Continuous Contract #1
                        ("CME_TU1", "Futures.Financials.Y2TreasuryNote", 4), # 2 Yr Note Futures, Continuous Contract #1
                        ("SGX_JB1", "Futures.Currencies.JapaneseYenEmini", 4) # SGX 10-Year Mini Japanese Government Bond Futures
                    ]
                    
        self.period = 3 * 21 # Three months of daily data.
        self.volatility_target = 0.1
        self.leverage_cap = 5
        
        self.data = {}
        
        for symbol_tuple in self.symbols:
            quantpedia_future = symbol_tuple[0]
            qc_future = symbol_tuple[1]
            asset_group = symbol_tuple[2]
            # Back adjusted and spliced data import.
            data = self.AddData(QuantpediaFutures, quantpedia_future, Resolution.Daily)
            
            data.SetFeeModel(CustomFeeModel())
            data.SetLeverage(10)
            
            data = self.AddFuture(qc_future, Resolution.Daily)
            contract_multiplier = data.SymbolProperties.ContractMultiplier
            
            self.data[quantpedia_future] = SymbolData(self.period, contract_multiplier, asset_group)
    def OnData(self, data):
        indexes = {} # asset_group = 3
        currencies = {} # asset_group = 2
        commodities = {} # asset_group = 1
        goverment_bonds = {} # asset_group = 4
        
        for symbol_tuple in self.symbols:
            symbol = symbol_tuple[0]
            
            if symbol in data and data[symbol]:
                price = data[symbol].Value
                self.data[symbol].update(price)
                
            if self.data[symbol].is_ready():
                asset_group = self.data[symbol].AssetGroup
                
                # notional value = asset price * contract multiplier
                notional_value = self.data[symbol].LastPrice * self.data[symbol].ContractMultiplier
                
                if asset_group == 1:
                    commodities[symbol] = notional_value
                elif asset_group == 2:
                    currencies[symbol] = notional_value
                elif asset_group == 3:
                    indexes[symbol] = notional_value
                elif asset_group == 4:
                    goverment_bonds[symbol] = notional_value
                    
        self.Liquidate()
                
        if len(indexes) or len(commodities) or len(goverment_bonds) or len(currencies):
            # First element in sorted list has the first rank in it's rank list,
            # because future with highest notional value has lowest rank.
            sorted_indexes = [x[0] for x in sorted(indexes.items(), key = lambda item: item[1], reverse=True)]
            sorted_commodities = [x[0] for x in sorted(commodities.items(), key = lambda item: item[1], reverse=True)]
            sorted_goverment_bonds = [x[0] for x in sorted(goverment_bonds.items(), key = lambda item: item[1], reverse=True)]
            sorted_currencies = [x[0] for x in sorted(currencies.items(), key = lambda item: item[1], reverse=True)]
            
            indexes_avg_rank = np.mean([x[1] for x in indexes.items()])
            commodities_avg_rank = np.mean([x[1] for x in commodities.items()])
            goverment_bonds_avg_rank = np.mean([x[1] for x in goverment_bonds.items()])
            currencies_avg_rank = np.mean([x[1] for x in currencies.items()])
            
            indexes_notional_values = [x[1] for x in sorted(indexes.items(), key = lambda item: item[1], reverse=True)]
            commodities_notional_values = [x[1] for x in sorted(commodities.items(), key = lambda item: item[1], reverse=True)]
            goverment_bonds_notional_values = [x[1] for x in sorted(goverment_bonds.items(), key = lambda item: item[1], reverse=True)]
            currencies_notional_values = [x[1] for x in sorted(currencies.items(), key = lambda item: item[1], reverse=True)]
            
            # The weight of particular contract is determined as its rank minus the average rank of all contracts from this asset class
            # multiplied by a constant to ensure that the weights of long (short) sum to 1 (-1).
            indexes_weights = self.CreateWeights(sorted_indexes, indexes_notional_values, indexes_avg_rank)
            
            commodities_weights = self.CreateWeights(sorted_commodities, commodities_notional_values, commodities_avg_rank)
            
            goverment_bonds_weights = self.CreateWeights(sorted_goverment_bonds, goverment_bonds_notional_values, goverment_bonds_avg_rank)
            
            currencies_weights = self.CreateWeights(sorted_currencies, currencies_notional_values, currencies_avg_rank)
            
            # Calculate portfolio volatilty for each asset class
            indexes_vol = self.AnnualPortfolioVolatility(indexes_weights)
            commodities_vol = self.AnnualPortfolioVolatility(commodities_weights)
            goverment_bonds_vol = self.AnnualPortfolioVolatility(goverment_bonds_weights)
            currencies_vol = self.AnnualPortfolioVolatility(currencies_weights)
            
            # Calculate asset class weights.
            # List returned from self.AssetClassWeights:
            # [indexes_ac_weight, commodities_ac_weight, govermnet_bonds_ac_weight, currencies_ac_weight]
            ac_weights = self.AssetClassWeights(indexes_vol, commodities_vol, goverment_bonds_vol, currencies_vol)
            
            if len(ac_weights) == 0:
                return
            
            # Trade execution.
            for symbol, weight in indexes_weights:
                leverage = self.volatility_target / indexes_vol
                leverage = min(leverage, self.leverage_cap)
                self.SetHoldings(symbol, weight * ac_weights[0] * leverage)    
                
            for symbol, weight in commodities_weights:
                leverage = self.volatility_target / commodities_vol
                leverage = min(leverage, self.leverage_cap)
                self.SetHoldings(symbol, weight * ac_weights[1] * leverage) 
                
            for symbol, weight in goverment_bonds_weights:
                leverage = self.volatility_target / goverment_bonds_vol
                leverage = min(leverage, self.leverage_cap)
                self.SetHoldings(symbol, weight * ac_weights[2] * leverage)
                
            for symbol, weight in currencies_weights:
                leverage = self.volatility_target / currencies_vol
                leverage = min(leverage, self.leverage_cap)
                self.SetHoldings(symbol, weight * ac_weights[3] * leverage)
                    
    def CreateWeights(self, symbols, notional_values, avg_rank):
        long = []
        short = []
        
        # Split symbols into long an short based on their rank.
        for symbol, notional_value in zip(symbols, notional_values):
            symbol_rank = notional_value - avg_rank
            
            if symbol_rank > 0:
                long.append([symbol, symbol_rank])
            elif symbol_rank < 0:
                short.append([symbol, symbol_rank])
            
        # Calculate total rank of long and short.
        total_long_rank = sum([x[1] for x in long])
        total_short_rank = sum([x[1] for x in short])
        
        weights = []
        
        # Create one list with long and short symbols with their weights.
        for item in long:
            weights.append([item[0], item[1] / total_long_rank])
            
        for item in short:
            weights.append([item[0], -item[1] / total_short_rank])
            
        return weights
        
    def AnnualPortfolioVolatility(self, asset_weights):
        # Portfolio volatility calc.
        df = pd.dataframe()
        weights = []
        for symbol, w in asset_weights:
            df[str(symbol)] = [x for x in self.data[symbol].Prices]
            weights.append(w)
        
        weights = np.array(weights)
        
        daily_returns = df.pct_change()
        return np.sqrt(np.dot(weights.T, np.dot(daily_returns.cov() * 252, weights)))
        
    def AssetClassWeights(self, indexes_vol, commodities_vol, goverment_bonds_vol, currencies_vol):
        ac_weights = []
        
        # Calculated based on https://breakingdownfinance.com/finance-topics/alternative-investments/equal-volatility-weighting/
        total_vol_weight = 0
        
        if indexes_vol:
            indexes_vol = 1 / indexes_vol
            total_vol_weight += indexes_vol
        
        if commodities_vol:
            commodities_vol = 1 / commodities_vol
            total_vol_weight += commodities_vol
            
        if goverment_bonds_vol:
            goverment_bonds_vol = 1 / goverment_bonds_vol
            total_vol_weight += goverment_bonds_vol
            
        if currencies_vol:
            currencies_vol = 1 / currencies_vol
            total_vol_weight += currencies_vol
            
        if total_vol_weight != 0:
            ac_weights.append(indexes_vol / total_vol_weight)
            ac_weights.append(commodities_vol / total_vol_weight)
            ac_weights.append(goverment_bonds_vol / total_vol_weight)
            ac_weights.append(currencies_vol / total_vol_weight)
        
        return ac_weights
        
class SymbolData():
    def __init__(self, period, contract_multiplier, asset_group):
        self.Prices = RollingWindow[float](period)
        self.ContractMultiplier = contract_multiplier
        self.AssetGroup = asset_group
        self.LastPrice = 0
        
    def update(self, price):
        self.Prices.Add(price)
        self.LastPrice = price
        
    def is_ready(self):
        return self.Prices.IsReady
        
# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaFutures(PythonData):
    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
    def Reader(self, config, line, date, isLiveMode):
        data = QuantpediaFutures()
        data.Symbol = config.Symbol
        
        if not line[0].isdigit(): return None
        split = line.split(';')
        
        data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1)
        data['back_adjusted'] = float(split[1])
        data['spliced'] = float(split[2])
        data.Value = float(split[1])
        return data
# Custom fee model.
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

VI. Backtest Performance

Leave a Reply

Discover more from Quant Buffet

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

Continue reading