Quant BuffetRelax, Not Over Thinking

Cross-Section of Stock Returns Predicted by Commitment of Traders Information

Log in to collect

Academic paper

Is There Smart Money? How Information in the Futures Market Gets Priced into the Cross-Section of Stock Returns with Delay

AuthorsSteven Wei Ho; Alexandre R. Lauwers

Institute
  • Columbia University
  • ?Columbia University, Graduate School of Arts and Sciences, Department of Economics
  • CHUniversity of Geneva
  • CHGraduate Institute of International and Development Studies
  • ?University of Geneva - Graduate Institute, Geneva (IHEID)

Strategy in a nutshell

Trades NYSE, AMEX, and NASDAQ stocks linked to 11 commodities using CFTC Disaggregated COT data. Calculates growth in managed money (MM) long positions as a signal. Go long on positive signal growth, short on negative. Equal-weighted portfolios, rebalanced weekly.

Economic rationale

MM trader positions in futures predict related stock returns, reflecting informed speculative views on commodity prices. Their signals generate strong alphas independent of standard factors, robust across weighting schemes, timing, and business cycles.

Backtest performance

Annualised return19.21%
Volatility28.57%
Beta-0.047
Sharpe ratio0.67
Sortino ratio0.042
Win rate52%

Full Python code

from AlgorithmImports import *
from functools import reduce
from typing import List, Dict, Tuple
from numpy import isnan
class CrossSectionOfStockReturnsPredictedByCommitmentOfTradersInformation(QCAlgorithm):
def Initialize(self):
 self.SetStartDate(2005, 1, 1)
 self.SetCash(100000)
 
 self.exchange_codes:List[str] = ['NYS', 'NAS', 'ASE']	
 
 self.min_share_price:int = 5
 self.leverage:int = 5
 self.SIC_stocks = {}    # storing list of stocks symbols keyed by SIC code
 
 self.COT_tickers_SICs:List[Tuple[List]] = [
     (['QHG'], [1020, 1021, 3331]), # Copper
     (['QGC'], [1040, 1041]), # Gold
     (['QSI'], [1044]), # Silver
     (['QLB'], [2400]), # Lumber
     (['QGO', 'QCL'], [1310, 1311]), # Gas, Oil
     (['QPL', 'QPA'], [3449, 3491, 3492, 3493, 3494, 3495, 3496, 3497, 3498, 3499]), # Platinum, Palladium
 ]
 
 # create 1D list from SIC codes
 self.SIC_universe:List[int] = map(lambda x: x[1], self.COT_tickers_SICs)
 self.SIC_universe:List[int] = reduce(lambda x,y: x+y , self.SIC_universe)
 
 self.last_long_prop:Dict[str, None] = {
     'QHG': None,
     'QGC': None,
     'QSI': None,
     'QLB': None,
     'QGO': None,
     'QCL': None,
     'QPL': None,
     'QPA': None
 }
 
 # subscribe to COT data
 for cot_ticker, _ in self.last_long_prop.items():
     data = self.AddData(CommitmentsOfTraders, cot_ticker, Resolution.Daily)
     
 self.symbol:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
 
 self.selection_flag:bool = False
 self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
 self.UniverseSettings.Resolution = Resolution.Daily
 self.AddUniverse(self.FundamentalSelectionFunction)
 
 self.Schedule.On(self.DateRules.MonthStart(self.symbol), self.TimeRules.BeforeMarketClose(self.symbol, 0), self.Selection)
 self.settings.daily_precise_end_time = False
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
 for security in changes.AddedSecurities:
     security.SetFeeModel(CustomFeeModel())
     security.SetLeverage(self.leverage)
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
 # selection on monthly basis
 if not self.selection_flag:
     return Universe.Unchanged
 self.selection_flag = False
 
 # filter all symbol of stocks
 selected:List[Fundamental] = [
     x for x in fundamental if x.HasFundamentalData and x.Price > self.min_share_price \
     and not isnan(x.AssetClassification.SIC != 0) and (x.AssetClassification.SIC != 0) \
     and (x.AssetClassification.SIC in self.SIC_universe) and x.SecurityReference.ExchangeId in self.exchange_codes
 ]
 
 selected_symbols:List[Symbol] = []
 # firstly clear stocks from old selection
 self.SIC_stocks.clear()
 
 # store relevant stocks symbols into their basket according to SIC code
 for stock in selected:
     symbol = stock.Symbol
     SIC_code = stock.AssetClassification.SIC
     
     # make sure list for stocks is initialized
     if SIC_code not in self.SIC_stocks:
         self.SIC_stocks[SIC_code] = []
     
     # add stock's symbol to it's basket based on SIC code
     self.SIC_stocks[SIC_code].append(symbol)
 
     selected_symbols.append(symbol)
     
 return selected_symbols
 
