由交易者承诺报告信息预测的股票横截面收益
登录后收藏学术论文
Steven Wei Ho; Alexandre R. Lauwers
- 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)
策略概要
该策略利用商品期货交易委员会(CFTC)的交易商持仓报告 (COT) 数据,交易与11种商品(例如,金属、能源和软商品)相关的纽约证券交易所、美国证券交易所和纳斯达克普通股。对于每个与四位SIC代码相关联的商品,识别出具有匹配代码的上市公司,从而形成11个商品特定投资组合。
分类COT(DCOT)报告提供每周交易商持仓数据。多头比例增长指标计算为管理资金(MM)交易商多头头寸增长率除以总头寸(MMlong/(MMlong+MMshort+2MMspreading))。股票每周根据滞后信号变量被分类为多空投资组合。对于具有正信号增长率的股票采取多头头寸,而空头头寸则针对负信号增长率的股票。
该策略使用等权重投资组合,但也允许其他加权方案,例如价值加权或度加权组合。投资组合每周根据更新的COT信号重新平衡,利用交易商头寸趋势来预测商品相关股票的表现。
II. 策略合理性
DCOT报告显示,管理资金(MM)交易者在商品期货中的头寸为股票回报提供了有价值的预测信号。MM交易者以其投机策略、杠杆和市场洞察力而闻名,比生产商(PM)更能有效地预测股票回报的横截面,后者头寸缺乏预测能力。MM头寸反映了对未来商品价格的看法,这与生产商股票的价格相关。该策略从MM多头/空头头寸中产生了经济和统计上显著的阿尔法。这些结果在各种衡量标准、加权方案、时间滞后和商业周期中均保持稳健。此外,多变量回归证实,MM信号独立于市场敞口、规模或动量等传统预测因子,能够预测股票回报。
回测表现
波动率28.57%
夏普比率0.67
索提诺比率0.042
胜率52%
完整 Python 代码
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"))