投资范围包括在孟买证券交易所交易的股票,排除不一致和市值较小的股票。月度数据来自汤森路透DataStream。主要变量是累积平均回报率(CAR),即过去12个月的回报总和。每月末将股票排序为赢家、输家和其他组合,做多输家投资组合,做空赢家投资组合。该策略为等权重,并每月重新平衡。

策略概述

投资范围包括在孟买证券交易所交易的股票,排除了不一致和市值较小的股票。月度数据来自汤森路透DataStream。主要变量是累积平均回报率(CAR),即股票在过去12个月的回报之和。在每月月底,将股票排序为三个投资组合:赢家、输家或其他。赢家投资组合包括过去12个月累积平均回报率(CAR)在前20%的股票,输家投资组合包括累积平均回报率在后20%的股票。做多输家投资组合,做空赢家投资组合。该策略为等权重,并每月重新平衡。

策略合理性

该策略的有效性源于反向收益与股票市场状况之间的联系。反向收益主要来自于市场状态的变化。股票市场状况的变化对反向收益有重要影响,反转回报在市场处于负面状态、波动率较高或危机时期更为显著。 即便考虑了已知的风险因素影响,反向收益与市场状况变化的关联性依然存在。时间依赖的市场效率降低,尤其在股票市场异常收益较高时与市场状况变化相关。研究表明,像印度这样的新兴市场的投资者通常难以适应市场条件的变化。因此,反向回报机会经常出现,这些市场存在持续的弱式市场效率低下。

论文来源

Adaptive Market Hypothesis and Timevarying Contrarian Effect: Evidence From Emerging Stock Markets of South Asia [点击浏览原文]

<摘要>

本研究为股票市场状况变化与股票市场异常收益之间的关系辩论提供了新贡献。我们研究了市场条件变化对时间变化的反向收益的影响,以检验适应性市场假说(AMH)在南亚新兴股票市场中的存在。实证结果显示,强劲的反向效应在所有新兴市场中均存在。我们还发现,基于反向投资组合的股票回报机会随时间而变化。研究表明,反向收益在市场处于下行状态、波动率较高和危机时期尤其强劲,特别是在亚洲金融危机期间。有趣的是,市场状态而非市场波动率是反向收益的主要预测指标,这与发达市场的研究结果相矛盾。我们认为,这种联系源于新兴市场中的结构和心理差异,产生了对股票市场异常收益的独特直觉。本研究关于时间变化的反向收益的整体发现部分支持了AMH。此外,本研究的重要结果表明,南亚新兴市场的投资者与发达市场投资者一样,难以适应不断变化的市场条件。因此,反向收益往往存在,新兴市场中持续的弱式市场效率低下现象普遍存在。

回测表现

年化收益率47.74%
波动率36.21%
Beta-0.024
夏普比率1.32
索提诺比率N/A
最大回撤N/A
胜率56%

完整python代码

from AlgorithmImports import *
from data_tools import QuantpediaIndiaStocks, SymbolData, CustomFeeModel
#endregion

class ReversalEffectInIndia(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        
        self.data:dict = {}
        
        self.portfolio_percentage:float = 0.5
        self.days_in_month:int = 21
        self.period:int = 12 * self.days_in_month # 12 months of daily closes
        self.SetWarmUp(self.period, Resolution.Daily)
        
        self.quantile:int = 5
        self.leverage:int = 5
        self.perf_period:int = 12
        self.max_missing_days:int = 5
        self.symbol:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        
        csv_string_file = self.Download('data.quantpedia.com/backtesting_data/equity/india_stocks/india_nifty_500_tickers.csv')
        line_split = csv_string_file.split(';')
        
        # NOTE: Download method is rate-limited to 100 calls (https://github.com/QuantConnect/Documentation/issues/345)
        for ticker in line_split[:99]:
            security = self.AddData(QuantpediaIndiaStocks, ticker, Resolution.Daily)
            security.SetFeeModel(CustomFeeModel())
            security.SetLeverage(self.leverage)
        
            symbol:Symbol = security.Symbol
            self.data[symbol] = SymbolData(self.period)
        
        self.recent_month:int = -1

    def OnData(self, data):
        # store daily prices
        for symbol in self.data:
            if symbol in data and data[symbol]:
                price:float = data[symbol].Value
                if price != 0 and not np.isnan(price):
                    self.data[symbol].update(price)
        
        if self.IsWarmingUp: return

        # rebalance monthly
        if self.Time.month == self.recent_month:
            return
        self.recent_month = self.Time.month

        performance:[Symbol, float] = {}
        
        for symbol, symbol_obj in self.data.items():
            # prices are ready and data is still comming in
            if symbol_obj.is_ready() and self.Securities[symbol].GetLastData() and \
                (self.Time.date() - self.Securities[symbol].GetLastData().Time.date()).days <= self.max_missing_days:
                perf:float = self.data[symbol].performance(self.days_in_month)
                if perf != 0 and not np.isnan(perf):
                    performance[symbol] = perf
        
        long_part:list[Symbol] = []
        short_part:list[Symbol] = []

        if len(performance) >= self.quantile:
            quantile:int = int(len(performance) / self.quantile)
            sorted_by_performance = [x[0] for x in sorted(performance.items(), key=lambda item: item[1], reverse=True)]

            # Long the loser portfolio and short the winner portfolio
            # The winner portfolio consists of stocks with the prior 12-month CARs in the top 20%.
            short_part = sorted_by_performance[-quantile:]
            # The loser portfolio consists of stocks with the prior 12-month CARs in the bottom 20%.
            long_part = sorted_by_performance[:quantile]
                
        # trade execution
        long_count:int = len(long_part)
        short_count:int = len(short_part)

        stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested]
        for symbol in stocks_invested:
            if symbol not in long_part + short_part:
                self.Liquidate(symbol)

        for symbol in long_part:
            self.SetHoldings(symbol, (1 / long_count) * self.portfolio_percentage)
        for symbol in short_part:
            self.SetHoldings(symbol, (-1 / short_count) * self.portfolio_percentage)

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading