
“该策略每月交易20种商品期货,根据曲线的现货溢价或期货溢价采取多头/空头头寸,等权重并进行再平衡,以利用期货价格差异。”
资产类别: 期货 | 地区: 全球 | 周期: 每月 | 市场: 大宗商品 | 关键词: 日历,价差
I. 策略概要
该策略的目标是20种商品期货,使用前12个月的合约进行信号生成和交易。每个月,投资者通过计算前五个合约之间的平均差值来评估期货曲线的形状。正结果表示现货溢价,而负结果表示期货溢价。
在现货溢价中,投资者在现货溢价最高的合约(差值最大)中建立多头头寸,在现货溢价最低的合约(差值最小)中建立空头头寸。在期货溢价中,该过程相反,在差值最大的合约中建立空头头寸,在差值最小的合约中建立多头头寸。
投资组合在20种商品中平均分配权重,并每月进行再平衡,系统地利用期货曲线结构的差异来寻找交易机会。
II. 策略合理性
学术研究将展期收益确定为商品回报的主要驱动因素。该策略通过做多现货溢价的商品和做空期货溢价的商品来捕捉这一点,同时通过曲线上的对冲合约来对冲头寸,以降低投资组合的波动性。
III. 来源论文
商品期货曲线的配对交易 [点击查看论文]
- Nikkanen
<摘要>
我在商品期货曲线上创建了一个配对交易,它捕捉商品期货的展期回报并最小化回报的标准差。最终结果是一个年化算术回报率为6.04%,年化标准差为2.01%的策略。交易成本和流动性也考虑在内。目标是创建和回测一个试图捕捉商品期货回报中展期回报成分的交易策略。为了降低商品回报非常高的现货价格波动性,通过配对交易引入了市场中性的系统套利。配对交易涉及采取相对于旨在捕捉展期回报的头寸的相反头寸,并尽可能小的负预期回报。实际上,捕捉展期回报成分意味着在现货溢价的期货曲线的最大美元差额中建立多头头寸。然后,配对交易成分是在同一曲线中建立空头头寸,但美元差额最小。如果商品期货曲线处于期货溢价状态,则程序反转。可以得出结论,这项研究的两个目标都实现了:捕捉商品期货的展期回报,并通过统计套利配对交易最小化波动性。旨在捕捉商品期货展期回报的交易年回报率为5.55%(占投资组合总回报的91.9%),而基准指数的年回报率为0.5%。旨在最小化这些回报标准差的配对交易将投资组合回报的年化标准差从6.37%降低到2.01%,使其几乎完全市场中性。

IV. 回测表现
| 年化回报 | 6.21% |
| 波动率 | 2.01% |
| β值 | 0.002 |
| 夏普比率 | 3.09 |
| 索提诺比率 | N/A |
| 最大回撤 | N/A |
| 胜率 | 51% |
V. 完整的 Python 代码
import numpy as np
class TradingCommodityCalendarSpreads(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
# 1st contract and the number of contracts to approximately 12 months away.
self.contracts = {
"CHRIS/CME_S" : 8, # Soybean Futures, Continuous Contract
"CHRIS/CME_W" : 6, # Wheat Futures, Continuous Contract
"CHRIS/CME_SM" : 9, # Soybean Meal Futures, Continuous Contract
"CHRIS/CME_BO" : 9, # Soybean Oil Futures, Continuous Contract
"CHRIS/CME_C" : 6, # Corn Futures, Continuous Contract
"CHRIS/CME_O" : 6, # Oats Futures, Continuous Contract
"CHRIS/CME_LC" : 7, # Live Cattle Futures, Continuous Contract
"CHRIS/CME_FC" : 7, # Feeder Cattle Futures, Continuous Contract
"CHRIS/CME_LN" : 9, # Lean Hog Futures, Continuous Contract
"CHRIS/CME_GC" : 9, # Gold Futures, Continuous Contract
"CHRIS/CME_SI" : 9, # Silver Futures, Continuous Contract
"CHRIS/CME_PL" : 7, # Platinum Futures, Continuous Contract
"CHRIS/CME_CL" : 12, # Crude Oil Futures, Continuous Contract
"CHRIS/CME_HG" : 13, # Copper Futures, Continuous Contract
"CHRIS/CME_LB" : 7, # Random Length Lumber Futures, Continuous Contract
"CHRIS/CME_NG" : 12, # Natural Gas (Henry Hub) Physical Futures, Continuous Contract
"CHRIS/CME_PA" : 7, # Palladium Futures, Continuous Contract
"CHRIS/CME_RR" : 7, # Rough Rice Futures, Continuous Contract
# "CHRIS/CME_CU" : 13, # Chicago Ethanol (Platts) Futures
"CHRIS/CME_DA" : 13, # Class III Milk Futures
"CHRIS/ICE_CC" : 6, # Cocoa Futures, Continuous Contract
"CHRIS/ICE_CT" : 6, # Cotton No. 2 Futures, Continuous Contract
"CHRIS/ICE_KC" : 6, # Coffee C Futures, Continuous Contract
"CHRIS/ICE_O" : 13, # Heating Oil Futures, Continuous Contract
"CHRIS/ICE_OJ" : 7, # Orange Juice Futures, Continuous Contract
"CHRIS/ICE_SB" : 5 # Sugar No. 11 Futures, Continuous Contract
}
self.rebalance_flag = True
for future, future_count in self.contracts.items():
for index in range(1, future_count + 1):
contract = future + str(index)
data = self.AddData(QuandlFutures, contract, Resolution.Daily)
data.SetFeeModel(CustomFeeModel(self))
data.SetLeverage(5)
self.Schedule.On(self.DateRules.MonthStart('CHRIS/CME_S1'), self.TimeRules.AfterMarketOpen('CHRIS/CME_S1'), self.Rebalance)
def Rebalance(self):
self.rebalance_flag = True
def OnData(self, data):
if not self.rebalance_flag:
return
self.rebalance_flag = False
long = []
short = []
for future, future_count in self.contracts.items():
# curve_shape = sum( np.diff( [ self.Securities[future + str(index)].Price for index in range(1, 6) if self.Securities.ContainsKey(future + str(index)) ] ) )
curve_shape = sum( np.diff( [ data[future + str(index)].Value for index in range(1, 6) if (future + str(index)) in data ] ) )
curve_shape /= future_count
if curve_shape != 0:
# diff = np.diff( [ self.Securities[future + str(index)].Price for index in range(1, future_count + 1) if self.Securities.ContainsKey(future + str(index)) ] )
diff = np.diff( [ data[future + str(index)].Value for index in range(1, future_count + 1) if (future + str(index)) in data ] )
abs_diff = [abs(x) for x in diff]
max_diff_index = abs_diff.index(max(abs_diff))
min_diff_index = abs_diff.index(min(abs_diff))
# Offset index by 2 to get right symbols to trade.
max_diff_index += 2
min_diff_index += 2
if curve_shape > 0:
# Backwardation.
long.append(future + str(max_diff_index))
short.append(future + str(min_diff_index))
else:
# Contango.
long.append(future + str(min_diff_index))
short.append(future + str(max_diff_index))
# Trade execution.
invested = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in long + short:
self.Liquidate(symbol)
weight = 1 / len(self.contracts)
for symbol in long:
if self.Securities[symbol].Price != 0:
self.SetHoldings(symbol, weight)
for symbol in short:
if self.Securities[symbol].Price != 0:
self.SetHoldings(symbol, -weight)
# Quandl free data
class QuandlFutures(PythonQuandl):
def __init__(self):
self.ValueColumnName = "settle"
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))