该策略投资于32个可访问股票市场的国家。每年年底,投资者计算每个国家的Shiller CAPE(周期调整市盈率),并选择CAPE低于15的最低33%国家进行投资。投资组合按等权重分配,高于15的国家持有0%现金,每年重新平衡。

策略概述

投资范围包括32个拥有可访问股票市场的国家(例如通过ETF)。每年年底,投资者为其投资范围内的每个国家计算Shiller的“CAPE”(周期调整市盈率)。CAPE是股票市场的实际价格(经通胀调整)与该国股票指数过去10年的平均值(同样经通胀调整)之比。投资者然后投资于样本中CAPE最低的33%国家,这些国家的CAPE必须低于15。投资组合等权分配(对于CAPE高于15的国家,投资者持有0%的现金),并每年重新平衡。

策略合理性

这一异常现象来源于投资者心理。学术研究提出,投资者对新闻和事件过度反应;“赢家”(即受欢迎的国家)往往被高估,而“输家”(即被忽视的国家)则被低估。因此,逆向投资者可以利用这种普遍的投资者心态,通过在股票价格回归其内在价值时获利,从而利用市场效率低下的机会。

论文来源

Global Value: Building Trading Models with the 10 Year CAPE [点击浏览原文]

<摘要>

70多年前,Benjamin Graham和David Dodd提出了通过多年平滑的收益来对证券进行估值。Robert Shiller在1990年代后期推广了这种周期调整市盈率(CAPE)的方法,并及时发出了未来几年股票回报不佳的预警。我们将这一估值指标应用于30多个外国市场,发现它既实用又有效,并且在海外市场目睹了比美国更大的泡沫和崩盘现象。然后,我们创建了一个基于估值构建全球股票投资组合的交易系统,发现在基于相对和绝对估值选择市场时,显著优于其他策略。

回测表现

年化收益率14.7%
波动率26.1%
Beta0.55
夏普比率0.384
索提诺比率0.4
最大回撤36.1%
胜率79%

完整python代码

from AlgoLib import *
#endregion

class ValueFactorCAPEEffectwithinCountries(XXX):

    def Initialize(self):
        self.SetStartDate(2008, 1, 1)  
        self.SetCash(100000)

        self.symbols = {
            "Australia"     : "EWA",  # iShares MSCI Australia Index ETF
            "Brazil"        : "EWZ",  # iShares MSCI Brazil Index ETF
            "Canada"        : "EWC",  # iShares MSCI Canada Index ETF
            "Switzerland"   : "EWL",  # iShares MSCI Switzerland Index ETF
            "China"         : "FXI",  # iShares China Large-Cap ETF
            "France"        : "EWQ",  # iShares MSCI France Index ETF
            "Germany"       : "EWG",  # iShares MSCI Germany ETF 
            "Hong Kong"     : "EWH",  # iShares MSCI Hong Kong Index ETF
            "Italy"         : "EWI",  # iShares MSCI Italy Index ETF
            "Japan"         : "EWJ",  # iShares MSCI Japan Index ETF
            "Korea"         : "EWY",  # iShares MSCI South Korea ETF
            "Mexico"        : "EWW",  # iShares MSCI Mexico Inv. Mt. Idx
            "Netherlands"   : "EWN",  # iShares MSCI Netherlands Index ETF
            "South Africa"  : "EZA",  # iShares MSCI South Africe Index ETF
            "Singapore"     : "EWS",  # iShares MSCI Singapore Index ETF
            "Spain"         : "EWP",  # iShares MSCI Spain Index ETF
            "Sweden"        : "EWD",  # iShares MSCI Sweden Index ETF
            "Taiwan"        : "EWT",  # iShares MSCI Taiwan Index ETF
            "UK"            : "EWU",  # iShares MSCI United Kingdom Index ETF
            "USA"           : "SPY",  # SPDR S&P 500 ETF
            
            "Russia"        : "ERUS",  # iShares MSCI Russia ETF
            "Israel"        : "EIS",   # iShares MSCI Israel ETF
            "India"         : "INDA",  # iShares MSCI India ETF
            "Poland"        : "EPOL",  # iShares MSCI Poland ETF
            "Turkey"        : "TUR"    # iShares MSCI Turkey ETF
        }

        self.quantile:int = 3
        self.max_missing_days:int = 31
        self.leverage:int = 2

        for country, etf_symbol in self.symbols.items():
            data = self.AddEquity(etf_symbol, Resolution.Daily)
            data.SetLeverage(self.leverage)
            data.SetFeeModel(CustomFeeModel())
        
        # CAPE data import.
        self.cape_data = self.AddData(CAPE, 'CAPE',  Resolution.Daily).Symbol
            
        self.recent_month:int = -1
    
    def OnData(self, data:Slice) -> None:
        if self.Time.month == self.recent_month:
            return
        self.recent_month = self.Time.month

        if self.recent_month != 12:
            return
        
        price = {}
        for country, etf_symbol in self.symbols.items():
            if etf_symbol in data and data[etf_symbol]:
                # cape data is still comming in
                if self.Securities[self.cape_data].GetLastData() and (self.Time.date() - self.Securities[self.cape_data].GetLastData().Time.date()).days <= self.max_missing_days:
                    country_cape = self.Securities['CAPE'].GetLastData().GetProperty(country)
                    if country_cape < 15. and country_cape != 0.:
                        price[etf_symbol] = data[etf_symbol].Value

        long = []
        
        # Cape and price sorting.
        if len(price) >= self.quantile:
            sorted_by_price = sorted(price.items(), key = lambda x: x[1], reverse = True)
            tercile = int(len(sorted_by_price) / self.quantile)
            long = [x[0] for x in sorted_by_price[-tercile:]]
        
        # Trade execution.
        invested = [x.Key for x in self.Portfolio if x.Value.Invested]
        for symbol in invested:
            if symbol not in long:
                self.Liquidate(symbol)
        
        for symbol in long:
            if symbol in data and data[symbol]:
                self.SetHoldings(symbol, 1 / len(long))

# NOTE: IMPORTANT: Data order must be ascending (datewise)
# Data source: https://indices.barclays/IM/21/en/indices/static/historic-cape.app
class CAPE(PythonData):
    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource("data.quantpedia.com/backtesting_data/economic/cape_by_country.csv", SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)

    _header_columns:List[str] = []

    def Reader(self, config, line, date, isLiveMode):
        data = CAPE()
        data.Symbol = config.Symbol
        
        if not line[0].isdigit():
            CAPE._header_columns = line.split(',')[1:]
            return None

        split = line.split(',')
        
        data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
        for i, col in enumerate(CAPE._header_columns):
            if split[i+1] != '':
                data[col] = float(split[i+1])
            else:
                data[col] = 0.
        
        data.Value = float(split[1])

        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"))

Leave a Reply

Discover more from Quant Buffet

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

Continue reading