该策略通过10年滚动回归估算夏普比率,当夏普比率高于0.2时投资于股票,否则转向无风险资产。每月调整投资组合,以实现最佳的风险收益比。

I. 策略概述

该策略通过10年滚动回归估算条件夏普比率,分别使用回归方程预测股票收益的均值和波动率,变量包括股息收益率和股票回购收益率等。每月比较预测的夏普比率与固定阈值(0.2)。

该策略灵活支持其他预测变量(可参考论文详细描述),以动态调整投资组合分配,优化条件风险收益权衡,实现系统化的风险管理与回报最大化。

II. 策略合理性

学术研究提供了两种关于夏普比率可预测性的理论:

  1. 消费者情绪理论
    消费者情绪与商业周期相关联。投资者在周期高峰期往往过于乐观,导致股票被高估,进而未来风险收益比下降;在周期低谷期,悲观情绪导致股票被低估,风险收益比上升。
  2. 理性风险厌恶理论
    条件收益和波动的时间变化由总体风险厌恶的变化驱动。在经济扩张期间,风险厌恶下降,预期收益和波动率较低;在经济衰退期间,风险厌恶上升,预期收益和波动率上升。

两种理论共同揭示了情绪与理性调整在商业周期中对夏普比率可预测性的影响,为市场时机策略提供了理论支持。

III. 论文来源

Time-Varying Sharpe Ratios and Market Timing [点击浏览原文]

<摘要>

本文记录了股票市场夏普比率的时间变化及其可预测性。研究使用预先设定的金融变量估计股票收益的条件均值和波动率,将两者结合估算条件夏普比率,或直接通过这些变量构建线性方程预测夏普比率。研究发现,条件夏普比率的显著时间变化与商业周期阶段一致。一般而言,夏普比率在周期高峰期较低,而在周期低谷期较高。这表明,通过预测夏普比率的变化,投资者能够有效调整资产配置以优化风险收益权衡。

IV. 回测表现

年化收益率14.18%
波动率15.38%
Beta0.376
夏普比率0.66
索提诺比率N/A
最大回撤N/A
胜率64%

V. 完整python代码

