Quant Buffet放轻松,别过度思虑

期货动量策略中的月末效应

登录后收藏

学术论文

The MOM-TOM Effect: Detecting the Market Impact of CTA Trading

作者MOM-TOM效应:检测CTA交易的市场影响 [点击查看论文]

机构
  • NLAsser Institute
  • ?Man AHL

策略概要

该策略使用52种流动性期货(货币、商品、股指、固定收益)复制一个指数,并应用时间序列动量方法。一个由波动率缩放的100天SMA生成多头/空头信号,投资组合采用波动率加权。投资者仅在为期3天的月末(ToM)期间持有该投资组合,捕捉超过50%的时间序列动量总回报。通过利用月末过渡期间可预测的回报模式,这种ToM动量策略实现了比全周期动量策略显著更高的风险调整后回报。

II. 策略合理性

动量策略在月末(ToM)期间的功能归因于买入压力。月末期间的大量资金流入促使管理者扩大现有头寸或重新平衡投资组合,从而产生暂时的价格上涨。系统性趋势跟踪/动量CTA基金放大了这种效应,尤其是在流动性较差的商品中,价格压力更为明显。该论文强调,组合动量和ToM效应导致非ToM期间的部分反转,这与暂时的价格压力一致。此外,这种异常现象不能仅用被动多头头寸的ToM效应来解释,强调了动量策略在这些期间的独特贡献。

回测表现

波动率1.5%
夏普比率3.2
索提诺比率-0.476
胜率51%

完整 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"))