
“该策略投资于来自54个国家的10年期债券,做多回报排名前五分之一的债券,做空回报排名后五分之一的债券,采用等权重,并根据过去一个月的表现每月重新平衡。”
资产类别: 期货、互换 | 地区: 全球 | 周期: 每月 | 市场: 债券 | 关键词: 动量
I. 策略概要
该投资策略涉及来自54个国家的10年期政府债券。资产根据其过去一个月的收益率分为五分位。该方法对收益率最高的五分位建立多头头寸,对收益率最低的五分位建立空头头寸。该策略采用等权重,每月重新平衡。
II. 策略合理性
研究表明,上个月的回报可以预测股票、债券、票据、商品、货币和 pooled 资产类别的未来回报。这种短期动量独立于传统的长期12个月动量,并且无法用市场贝塔、波动率、价值、偏度或季节性等常见回报预测因子来解释。它提供了超越这些因子对未来回报的增量洞察。股票、债券和票据的短期动量显示出微弱但显著的相关性,表明可能存在一个共同的潜在因子。尽管其原因尚不清楚,但该效应在各种稳健性检查下都成立,包括子周期分析、市场状况和替代实施方案。虽然动量策略在票据中表现良好,但由于融资成本,实施做多-做空策略是不可行的,这使得它更适合作为一种只做多的方法。
III. 来源论文
Short-Term Momentum (Almost) Everywhere [点击查看论文]
- 亚当·扎伦巴(Adam Zaremba)、安德烈亚斯·卡拉萨纳索普洛斯(Andreas Karathanasopoulos)、龙怀刚(Huaigang Long),蒙彼利埃商学院;波兹南经济与商业大学;开普敦大学(UCT),迪拜大学,浙江大学;浙江财经大学
<摘要>
在个股之外是否存在短期反转效应?为了回答这个问题,我们调查了一个包含两个多世纪以来五大主要资产类别(股票指数、政府债券、国库券、商品和货币)回报的综合数据集。与股票层面的证据相反,我们发现了一个惊人的短期动量模式:最近一个月的收益率正向预测未来表现。这种效应无法用已建立的收益率预测因子(包括标准动量)来解释,并且对许多因素都具有鲁棒性。短期动量在具有高特殊波动性的资产中以及在收益率离散度较高的时期最为显著。此外,该策略的回报在不同资产类别之间显示出部分共性。


IV. 回测表现
| 年化回报 | 6.04% |
| 波动率 | 9.69% |
| β值 | -0.037 |
| 夏普比率 | 0.62 |
| 索提诺比率 | -0.305 |
| 最大回撤 | N/A |
| 胜率 | 49% |
V. 完整的 Python 代码
from AlgorithmImports import *
class OneMonthMomentum(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.symbols = [
"EUREX_FGBL1", # Euro-Bund (10Y) Futures, Continuous Contract #1 (Germany)
"CME_TY1", # 10 Yr Note Futures, Continuous Contract #1 (USA)
"MX_CGB1", # Ten-Year Government of Canada Bond Futures, Continuous Contract #1 (Canada)
"ASX_XT1", # 10 Year Commonwealth Treasury Bond Futures, Continuous Contract #1 (Australia)
"SGX_JB1", # SGX 10-Year Mini Japanese Government Bond Futures, Continuous Contract #1 (Japan)
"LIFFE_R1", # Long Gilt Futures, Continuous Contract #1 (U.K.)
"EUREX_FBTP1" # Long-Term Euro-BTP Futures, Continuous Contract #1 (Italy)
]
self.period = 21
self.quantile = 5
self.SetWarmUp(self.period)
# Daily ROC data.
self.data = {}
for symbol in self.symbols:
data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily)
data.SetFeeModel(CustomFeeModel())
data.SetLeverage(5)
self.data[symbol] = self.ROC(symbol, self.period, Resolution.Daily)
self.rebalance_flag: bool = False
self.Schedule.On(self.DateRules.MonthStart(self.symbols[0]), self.TimeRules.At(0, 0), self.Rebalance)
self.settings.daily_precise_end_time = False
self.settings.minimum_order_margin_portfolio_percentage = 0.
def on_data(self, slice: Slice) -> None:
if not self.rebalance_flag:
return
self.rebalance_flag = False
# Return sorting.
sorted_by_return = sorted([x for x in self.data.items() if x[1].IsReady and self.Securities[x[0]].GetLastData() and self.Time.date() < QuantpediaFutures.get_last_update_date()[x[0]] and slice.contains_key(x[0]) and slice[x[0]]], key = lambda x: x[1].Current.Value, reverse = True)
long = []
short = []
if len(sorted_by_return) >= self.quantile:
quintile = int(len(sorted_by_return) / self.quantile)
long = [x[0] for x in sorted_by_return[:quintile]]
short = [x[0] for x in sorted_by_return[-quintile:]]
# Trade execution.
targets: List[PortfolioTarget] = []
for i, portfolio in enumerate([long, short]):
for symbol in portfolio:
if slice.contains_key(symbol) and slice[symbol]:
targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
self.SetHoldings(targets, True)
def Rebalance(self):
self.rebalance_flag = True
# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
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("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])
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
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))