
“该策略投资于27种商品,每月根据价差压力对其进行排序。做多压力最低的三种商品,做空压力最高的三种商品,每月重新平衡。”
资产类别: 差价合约、期货 | 地区: 全球 | 周期: 每月 | 市场: 大宗商品 | 关键词: 投机者、压力
I. 策略概要
该策略投资于能源、谷物、肉类、金属和软商品五个类别的27种商品。每种商品的价差压力计算为投机者持有的总价差头寸除以未平仓合约。商品每月根据12个月的平均价差压力进行排序。该策略做多价差压力最低的三种商品,做空价差压力最高的三种商品。投资组合等权重并每月重新平衡。这种方法旨在通过针对低价差压力和高价差压力商品来利用商品市场中的价差头寸。
II. 策略合理性
投机者利用商品内部价差头寸来对冲不同到期日商品期货价格变化带来的风险。价差头寸的程度,即价差压力,反映了市场对未来价格走势的预期,并传达了有关期货期限结构和市场流动性的信息。自2005年以来,价差压力相对于未平仓合约的增加与套利、动量和基差动量投资组合表现疲软同时发生。这种转变归因于套利行为的变化,套利者增加资本配置以利用套利和动量等市场异常,从而降低了其盈利能力。Boons和Prado(2019)也支持这一发现,指出较高的价差压力会降低商品基差动量投资组合的回报。
III. 来源论文
Speculator Spreading Pressure and the Commodity Futures Risk Premium [点击查看论文]
- 龚宇晶(Yujing Gong)、阿里·E·戈兹卢克鲁布(Arie E. Gozluklu)、金基赫(Gi H. Kim)
<摘要>
本文研究了投机者交易活动对商品期货风险溢价的影响。特别是,我们关注投机者的价差头寸,并研究价差压力对商品期货回报横截面的资产定价影响。我们记录了即使在控制了基差动量等期货回报的已知决定因素之后,价差压力仍然负向预测期货超额回报。此外,商品市场金融化后,价差压力因子模拟投资组合每年带来21.55%的显著风险溢价。我们的单因子模型提供了比现有文献中的双因子或三因子模型更好的横截面拟合。我们将这些结果解释为价差压力反映了投机者对期货期限结构斜率和曲率变化的预期,并且我们的价差压力因子与实际经济不确定性的创新相关。


IV. 回测表现
| 年化回报 | 21.19% |
| 波动率 | 24.42% |
| β值 | 0.08 |
| 夏普比率 | 0.87 |
| 索提诺比率 | -0.21 |
| 最大回撤 | N/A |
| 胜率 | 51% |
V. 完整的 Python 代码
import numpy as np
from AlgorithmImports import *
import data_tools
class SpeculatorSpreadingPressureandtheCommodityFuturesRiskPremium(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.symbols = [
"CME_S1", # Soybean Futures, Continuous Contract
"CME_W1", # Wheat Futures, Continuous Contract
"CME_SM1", # Soybean Meal Futures, Continuous Contract
"CME_BO1", # Soybean Oil Futures, Continuous Contract
"CME_C1", # Corn Futures, Continuous Contract
"CME_O1", # Oats Futures, Continuous Contract
"CME_LC1", # Live Cattle Futures, Continuous Contract
"CME_FC1", # Feeder Cattle Futures, Continuous Contract
"CME_LN1", # Lean Hog Futures, Continuous Contract
"CME_GC1", # Gold Futures, Continuous Contract
"CME_SI1", # Silver Futures, Continuous Contract
"CME_PL1", # Platinum Futures, Continuous Contract
"CME_CL1", # Crude Oil Futures, Continuous Contract
"CME_HG1", # Copper Futures, Continuous Contract
"CME_LB1", # Random Length Lumber Futures, Continuous Contract
# "CME_NG1", # Natural Gas (Henry Hub) Physical Futures, Continuous Contract
"CME_PA1", # Palladium Futures, Continuous Contract
"CME_RR1", # Rough Rice Futures, Continuous Contract
"CME_DA1", # Class III Milk Futures
"ICE_RS1", # Canola Futures, Continuous Contract
"ICE_GO1", # Gas Oil Futures, Continuous Contract
"CME_RB2", # Gasoline Futures, Continuous Contract
"CME_KW2", # Wheat Kansas, Continuous Contract
"ICE_WT1", # WTI Crude Futures, Continuous Contract
"ICE_CC1", # Cocoa Futures, Continuous Contract
"ICE_CT1", # Cotton No. 2 Futures, Continuous Contract
"ICE_KC1", # Coffee C Futures, Continuous Contract
"ICE_O1", # Heating Oil Futures, Continuous Contract
"ICE_OJ1", # Orange Juice Futures, Continuous Contract
"ICE_SB1" # Sugar No. 11 Futures, Continuous Contract
]
self.period = 12 * 4
self.traded_count = 3
# Spreading pressure weekly data.
self.spreading_pressure = {}
for symbol in self.symbols:
# Add quantpedia back-adjusted data.
data = self.AddData(data_tools.QuantpediaFutures, symbol, Resolution.Daily)
data.SetFeeModel(data_tools.CustomFeeModel())
data.SetLeverage(5)
# COT data import.
cot_symbol = 'Q' + symbol.split('_')[1][:-1]
self.AddData(data_tools.CommitmentsOfTraders, cot_symbol, Resolution.Daily)
self.spreading_pressure[symbol] = RollingWindow[float](self.period)
self.recent_month = -1
def OnData(self, data):
for symbol in self.symbols:
cot_symbol = 'Q' + symbol.split('_')[1][:-1]
# Check if custom data is still coming.
if any([self.securities[x].get_last_data() and self.time.date() > data_tools.LastDateHandler.get_last_update_date()[x] for x in [symbol, cot_symbol]]):
self.liquidate(symbol)
self.spreading_pressure[symbol].reset()
continue
if cot_symbol in data and data[cot_symbol]:
cot_data = self.Securities[cot_symbol].GetLastData()
if cot_data:
speculator_long_count = cot_data.GetProperty("LARGE_SPECULATOR_LONG")
speculator_short_count = cot_data.GetProperty("LARGE_SPECULATOR_SHORT")
hedger_long_count = cot_data.GetProperty("COMMERCIAL_HEDGER_LONG")
hedger_short_count = cot_data.GetProperty("COMMERCIAL_HEDGER_SHORT")
trader_long_count = cot_data.GetProperty("SMALL_TRADER_LONG")
trader_short_count = cot_data.GetProperty("SMALL_TRADER_SHORT")
spread = min(speculator_long_count, speculator_short_count)
open_interest = speculator_long_count + speculator_short_count + hedger_long_count + hedger_short_count + trader_long_count + trader_short_count
if spread != 0 and open_interest != 0:
self.spreading_pressure[symbol].Add(spread / open_interest)
if self.recent_month == self.Time.month:
return
self.recent_month = self.Time.month
sorted_by_avg_spreading_pressure = sorted([x for x in self.spreading_pressure.items() if x[1].IsReady], key = lambda y: np.mean([x for x in y[1]]), reverse = True)
long = []
short = []
if len(sorted_by_avg_spreading_pressure) >= self.traded_count*2:
long = [x[0] for x in sorted_by_avg_spreading_pressure[-self.traded_count:]]
short = [x[0] for x in sorted_by_avg_spreading_pressure[:self.traded_count]]
# Trade 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)