
“该策略在时间序列动量方法中使用52种期货,在月末期间交易波动率加权头寸,与原始策略相比,实现了卓越的风险调整后回报。”
资产类别: 差价合约、期货 | 地区: 全球 | 周期:
每日 | 市场: 债券、商品、外汇、股票 | 关键词: 月末效应,期货动量
I. 策略概要
该策略使用52种流动性期货(货币、商品、股指、固定收益)复制一个指数,并应用时间序列动量方法。一个由波动率缩放的100天SMA生成多头/空头信号,投资组合采用波动率加权。投资者仅在为期3天的月末(ToM)期间持有该投资组合,捕捉超过50%的时间序列动量总回报。通过利用月末过渡期间可预测的回报模式,这种ToM动量策略实现了比全周期动量策略显著更高的风险调整后回报。
II. 策略合理性
动量策略在月末(ToM)期间的功能归因于买入压力。月末期间的大量资金流入促使管理者扩大现有头寸或重新平衡投资组合,从而产生暂时的价格上涨。系统性趋势跟踪/动量CTA基金放大了这种效应,尤其是在流动性较差的商品中,价格压力更为明显。该论文强调,组合动量和ToM效应导致非ToM期间的部分反转,这与暂时的价格压力一致。此外,这种异常现象不能仅用被动多头头寸的ToM效应来解释,强调了动量策略在这些期间的独特贡献。
III. 来源论文
MOM-TOM效应:检测CTA交易的市场影响 [点击查看论文]
- Otto van Hemert. IMC资产管理
<摘要>
受到CTA管理资产爆炸性增长的推动,再加上最近该行业许多经理人的业绩不佳,我们探讨了许多CTA采用的趋势跟踪交易风格是否变得拥挤。明确地,我们使用以下假设测试市场影响:在月末(TOM)附近,趋势跟踪(MOM)策略消化大量资金流入,导致经理人增加其现有头寸,从而暂时将价格推向对他们有利的方向。主要的实证检验是MOM策略在TOM日是否获得高于平均水平的回报,我们称之为MOM-TOM效应。我们发现Newedge趋势指数回报中存在非常强的MOM-TOM效应,自2000年以来90%的累计回报是在三个TOM日实现的。此外,我们设计的密切跟踪Newedge趋势指数的复制策略也显示出很强的MOM-TOM效应。


