“该策略投资于32个可访问股票市场的国家。每年年底,投资者计算每个国家的Shiller CAPE(周期调整市盈率),并选择CAPE低于15的最低33%国家进行投资。投资组合按等权重分配,高于15的国家持有0%现金,每年重新平衡。”
资产类别:ETF | 地区:全球 | 频率:每年 | 市场:股票 | 关键词:价值,CAPE
策略概述
投资范围包括32个拥有可访问股票市场的国家(例如通过ETF)。每年年底,投资者为其投资范围内的每个国家计算Shiller的“CAPE”(周期调整市盈率)。CAPE是股票市场的实际价格(经通胀调整)与该国股票指数过去10年的平均值(同样经通胀调整)之比。投资者然后投资于样本中CAPE最低的33%国家,这些国家的CAPE必须低于15。投资组合等权分配(对于CAPE高于15的国家,投资者持有0%的现金),并每年重新平衡。
策略合理性
这一异常现象来源于投资者心理。学术研究提出,投资者对新闻和事件过度反应;“赢家”(即受欢迎的国家)往往被高估,而“输家”(即被忽视的国家)则被低估。因此,逆向投资者可以利用这种普遍的投资者心态,通过在股票价格回归其内在价值时获利,从而利用市场效率低下的机会。
论文来源
Global Value: Building Trading Models with the 10 Year CAPE [点击浏览原文]
- Meb Faber,美国Cambria投资管理公司
<摘要>
70多年前,Benjamin Graham和David Dodd提出了通过多年平滑的收益来对证券进行估值。Robert Shiller在1990年代后期推广了这种周期调整市盈率(CAPE)的方法,并及时发出了未来几年股票回报不佳的预警。我们将这一估值指标应用于30多个外国市场,发现它既实用又有效,并且在海外市场目睹了比美国更大的泡沫和崩盘现象。然后,我们创建了一个基于估值构建全球股票投资组合的交易系统,发现在基于相对和绝对估值选择市场时,显著优于其他策略。


回测表现
| 年化收益率 | 14.7% |
| 波动率 | 26.1% |
| Beta | 0.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"))