def OnData(self, data: Slice) -> None:
 COT_data_last_update_date:Dict[Symbol, datetime.date] = CommitmentsOfTraders.get_last_update_date()
 # storing tuples (SIC_list, long_proportion_growth_value)
 long_proportion_growth:List[Tuple[List, float]] = []
 rebalance_flag:bool = False
 
 for COT_ticker_list, SIC_list in self.COT_tickers_SICs:
     
     long_proportion_growth_values:List[float] = []
     
     for COT_ticker in COT_ticker_list:
         
         if self.Securities[COT_ticker].GetLastData() and self.Time.date() < COT_data_last_update_date[COT_ticker]:
             if COT_ticker in data and data[COT_ticker]:
                 rebalance_flag = True
                 
                 # retrieve needed values from data object
                 large_spec_long:int = data[COT_ticker].get_Item('LARGE_SPECULATOR_LONG')
                 large_spec_short:int = data[COT_ticker].get_Item('LARGE_SPECULATOR_SHORT')
                 
                 if large_spec_long == 0 or large_spec_short == 0:
                     continue
                 
                 if not self.last_long_prop[COT_ticker]:
                     value:float = large_spec_long / (large_spec_short + large_spec_long + 0)
                     self.last_long_prop[COT_ticker] = value
                     continue
                 
                 curr_long_proportion:float = large_spec_long / (large_spec_short + large_spec_long + 0)
                 growth_value:float = (curr_long_proportion - self.last_long_prop[COT_ticker]) / self.last_long_prop[COT_ticker]
                 
                 # append long proportion growth value for current COT data
                 long_proportion_growth_values.append(growth_value)
                 
                 # update last long proporiton value
                 self.last_long_prop[COT_ticker] = curr_long_proportion
         
     if len(long_proportion_growth_values) != 0:
         # storing tuples (SIC_list, long_proportion_growth_value)
         long_proportion_growth.append( (SIC_list, np.mean(long_proportion_growth_values)) )
 
 # rebalance weekly
 if len(long_proportion_growth) != 0 and rebalance_flag:
     # long stocks with positive signal growth rates and short stocks with negative signal growth.
     long, short = self.CreateLongShortPortfolio(long_proportion_growth)
     
     # order execution
     targets:List[PortfolioTarget] = []
     for i, portfolio in enumerate([long, short]):
         for symbol in portfolio:
             if symbol in data and data[symbol]:
                 targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
     
     self.SetHoldings(targets, True)
     
 elif len(long_proportion_growth) == 0 and rebalance_flag:
     self.Liquidate()
         
def CreateLongShortPortfolio(self, long_proportion_growth:Tuple):
 long:List[Symbol] = []
 short:List[Symbol] = []
 
 # long stocks with positive signal growth rates and short stocks with negative signal growth.
 for SIC_list, value in long_proportion_growth:
     for SIC in SIC_list:
         
         # make sure SIC code has stocks
         if SIC not in self.SIC_stocks:
             continue
         
         if value > 0:
             long += self.SIC_stocks[SIC]
         else:
             short += self.SIC_stocks[SIC]
 
 return long, short
 
def Selection(self) -> None:
 self.selection_flag = True
# Commitments of Traders data.
# NOTE: IMPORTANT: Data order must be ascending (datewise).
# Data source: https://commitmentsoftraders.org/cot-data/
# Data description: https://commitmentsoftraders.org/wp-content/uploads/Static/CoTData/file_key.html
class CommitmentsOfTraders(PythonData):
_last_update_date:Dict[Symbol, datetime.date] = {}
@staticmethod
def get_last_update_date() -> Dict[Symbol, datetime.date]:
return CommitmentsOfTraders._last_update_date
def GetSource(self, config, date, isLiveMode):
 return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/cot/{0}.PRN".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
# File example.
# DATE   OPEN     HIGH        LOW       CLOSE     VOLUME   OI
# ----   ----     ----        ---       -----     ------   --
# DATE   LARGE    SPECULATOR  COMMERCIAL HEDGER   SMALL TRADER
#        LONG     SHORT       LONG      SHORT     LONG     SHORT
def Reader(self, config, line, date, isLiveMode):
 data = CommitmentsOfTraders()
 data.Symbol = config.Symbol
 
 if not line[0].isdigit(): return None
 split = line.split(',')
 
 # Prevent lookahead bias.
 data.Time = datetime.strptime(split[0], "%Y%m%d") + timedelta(days=1)
 
 data['LARGE_SPECULATOR_LONG'] = int(split[1])
 data['LARGE_SPECULATOR_SHORT'] = int(split[2])
 data['COMMERCIAL_HEDGER_LONG'] = int(split[3])
 data['COMMERCIAL_HEDGER_SHORT'] = int(split[4])
 data['SMALL_TRADER_LONG'] = int(split[5])
 data['SMALL_TRADER_SHORT'] = int(split[6])
 data.Value = int(split[1])
 if config.Symbol.Value not in CommitmentsOfTraders._last_update_date:
     CommitmentsOfTraders._last_update_date[config.Symbol.Value] = datetime(1,1,1).date()
 if data.Time.date() > CommitmentsOfTraders._last_update_date[config.Symbol.Value]:
     CommitmentsOfTraders._last_update_date[config.Symbol.Value] = data.Time.date()
 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"))