IV. 回测表现
| 年化回报 | 4.8% |
| 波动率 | 1.5% |
| β值 | -0.01 |
| 夏普比率 | 3.2 |
| 索提诺比率 | -0.476 |
| 最大回撤 | N/A |
| 胜率 | 51% |
V. 完整的 Python 代码
import numpy as np
from AlgorithmImports import *
from collections import deque
from pandas.tseries.offsets import BDay
from pandas.tseries.offsets import BMonthEnd
class TOMEffectFuturesMomentumStrategy(QCAlgorithm):
def Initialize(self):
self.SetStartDate(1991, 1, 1)
self.SetCash(100000)
self.symbols = [
"CME_S1", # Soybean Futures, Continuous Contract
"CME_W1", # Wheat 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_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_PA1", # Palladium Futures, Continuous Contract
"CME_RR1", # Rough Rice Futures, Continuous Contract
"ICE_CC1", # Cocoa Futures, Continuous Contract
"ICE_KC1", # Coffee C Futures, Continuous Contract
"ICE_OJ1", # Orange Juice Futures, Continuous Contract
"ICE_SB1", # Sugar No. 11 Futures, Continuous Contract
"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
"CME_AD1", # Australian Dollar Futures, Continuous Contract #1
"CME_BP1", # British Pound Futures, Continuous Contract #1
"CME_CD1", # Canadian Dollar Futures, Continuous Contract #1
"CME_EC1", # Euro FX Futures, Continuous Contract #1
"CME_JY1", # Japanese Yen Futures, Continuous Contract #1
"CME_SF1", # Swiss Franc Futures, Continuous Contract #1
"CME_NQ1", # E-mini NASDAQ 100 Futures, Continuous Contract #1
"CME_ES1", # E-mini S&P 500 Futures, Continuous Contract #1
"EUREX_FSMI1", # SMI Futures, Continuous Contract #1
"EUREX_FSTX1", # STOXX Europe 50 Index Futures, Continuous Contract #1
"LIFFE_FCE1", # CAC40 Index Futures, Continuous Contract #1
"LIFFE_Z1", # FTSE 100 Index Futures, Continuous Contract #1
"CME_TY1", # 10 Yr Note Futures, Continuous Contract #1 -5000
"CME_FV1", # 5 Yr Note Futures, Continuous Contract #1-8000
"CME_TU1", # 2 Yr Note Futures, Continuous Contract #1 -10000
"ASX_XT1", # 10 Year Commonwealth Treasury Bond Futures, Continuous Contract #1 # 'Settlement price' instead of 'settle' on quandl.
"ASX_YT1", # 3 Year Commonwealth Treasury Bond Futures, Continuous Contract #1 # 'Settlement price' instead of 'settle' on quandl.
"EUREX_FGBL1", # Euro-Bund (10Y) Futures, Continuous Contract #1
"EUREX_FGBM1", # Euro-Bobl Futures, Continuous Contract #1
"EUREX_FGBS1", # Euro-Schatz Futures, Continuous Contract #1
"SGX_JB1", # SGX 10-Year Mini Japanese Government Bond Futures
"LIFFE_R1" # Long Gilt Futures, Continuous Contract #1
"MX_CGB1", # Ten-Year Government of Canada Bond Futures, Continuous Contract #1 # 'Settlement price' instead of 'settle' on quandl.
]
ma_period = 100
vol_period = 60
self.SetWarmUp(vol_period)
self.data = {}
self.sma = {}
self.days = 0
for symbol in self.symbols:
data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily)
data.SetLeverage(5)
data.SetFeeModel(CustomFeeModel())
self.data[symbol] = deque(maxlen=vol_period)
self.sma[symbol] = self.SMA(symbol, ma_period, Resolution.Daily)
def OnData(self, data):
for symbol in self.symbols:
# data is still coming
if self.securities[symbol].get_last_data() and self.time.date() > QuantpediaFutures.get_last_update_date()[symbol]:
self.liquidate(symbol)
self.data[symbol].clear()
continue
if symbol in data and data[symbol]:
price = data[symbol].Value
self.data[symbol].append(price)
if self.IsWarmingUp: return
if self.Portfolio.Invested:
self.days += 1
if self.days == 3:
self.Liquidate()
self.days = 0
offset = BMonthEnd()
last_day = offset.rollforward(self.Time)
# day before EOM
if self.Time.date() == last_day.date():
# Volatility calculation
volatility = {}
for symbol in self.symbols:
if len(self.data[symbol]) == self.data[symbol].maxlen:
volatility[symbol] = self.Volatility(self.data[symbol])
if len(volatility) == 0: return
# MA sorting
long = [x[0] for x in volatility.items() if self.data[x[0]][-1] > self.sma[x[0]].Current.Value]
short = [x[0] for x in volatility.items() if self.data[x[0]][-1] < self.sma[x[0]].Current.Value]
# Volatility weighting
weight = {}
total_vol_long = sum([1/volatility[x] for x in long])
if total_vol_long != 0:
for symbol in long:
vol = volatility[symbol]
if vol != 0:
weight[symbol] = (1.0 / vol) / total_vol_long
else:
weight[symbol] = 0
total_vol_short = sum([1/volatility[x] for x in short])
if total_vol_short != 0:
for symbol in short:
vol = volatility[symbol]
if vol != 0:
weight[symbol] = (1.0 / vol) / total_vol_short
else:
weight[symbol] = 0
# Trade execution
for symbol in long:
if data.contains_key(symbol) and data[symbol]:
self.SetHoldings(symbol, weight[symbol])
for symbol in short:
if data.contains_key(symbol) and data[symbol]:
self.SetHoldings(symbol, -weight[symbol])
def Volatility(self, history):
prices = np.array(history)
returns = (prices[1:]-prices[:-1])/prices[:-1]
vol = np.std(returns) * np.sqrt(252)
return vol
# Quantpedia data
class QuantpediaFutures(PythonData):
_last_update_date:Dict[Symbol, datetime.date] = {}
@staticmethod
def get_last_update_date() -> Dict[Symbol, datetime.date]:
return QuantpediaFutures._last_update_date
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource("http://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
try:
if not line[0].isdigit(): return None
split = line.split(';')
data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1)
data['settle'] = float(split[1])
data.Value = float(split[1])
if config.Symbol.Value not in QuantpediaFutures._last_update_date:
QuantpediaFutures._last_update_date[config.Symbol.Value] = datetime(1,1,1).date()
if data.Time.date() > QuantpediaFutures._last_update_date[config.Symbol.Value]:
QuantpediaFutures._last_update_date[config.Symbol.Value] = data.Time.date()
except:
return None
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"))