
“该策略分析55种期货,使用12个月的回报概率和0.4的阈值来发出头寸信号,按波动率缩放回报,并每月重新平衡以优化绩效。”
资产类别: 差价合约、期货 | 地区: 全球 | 周期: 每月 | 市场: 债券、大宗商品、外汇、股票 | 关键词: 动量
I. 策略概要
投资策略涉及55种流动性交易所交易的期货。分析12个月的回溯期内的月度回报。如果正回报的概率达到或超过0.4的阈值,则触发多头头寸的“买入”信号;否则,进入空头头寸。使用年化事前波动率缩放回报,年波动率的临界值为40%。投资组合每月重新平衡,以调整变化。这种系统性方法使用基于概率的信号和波动率管理来优化投资组合的回报。
II. 策略合理性
RSM(回报符号动量)的合理性源于短期反应不足和延迟过度反应。回报符号可预测性基于以下理论原则:当回报的条件均值非零时,符号依赖性存在。由于大多数金融资产在长期内表现出正回报,因此检测符号依赖性是可行的。这强调了回报行为和条件均值之间的关系,支持了回报符号中的可预测模式与更广泛的市场趋势相关联的观点。
III. 来源论文
Returns Signal Momentum [点击查看论文]
- Papailias、Liu、Thomakos,伦敦国王学院 – 国王商学院;Knot Analytics有限公司,贝尔法斯特女王大学 – 女王管理学院,雅典大学 – 工商管理系。
<摘要>
引入了一种基于过去回报符号的新型动量。这种动量主要由符号依赖性驱动,符号依赖性与平均回报呈正相关,与回报波动率呈负相关。使用商品和金融期货的投资范围进行的实证应用为这种动量的存在提供了支持证据。基于回报信号动量的投资策略相对于时间序列动量和其他基准策略,产生了更高的回报和夏普比率,以及更低的下跌幅度。总体而言,回报信号动量可以作为投机和对冲的有效策略,使投资者受益。


IV. 回测表现
| 年化回报 | 11.9% |
| 波动率 | 12.3% |
| β值 | -0.084 |
| 夏普比率 | 0.97 |
| 索提诺比率 | -0.215 |
| 最大回撤 | -19.5% |
| 胜率 | 52% |
V. 完整的 Python 代码
import numpy as np
from AlgorithmImports import *
class ReturnsSignalMomentum(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2005, 1, 1)
self.SetCash(100000)
self.symbols = ["CME_S1", # Soybean Futures, Continuous Contract
"CME_W1", # Wheat Futures, Continuous Contract
"CME_SM1", # Soybean Meal 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_LN1", # Lean Hog 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_LB1", # Random Length Lumber Futures, Continuous Contract
# "CME_NG1", # Natural Gas (Henry Hub) Physical Futures, Continuous Contract
"CME_PA1", # Palladium Futures, Continuous Contract
"CME_RR1", # Rough Rice 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
"ICE_CC1", # Cocoa Futures, Continuous Contract
"ICE_CT1", # Cotton No. 2 Futures, Continuous Contract
"ICE_KC1", # Coffee C Futures, Continuous Contract
"ICE_O1", # Heating Oil Futures, Continuous Contract
"ICE_OJ1", # Orange Juice Futures, Continuous Contract
"ICE_SB1", # Sugar No. 11 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_MP1", # Mexican Peso Futures, Continuous Contract #1
#"CME_NE1",# New Zealand Dollar Futures, Continuous Contract #1 # Short history ~2007
"CME_SF1", # Swiss Franc Futures, Continuous Contract #1
"ICE_DX1", # US Dollar Index Futures, Continuous Contract #1
"CME_NQ1", # E-mini NASDAQ 100 Futures, Continuous Contract #1
"EUREX_FDAX1", # DAX 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
"SGX_NK1", # SGX Nikkei 225 Index Futures, Continuous Contract #1
"CME_TY1", # 10 Yr Note Futures, Continuous Contract #1
"CME_FV1", # 5 Yr Note Futures, Continuous Contract #1
"CME_TU1", # 2 Yr Note Futures, Continuous Contract #1
#"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_FBTP1", # Long-Term Euro-BTP Futures, Continuous Contract #1 # Short history
"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.
]
self.data = {}
self.return_history = {}
# lookup_period = 60
lookup_period = 21
self.return_months_count = 12
self.SetWarmUp(lookup_period)
for symbol in self.symbols:
data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily)
data.SetFeeModel(CustomFeeModel())
data.SetLeverage(5)
self.data[symbol] = RollingWindow[float](lookup_period)
self.return_history[symbol] = RollingWindow[float](self.return_months_count)
self.Schedule.On(self.DateRules.MonthStart(self.symbols[0]), self.TimeRules.At(0, 0), self.Rebalance)
def OnData(self, data):
for symbol in self.symbols:
if symbol in data and data[symbol]:
price = data[symbol].Value
if price != 0:
self.data[symbol].Add(price)
def Rebalance(self):
if self.IsWarmingUp: return
volatility = {}
for symbol in self.symbols:
if self.data[symbol].IsReady:
if self.Securities[symbol].GetLastData() and self.Time.date() < QuantpediaFutures.get_last_update_date()[symbol]:
prices = [x for x in self.data[symbol]]
volatility[symbol] = self.Volatility(prices)
prices = prices[:21] # Last month of daily prices
self.return_history[symbol].Add(self.Return(prices))
if len(volatility) == 0:
self.Liquidate()
return
long = []
short = []
threshold = int(0.4*self.return_months_count)
# Create long and short portfolio
for symbol, roll_window in self.return_history.items():
# Check if monthly returns are ready
if not roll_window.IsReady:
continue
if symbol not in volatility:
continue
# Select only positive returns
temp = [x for x in roll_window if x > 0]
if len(temp) >= threshold:
long.append(symbol)
else:
short.append(symbol)
if len(long + short) == 0:
self.Liquidate()
return
# Volatility weighting
total_vol_long = sum([1 / volatility[x] for x in long if volatility[x] != 0])
total_vol_short = sum([1 / volatility[x] for x in short if volatility[x] != 0])
weight = {}
if total_vol_long != 0:
# Calculate long stocks weights
for symbol in long:
vol = volatility[symbol]
if vol != 0:
weight[symbol] = (1 / vol) / total_vol_long
if total_vol_short != 0:
# Calculate short stocks weights
for symbol in short:
vol = volatility[symbol]
if vol != 0:
weight[symbol] = -(1 / vol) / total_vol_short
# Trade execution
invested = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in weight:
self.Liquidate(symbol)
for symbol, w in weight.items():
self.SetHoldings(symbol, w)
def Return(self, history):
return (history[0] - history[-1]) / history[-1]
def Volatility(self, history):
values = np.array(history)
returns = (values[:-1] - values[1:]) / values[1:]
return np.std(returns)
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))
# 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])
except:
return None
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()
return data