“每天计算 WTI/Brent 价差的 20 天移动平均。如果当前价差超过 SMA 20,则开启一个空头头寸,目标是缩小价差。相反,当价差低于 SMA 20 时,开启一个多头头寸,并相应地关闭交易。”
资产类别:期货 | 地区:全球 | 频率:每日 | 市场:大宗商品 | 关键词:WTI,BRENT
策略概述
每天计算 WTI/Brent 价差的 20 天移动平均。如果当前价差值高于 SMA 20,则在收盘时进入价差的空头头寸(打赌价差将减少到由 SMA 20 表示的公允价值)。当价差穿过公允价值时,交易在交易日收盘时关闭。如果当前价差值低于 SMA 20,则进入多头头寸,打赌价差将增加,并在价差穿过公允价值时在交易日收盘时关闭交易。
策略合理性
两种原油在化学成分、生产和运输属性上都有所不同。这些差异体现在两种期货合约之间的价格差。这种差距是均值回归的,因为大多数价格冲击只是暂时的,所以价格差会回到其长期经济均衡状态,因此可以基于这种均值回归创建交易策略。只需注意使用来源论文中的参数,因为它们是基于短期历史数据,因此可能容易受到数据挖掘偏差的影响。
论文来源
Trading Futures Spread: An Application of Correlation [点击浏览原文]
- Christian L. Dunis,英国利物浦约翰摩尔大学利物浦商学院
- Jason Laws,英国利物浦约翰摩尔大学利物浦商学院
- Ben Evans,WINGAS天然气交易公司
<摘要>
该研究探讨了期货价差交易的主题,即交易两个期货合约之间的价格差异。我们使用四种交易模型对三个油价差进行等权重投资组合交易:公平价值协整模型、广义自回归条件异方差模型、移动平均收敛/发散模型和神经网络回归模型(NNR)。本研究的动机是双重的。首先,价差市场的盈利能力已经被进一步测试,超出了传统的公平价值模型。其次,通过研究将阈值过滤器和相关性过滤器的输入结合起来的效果,扩展了相关性过滤器。结果表明,交易价差的最佳模型类型是 NNR 模型,在样本外年化百分比收益率为 10.76%,回撤为 -1.52%,在混合过滤器情况下的杠杆系数为 7.0964。此外,结果显示混合过滤器是一个可靠的发展,证明在选择时是最佳的样本外过滤器。


回测表现
| 年化收益率 | 9.92% |
| 波动率 | 11.27% |
| Beta | 0.023 |
| 夏普比率 | -0.377 |
| 索提诺比率 | -0.363 |
| 最大回撤 | 70.8% |
| 胜率 | 48% |
Python代码及解释
完整python代码
from AlgoLib import *
class WTIBRENTSpread(XXX):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.symbols = [
"ICE_WT1", # WTI Crude Futures, Continuous Contract
"ICE_B1" # Brent Crude Oil Futures, Continuous Contract
]
self.spread = RollingWindow[float](20)
for symbol in self.symbols:
data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily)
data.SetLeverage(5)
data.SetFeeModel(CustomFeeModel())
def OnData(self, data):
symbol1 = self.Symbol(self.symbols[0])
symbol2 = self.Symbol(self.symbols[1])
if symbol1 in data.Keys and symbol2 in data.Keys and data[symbol1] and data[symbol2]:
price1 = data[symbol1].Price
price2 = data[symbol2].Price
if price1 != 0 and price2 != 0:
spread = price1 - price2
self.spread.Add(spread)
# MA calculation.
if self.spread.IsReady:
if (self.Time.date() - self.Securities[symbol1].GetLastData().Time.date()).days < 5 and (self.Time.date() - self.Securities[symbol2].GetLastData().Time.date()).days < 5:
spreads = [x for x in self.spread]
spread_ma20 = sum(spreads) / len(spreads)
current_spread = spreads[0]
if current_spread > spread_ma20:
self.SetHoldings(symbol1, -1)
self.SetHoldings(symbol2, 1)
elif current_spread < spread_ma20:
self.SetHoldings(symbol1, 1)
self.SetHoldings(symbol2, -1)
else:
self.Liquidate()
# 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"))
