“每天计算 WTI/Brent 价差的 20 天移动平均。如果当前价差超过 SMA 20,则开启一个空头头寸,目标是缩小价差。相反,当价差低于 SMA 20 时,开启一个多头头寸,并相应地关闭交易。”

策略概述

每天计算 WTI/Brent 价差的 20 天移动平均。如果当前价差值高于 SMA 20,则在收盘时进入价差的空头头寸(打赌价差将减少到由 SMA 20 表示的公允价值)。当价差穿过公允价值时,交易在交易日收盘时关闭。如果当前价差值低于 SMA 20,则进入多头头寸,打赌价差将增加,并在价差穿过公允价值时在交易日收盘时关闭交易。

策略合理性

两种原油在化学成分、生产和运输属性上都有所不同。这些差异体现在两种期货合约之间的价格差。这种差距是均值回归的,因为大多数价格冲击只是暂时的,所以价格差会回到其长期经济均衡状态,因此可以基于这种均值回归创建交易策略。只需注意使用来源论文中的参数,因为它们是基于短期历史数据,因此可能容易受到数据挖掘偏差的影响。

论文来源

Trading Futures Spread: An Application of Correlation [点击浏览原文]

<摘要>

该研究探讨了期货价差交易的主题,即交易两个期货合约之间的价格差异。我们使用四种交易模型对三个油价差进行等权重投资组合交易:公平价值协整模型、广义自回归条件异方差模型、移动平均收敛/发散模型和神经网络回归模型(NNR)。本研究的动机是双重的。首先,价差市场的盈利能力已经被进一步测试,超出了传统的公平价值模型。其次,通过研究将阈值过滤器和相关性过滤器的输入结合起来的效果,扩展了相关性过滤器。结果表明,交易价差的最佳模型类型是 NNR 模型,在样本外年化百分比收益率为 10.76%,回撤为 -1.52%,在混合过滤器情况下的杠杆系数为 7.0964。此外,结果显示混合过滤器是一个可靠的发展,证明在选择时是最佳的样本外过滤器。

回测表现

年化收益率9.92%
波动率11.27%
Beta0.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"))

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading