
“该策略使用季节性回报模式交易10年期以上债券,做多高回报月份,做空低回报月份,每月重新平衡,建议使用期货以提高流动性和成本效益。”
资产类别: 期货 | 地区: 全球 | 周期: 每月 | 市场: 债券 | 关键词: 季节性、国际政府
I. 策略概要
该策略的目标是来自22个发达市场和新兴市场的10年期以上债券,包括美国、英国、中国和德国。季节性回报预测变量(SAME)计算为过去20年同一日历月份的平均本地货币回报。每月按SAME对债券进行排序,并使用20%的截止值形成等权重零投资组合。做多SAME最高的债券,做空SAME最低的债券。投资组合每月重新平衡。为了实际交易,建议使用这些债券的期货,以提高流动性并降低交易成本。
II. 策略合理性
该论文将债券回报的季节性确定为与特定风险因素、宏观经济风险或与股票等其他资产类别的相关性无关。相反,它与行为解释相符,表明投资者情绪的周期性波动驱动了这种异常现象,尤其是在情绪高涨和非理性时期。该策略在套利限制较高的市场中尤其有利可图,但面临两个挑战:高换手率导致显著的交易成本,以及持有期超过一个月时表现不佳。使用期货可以缓解这些问题,提高流动性并降低成本,使基于季节性的交易策略更实用且有利可图。
III. 来源论文
Cross-Sectional Seasonalities in International Government Bond Returns [点击查看论文]
- 亚当·扎伦巴(Adam Zaremba)。蒙彼利埃商学院;波兹南经济与商业大学;开普敦大学(UCT)
<摘要>
我们是第一个记录国际政府债券横截面回报季节性效应的研究者。我们使用各种测试,检验了1980年至2018年间22个国家的固定收益证券。过去同一日历月份回报高的债券在未来继续跑赢大盘,而回报低的债券则继续跑输大盘。这种效应对于许多因素都是稳健的,包括控制债券回报的既定预测因子。我们的结果支持这种异常现象的行为解释,表明其在投资者情绪高涨时期和套利限制较强的细分市场中盈利能力最高。尽管如此,由于交易成本高和所需的短持有期,债券季节性的投资应用可能具有挑战性。


IV. 回测表现
| 年化回报 | 5.41% |
| 波动率 | 11.07% |
| β值 | 0.015 |
| 夏普比率 | 0.49 |
| 索提诺比率 | -0.573 |
| 最大回撤 | N/A |
| 胜率 | 55% |
V. 完整的 Python 代码
from AlgorithmImports import *
import data_tools
from collections import deque
class SeasonalitiesBondReturns(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.symbols = {
"ASX_XT1", # 10 Year Commonwealth Treasury Bond Futures, Continuous Contract #1 (Australia)
"MX_CGB1", # Ten-Year Government of Canada Bond Futures, Continuous Contract #1 (Canada)
"EUREX_FOAT1", # Euro-OAT Futures, Continuous Contract #1 (France)
"EUREX_FGBL1", # Euro-Bund (10Y) Futures, Continuous Contract #1 (Germany)
"LIFFE_R1", # Long Gilt Futures, Continuous Contract #1 (U.K.)
"EUREX_FBTP1", # Long-Term Euro-BTP Futures, Continuous Contract #1 (Italy)
"CME_TY1", # 10 Yr Note Futures, Continuous Contract #1 (USA)
"SGX_JB1" # SGX 10-Year Mini Japanese Government Bond Futures, Continuous Contract #1 (Japan)
}
# daily price data
self.data = {}
# monthly returns
self.monthly_return = {}
self.daily_period = 21
self.monthly_period = 20 * 12
self.traded_count = 1
for symbol in self.symbols:
# Bond future data.
data = self.AddData(data_tools.QuantpediaFutures, symbol, Resolution.Daily)
data.SetFeeModel(data_tools.CustomFeeModel())
data.SetLeverage(5)
self.data[symbol] = RollingWindow[float](self.daily_period)
self.monthly_return[symbol] = deque(maxlen=self.monthly_period)
self.settings.minimum_order_margin_portfolio_percentage = 0.
self.settings.daily_precise_end_time = False
self.rebalance_flag: bool = False
self.Schedule.On(self.DateRules.MonthEnd('ASX_XT1'), self.TimeRules.At(0, 0), self.Rebalance)
def OnData(self, data):
# store monthly future returns
for symbol in self.symbols:
if symbol in data and data[symbol]:
price = data[symbol].Value
self.data[symbol].Add(price)
if not self.rebalance_flag:
return
self.rebalance_flag = False
curr_month = self.Time.month
SAME = {}
# store monthly returns
for symbol in self.symbols:
if self.Securities[symbol].GetLastData() and self.Time.date() < data_tools.QuantpediaFutures.get_last_update_date()[symbol]:
if self.data[symbol].IsReady:
monthly_ret = self.data[symbol][0] / self.data[symbol][self.daily_period - 1] - 1
self.monthly_return[symbol].append((monthly_ret, curr_month))
# monthly returns are ready
if len(self.monthly_return[symbol]) >= self.monthly_period / 2:
next_month = curr_month+1 if curr_month < 12 else 1
same_month_returns = [x[0] for x in self.monthly_return[symbol] if x[1] == next_month]
SAME[symbol] = np.mean(same_month_returns)
else:
self.liquidate(symbol)
continue
long = []
short = []
if len(SAME) >= self.traded_count * 2:
# decile = int(len(SAME) / self.quantile)
# count = decile
# sorting by SAME
sorted_by_SAME = sorted(SAME.items(), key = lambda x: x[1], reverse = True)
long = [x[0] for x in sorted_by_SAME[:self.traded_count]]
short = [x[0] for x in sorted_by_SAME[-self.traded_count:]]
# order execution
targets: List[PortfolioTarget] = []
for i, portfolio in enumerate([long, short]):
for symbol in portfolio:
if symbol in data and data[symbol]:
targets.append(PortfolioTarget(symbol, ((-1) ** i) / self.traded_count))
self.SetHoldings(targets, True)
def Rebalance(self):
self.rebalance_flag = True