Quant Buffet放轻松,别过度思虑

使用木材/黄金比率进行市场择时

登录后收藏

学术论文

Lumber: Worth It's Weight in Gold: Offense and Defense in Active Portfolio Management

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

机构
  • ?Lead-Lag Publishing, LLC

策略概要

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

II. 策略合理性

回测表现

波动率11.8%
夏普比率0.84
索提诺比率-0.15
最大回撤-20.8%
胜率61%

完整 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