“该策略使用过去的股票和债券回报来指导20个工业国家的股票或债券投资。头寸持有一个月,目标波动率为10%,国家份额等权重。”

I. 策略概要

投资范围包括20个主要工业国家的债券和股票指数。该策略依赖于过去的回报作为预测指标。如果过去12个月股票回报为负,债券回报为正,投资者做多债券。如果股票和债券回报均为正,投资者做多股票。在其他情况下,资本存入以美元计价的保证金账户,赚取美国无风险利率。头寸持有一个月,目标波动率为10%,最终投资组合中各国份额等权重。

II. 策略合理性

跨资产时间序列动量效应受到债券和股票市场中缓慢移动的资本的影响,这些资本受到诸如注意力不集中、决策延迟和资本市场摩擦等因素的驱动。正的债券回报通过降低借贷成本和增加贷款抵押品来帮助受约束的投资者,这反过来又刺激了股票需求和回报。当利率下降时,投资者可以利用更多的杠杆投资股票。股票正回报与未来贷款活动相关,推高债券收益率,导致债券负回报。相反,股票市场回报与无风险利率的变化相关,这在股票市场表现良好后对债券回报产生负面影响。

III. 来源论文

Cross-Asset Signals and Time Series Momentum [点击查看论文]

<摘要>

我们记录了债券和股票市场中的一种新现象,我们称之为跨资产时间序列动量。使用来自20个国家的数据,我们表明过去的债券市场回报是未来股票市场回报的正预测指标,而过去的股票市场回报是未来债券市场回报的负预测指标。我们利用这种可预测性构建了一个多元化的跨资产时间序列动量投资组合,其夏普比率比标准时间序列动量投资组合高45%。我们提供的证据表明,时间序列动量和跨资产时间序列动量是由债券和股票市场中缓慢移动的资本驱动的。

IV. 回测表现

年化回报6.5%
波动率10%
β值0.093
夏普比率0.65
索提诺比率0.077
最大回撤N/A
胜率59%

V. 完整的 Python 代码

import numpy as np
from AlgorithmImports import *
class GlobalCrossAssetTimeSeriesMomentum(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        
        self.symbols = {
            "ASX_YAP1" : "ASX_XT1",        # 10 Year Commonwealth Treasury Bond Futures, Continuous Contract #1 (Australia)
            "LIFFE_FCE1" : "MX_CGB1",       # Ten-Year Government of Canada Bond Futures, Continuous Contract #1 (Canada)
            "EUREX_FSTX1" : "EUREX_FGBL1",  # Euro-Bund (10Y) Futures, Continuous Contract #1 (Germany)
            "SGX_NK1" : "SGX_JB1",          # SGX 10-Year Mini Japanese Government Bond Futures, Continuous Contract #1 (Japan)
            "LIFFE_Z1" : "LIFFE_R1",        # Long Gilt Futures, Continuous Contract #1 (U.K.)
            "CME_ES1" : "CME_TY1"           # 10 Yr Note Futures, Continuous Contract #1 (USA)
        }
        self.data = {}
        self.period = 12*21
        self.SetWarmUp(self.period)
        self.leverage_cap = 5
        
        for eq in self.symbols:
            bond = self.symbols[eq]
            
            data = self.AddData(QuantpediaFutures, eq, Resolution.Daily)
            self.data[eq] = RollingWindow[float](self.period)
            data.SetFeeModel(CustomFeeModel())
            data.SetLeverage(self.leverage_cap)
            
            data = self.AddData(QuantpediaFutures, bond, Resolution.Daily)
            self.data[bond] = RollingWindow[float](self.period)
            data.SetFeeModel(CustomFeeModel())
            data.SetLeverage(self.leverage_cap)
            
        first_key = [x for x in self.symbols.keys()][0]
        self.rebalance_flag: bool = False
        self.Schedule.On(self.DateRules.MonthStart(first_key), self.TimeRules.At(0, 0), self.Rebalance)
    def OnData(self, data):
        for eq in self.symbols:
            bond = self.symbols[eq]
            
            if eq in data and bond in data:
                if data[eq] and data[bond]:
                    eq_price = data[eq].Value
                    bond_price = data[bond].Value
                    if eq_price != 0 and bond_price != 0:
                        self.data[eq].Add(eq_price)
                        self.data[bond].Add(bond_price)
        if not self.rebalance_flag:
            return
        self.rebalance_flag = False
        volatility = {}
        for eq in self.symbols:
            bond = self.symbols[eq]
            
            if all([self.data[x].IsReady and self.Securities[x].GetLastData() and self.Time.date() < QuantpediaFutures.get_last_update_date()[x] for x in [eq, bond]]):
                eq_prices = np.array([x for x in self.data[eq]])
                bond_prices = np.array([x for x in self.data[bond]])
                
                eq_return = eq_prices[0] / eq_prices[-1] - 1
                bond_return = bond_prices[0] / bond_prices[-1] - 1
                
                if eq_return < 0 and bond_return > 0:
                    bond_returns = bond_prices[:-1] / bond_prices[1:] - 1
                    volatility[bond] = np.std(bond_returns) * np.sqrt(252)
                elif eq_return > 0 and bond_return > 0:
                    eq_returns = eq_prices[:-1] / eq_prices[1:] - 1
                    volatility[eq] = np.std(eq_returns) * np.sqrt(252)
        
        if len(volatility) == 0: return
        
        mean_vol = np.mean([x[1] for x in volatility.items()])
        # leverage = (0.0833 / total_vol_annualized) * 100
        leverage = min((0.1 / mean_vol), self.leverage_cap)
        
        self.Liquidate()
        count = len(volatility)
        
        for symbol in volatility:
            if data.contains_key(symbol) and data[symbol]:
            # self.SetHoldings(symbol, 0.1667 * (1/count) * leverage)
                self.SetHoldings(symbol, leverage / count)
    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"))

发表评论

了解 Quant Buffet 的更多信息

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

继续阅读