“该策略使用季节性回报模式交易10年期以上债券,做多高回报月份,做空低回报月份,每月重新平衡,建议使用期货以提高流动性和成本效益。”

I. 策略概要

该策略的目标是来自22个发达市场和新兴市场的10年期以上债券,包括美国、英国、中国和德国。季节性回报预测变量(SAME)计算为过去20年同一日历月份的平均本地货币回报。每月按SAME对债券进行排序,并使用20%的截止值形成等权重零投资组合。做多SAME最高的债券,做空SAME最低的债券。投资组合每月重新平衡。为了实际交易,建议使用这些债券的期货,以提高流动性并降低交易成本。

II. 策略合理性

该论文将债券回报的季节性确定为与特定风险因素、宏观经济风险或与股票等其他资产类别的相关性无关。相反,它与行为解释相符,表明投资者情绪的周期性波动驱动了这种异常现象,尤其是在情绪高涨和非理性时期。该策略在套利限制较高的市场中尤其有利可图,但面临两个挑战:高换手率导致显著的交易成本,以及持有期超过一个月时表现不佳。使用期货可以缓解这些问题,提高流动性并降低成本,使基于季节性的交易策略更实用且有利可图。

III. 来源论文

Cross-Sectional Seasonalities in International Government Bond Returns [点击查看论文]

<摘要>

我们是第一个记录国际政府债券横截面回报季节性效应的研究者。我们使用各种测试,检验了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

发表评论

了解 Quant Buffet 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读