from AlgorithmImports import *
import numpy as np
import statsmodels.api as sm
#endregion
class MarketTimingUsingSharpeRatios(QCAlgorithm):
    
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        
        self.data = {}
        self.period = 21 # One month period
        self.regression_data = {}
        # self.regression_period = 10 * 12 + 1 # Need 10 years and one month of data, to predict Y
        self.regression_period = 5 * 12 + 1
        
        self.rf_asset = self.AddEquity('BIL', Resolution.Daily).Symbol  # 5/29/2007.
        
        self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        self.spy_monthly_return = RollingWindow[float](self.regression_period)
        self.spy_volatility = RollingWindow[float](self.regression_period)
        self.data[self.symbol] = SymbolData(self.period)
        
        self.dividend_yield = self.AddData(QuandlValue, 'MULTPL/SP500_DIV_YIELD_MONTH', Resolution.Daily).Symbol
        self.regression_data[self.dividend_yield] = RollingWindow[float](self.regression_period)
        
        self.buyback_yield = self.AddData(QuantpediaBuyBackYield, 'BUYBACK_YIELD', Resolution.Daily).Symbol
        self.regression_data[self.buyback_yield] = RollingWindow[float](self.regression_period)
        
        self.symbols = [self.symbol, self.buyback_yield, self.dividend_yield]
        
        self.threshold = 0.2
        self.selection_flag = False
        self.temp_buyback_storing = 0
        self.UniverseSettings.Resolution = Resolution.Daily
        self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection)
        
    def OnSecuritiesChanged(self, changes):
        for security in changes.AddedSecurities:
            security.SetFeeModel(CustomFeeModel())
            security.SetLeverage(5)
        
    def OnData(self, data):
        for symbol in self.symbols:
            if symbol in data:
                if data[symbol]:
                    value = data[symbol].Value
                    if value != 0:
                        if symbol == self.symbol: # Storing daily SPY close
                            self.data[symbol].update(value)
                        elif symbol == self.buyback_yield: # This will secure last value of the month
                            self.temp_buyback_storing = value
                        elif symbol == self.dividend_yield: # Annoucment is at the end of month, that's why we use self.temp_buyback_storing for best synchronization
                            self.regression_data[symbol].Add(value)
        
        if not self.selection_flag:
            return
        
        # make sure data is still comming in
        if self.Securities[self.buyback_yield].GetLastData() and (self.Time.date() - self.Securities[self.buyback_yield].GetLastData().Time.date()).days > 31:
            self.Liquidate()
            return
        if self.Securities[self.dividend_yield].GetLastData() and (self.Time.date() - self.Securities[self.dividend_yield].GetLastData().Time.date()).days > 5:
            self.Liquidate()
            return
        
        # Dividend_yield is announced at the end of month, so we take the data of buyback_yield at the end of month, if those two can't be synchronized
        self.regression_data[self.buyback_yield].Add(self.temp_buyback_storing)
        self.selection_flag = False
        
        # Storing monthly return and volatility of SPY
        if self.data[self.symbol].is_ready():
            self.spy_monthly_return.Add(self.data[self.symbol].monthly_return())
            self.spy_volatility.Add(self.data[self.symbol].volatility())
        
        sharpe_ratio = None
        
        # Check if our Y's are ready
        if self.spy_monthly_return.IsReady and self.spy_volatility.IsReady:
            # Check if data for our X are ready
            if self.regression_data[self.dividend_yield].IsReady and self.regression_data[self.buyback_yield].IsReady:
                Y1 = [x  for x in self.spy_volatility][:-1] # Everything except the first data
                Y2 = [x for x in self.spy_monthly_return][:-1] # Everything except the first data
                
                dividend_yield = [x for x in self.regression_data[self.dividend_yield]]
                buyback_yield = [x for x in self.regression_data[self.buyback_yield]]
                
                X = [
                  dividend_yield[1:], # Everything except the last data
                  buyback_yield[1:] # Everything except the last data
                ]
                
                # beta = slope, alpha = intercept
                # Get expected volatility
                regression_model = self.MultipleLinearRegression(X, Y1) # Volatility regression
                expected_volatility = regression_model.predict([1, dividend_yield[0], buyback_yield[0]]) # 1 is needed constant
                
                # Get expected monthly return
                regression_model = self.MultipleLinearRegression(X, Y2) # Monthly return regression
                expected_return = regression_model.predict([1, dividend_yield[0], buyback_yield[0]]) # 1 is needed constant
                
                sharpe_ratio = (expected_return / expected_volatility)[0]
                    
        # Trade execution
        if sharpe_ratio:
            # Liquidate risk-free asset and invest into SPY
            if sharpe_ratio > self.threshold:
                if self.Securities[self.rf_asset].Invested:
                    self.Liquidate(self.rf_asset)
                self.SetHoldings(self.symbol, 1)
            else: # Liquidate SPY and invest into risk-free asset
                if self.Securities[self.symbol].Invested:
                    self.Liquidate(self.symbol)
                self.SetHoldings(self.rf_asset, 1)
                    
    def MultipleLinearRegression(self, x, y):
        x = np.array(x).T
        x = sm.add_constant(x)
        result = sm.OLS(endog=y, exog=x).fit()
        return result        
        
    def Selection(self):
        self.selection_flag = True
class SymbolData():
    def __init__(self, period):
        self.Closes = RollingWindow[float](period)
        
    def update(self, close):
        self.Closes.Add(close)
        
    def is_ready(self):
        return self.Closes.IsReady
        
    def volatility(self):
        values = [x for x in self.Closes]
        values = np.array(values)
        returns = (values[:-1] - values[1:]) / values[1:]
        return np.std(returns) 
        
    def monthly_return(self):
        values = [x for x in self.Closes]
        return (values[0] - values[-1]) / values[-1]
        
# Custom fee model
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))
        
# Quandl "value" data
class QuandlValue(PythonQuandl):
    def __init__(self):
        self.ValueColumnName = 'Value'
        
# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaBuyBackYield(PythonData):
    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource("data.quantpedia.com/backtesting_data/buyback_yield/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
    def Reader(self, config, line, date, isLiveMode):
        data = QuantpediaBuyBackYield()
        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)
        buyback = float(split[1])
        SPX = float(split[2])
        data.Value = float(SPX / buyback)
        return data




发表评论

了解 Quant Buffet 的更多信息

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

继续阅读