“该策略每周在小盘股和国债之间切换,基于木材与黄金13周的表现,当两种资产之间的领导地位发生变化时,采取激进或防御性头寸。”

I. 策略概要

该策略根据过去13周木材与黄金的相对表现来择时小盘股和国债。如果木材表现优于黄金,投资组合将转向小盘股,采取更激进的立场。如果黄金表现优于木材,投资组合将转向国债,采取防御性立场。每周重新评估信号,仅在木材和黄金之间的领导地位发生变化时进行投资组合调整,使投资者能够适应市场趋势并有效地平衡风险和回报。

II. 策略合理性

学术论文强调,受住房和建筑活动驱动的木材价格,作为与消费需求相关的经济和股票市场增长的周期性领先指标。相反,黄金表现出避险特征,反映了风险厌恶。这种动态为投资者评估木材价格的变化提供了一个自然的基准,从而能够将周期性增长信号(木材)与风险厌恶趋势(黄金)进行比较。这些资产之间的关系为评估经济状况和根据风险情绪和增长预期的变化调整投资策略提供了一个框架。

III. 来源论文

价值如黄金:主动投资组合管理中的进攻与防守 [点击查看论文]

<摘要>

先前的学术研究侧重于将商品孤立地作为领先的经济指标,而忽略了价格行为可能对其他资产类别产生的信息。我们发现,木材相对于黄金的相对变动提供了关于经济增长和通胀预期的重要信息,这些信息会滞后地逐渐扩散到股票和债券市场。木材对住房的敏感性,而住房是美国国内经济增长的关键来源,这使得木材成为一种独特的商品,因为它与宏观基本面和风险偏好行为有关。光谱的另一端是黄金,它的独特之处在于,它在波动性加剧和股市压力时期历来表现出避险特性。我们发现,木材和黄金之间的关系有助于回答在主动投资组合管理中何时“打防守”和何时“打进攻”的关键问题。在本文中,我们表明,使用木材和黄金信号能力的策略,与被动的买入并持有指数相比,产生了更强的绝对回报和风险调整后回报。这种优异表现源于在木材领先于黄金的时期在投资组合中更加激进,而在黄金领先于木材的时期更加防御。结果对各种时间框架和跨多个经济和金融市场周期都具有稳健性。

IV. 回测表现

年化回报13.9%
波动率11.8%
β值0.042
夏普比率0.84
索提诺比率-0.15
最大回撤-20.8%
胜率61%

V. 完整的 Python 代码

import numpy as np
from AlgorithmImports import *
class LumberGoldRatio(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        self.etfs = ['IWM', 'IEF']
        self.symbols = ['CME_LB1', 'CME_GC1']
        self.data = {}
        ret_period = 13 * 5
        self.SetWarmUp(ret_period)
        
        for symbol in self.etfs:
            self.AddEquity(symbol, Resolution.Daily, leverage=4)
        
        for symbol in self.symbols:
            data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily)
            data.SetLeverage(5)
            self.data[symbol] = SymbolData(ret_period)
        
        self.rebalance_flag: bool = False
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday), self.TimeRules.AfterMarketOpen(self.etfs[0]), self.Rebalance)
    def OnData(self, data):
        for symbol in self.symbols:
            if self.securities[symbol].get_last_data() and self.time.date() > QuantpediaFutures.get_last_update_date()[symbol]:
                self.liquidate()
                return
            if symbol in data and data[symbol]:
                self.data[symbol].update(data[symbol].Value)
        if not self.rebalance_flag:
            return
        self.rebalance_flag = False
        lumber_data = self.data[self.symbols[0]]
        gold_data = self.data[self.symbols[1]]
        if all([data.contains_key(symbol) and data[symbol] for symbol in self.etfs]):
            if lumber_data.is_ready() and gold_data.is_ready():
                if lumber_data.performance() > gold_data.performance():
                    if self.Portfolio['IEF'].Invested:
                        self.Liquidate('IEF')
                    self.SetHoldings('IWM', 1)
                else:
                    if self.Portfolio['IWM'].Invested:
                        self.Liquidate('IWM')
                    self.SetHoldings('IEF', 1)
    def Rebalance(self):
        self.rebalance_flag = True
class SymbolData:
    def __init__(self, ret_lookback):
        self.history = RollingWindow[float](ret_lookback)
        self.price = 0.0
    def is_ready(self):
        return self.history.IsReady
        
    def update(self, value):
        self.price = float(value)
        self.history.Add(float(value))
    def performance(self):
        prices = np.array([x for x in self.history])
        return (prices[-1]-prices[0])/prices[0]
# 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

发表评论

了解 Quant Buffet 的更多信息

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

继续阅读