
“该策略投资于来自66个国家的ETF和期货,选择账面市值比(B/M)最高的33%国家做多,最低的33%国家做空,并每月重新平衡投资组合。”
资产类别:ETF、期货 | 区域:全球 | 频率:每月 | 市场:股票 | 关键词:价值效应
I. 策略概述
- 投资范围:
66个国家的ETF和本地期货。 - B/M比计算:
每个市场的账面市值比(B/M)基于个股的B/M比汇总计算。 - 国家排序:
根据国家的B/M比对市场进行排序: - 权重分配与再平衡:
通过动态调整投资组合,策略利用国家层面的价值效应,从被低估和高估的市场中获利。
II. 策略合理性
价值效应的根源在于投资者心理偏差:
- 过度反应: 投资者倾向于对新闻和事件反应过度,使“赢家”国家(受欢迎的市场)被高估,而“失败者”国家(被忽视的市场)被低估。
- 均值回归: 当股票价格回归其内在价值时,反向投资者可以利用这种市场低效性获利。
该策略基于这一理论,挖掘市场中由于心理偏差产生的套利机会。
III. 论文来源
A Performance Evaluation Model for Global Macro Funds [点击浏览原文]
- 作者: Zaremba, 蒙彼利埃商学院 & 波兹南经济与商业大学
<摘要>
本文集中研究国家投资组合中的价值效应和规模效应,对学术文献有以下三方面贡献:
1. 引入了一个宏观层面的替代Fama-French模型,与原型不同,该模型使用基于国家的因子。研究表明,与标准CAPM和Fama-French模型相比,这一修改模型在评估具有全球投资授权的基金表现方面更加成功。
2. 提供新的证据表明,价值效应和规模效应有助于解释国家间收益的横截面差异。计算基于2000-2013年66个国家的广泛样本。
3. 记录了国家层面的价值效应和规模效应对货币转换无差异性。
IV. 回测表现
| 年化收益率 | 6.8% |
| 波动率 | 9.21% |
| Beta | -0.012 |
| 夏普比率 | 0.3 |
| 索提诺比率 | N/A |
| 最大回撤 | N/A |
| 胜率 | 49% |
V. 完整python代码
from AlgorithmImports import *
#endregion
class ValueEffectwithinCountries(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
self.symbols = {
'Argentina' : 'ARGT',
'Australia' : 'EWA',
'Austria' : 'EWO',
'Belgium' : 'EWK',
'Brazil' : 'EWZ',
'Canada' : 'EWC',
'Chile' : 'ECH',
'China' : 'FXI',
'Egypt' : 'EGPT',
'France' : 'EWQ',
'Germany' : 'EWG',
'Hong Kong' : 'EWH',
'India' : 'INDA',
'Indonesia' : 'EIDO',
'Ireland' : 'EIRO',
'Israel' : 'EIS',
'Italy' : 'EWI',
'Japan' : 'EWJ',
'Malaysia' : 'EWM',
'Mexico' : 'EWW',
'Netherlands' : 'EWN',
'New Zealand' : 'ENZL',
'Norway' : 'NORW',
'Philippines' : 'EPHE',
'Poland' : 'EPOL',
'Russia' : 'ERUS',
'Saudi Arabia' : 'KSA',
'Singapore' : 'EWS',
'South Africa' : 'EZA',
'South Korea' : 'EWY',
'Spain' : 'EWS',
'Sweden' : 'EWD',
'Switzerland' : 'EWL',
'Taiwan' : 'EWT',
'Thailand' : 'THD',
'Turkey' : 'TUR',
'United Kingdom' : 'EWU',
'United States' : 'SPY'
}
for symbol in self.symbols:
data = self.AddEquity(self.symbols[symbol], Resolution.Daily)
data.SetLeverage(5)
self.country_pb_data:Symbol = self.AddData(CountryPB, 'CountryData').Symbol
self.quantile:int = 3
self.recent_month:int = -1
self.max_missing_days:int = 365
def OnData(self, data:Slice) -> None:
# rebalance once a month
if self.recent_month == self.Time.month:
return
self.recent_month = self.Time.month
if self.Securities[self.country_pb_data].GetLastData() and (self.Time.date() - self.Securities[self.country_pb_data].GetLastData().Time.date()).days > self.max_missing_days:
self.Liquidate()
return
bm_data:dict[str, float] = {}
country_pb_data = self.Securities[self.country_pb_data].GetLastData()
if country_pb_data:
for symbol in self.symbols:
pb:float = country_pb_data[symbol]
bm_data[symbol] = 1 / pb
long:List[str] = []
short:List[str] = []
if len(bm_data) >= self.quantile:
sorted_by_bm:List = sorted(bm_data.items(), key = lambda x: x[1], reverse = True)
quantile:int = int(len(bm_data) / self.quantile)
long = [x[0] for x in sorted_by_bm[:quantile]]
short = [x[0] for x in sorted_by_bm[-quantile:]]
# liquidate
invested:List[str] = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in long + short:
self.Liquidate(symbol)
# trade execution
long_count:int = len(long)
short_count:int = len(short)
for symbol in long:
traded_symbol:str = self.symbols[symbol]
if traded_symbol in data and data[traded_symbol]:
self.SetHoldings(traded_symbol, 1 / long_count)
for symbol in short:
traded_symbol:str = self.symbols[symbol]
if traded_symbol in data and data[traded_symbol]:
self.SetHoldings(traded_symbol, -1 / short_count)
# Country PB data
# NOTE: IMPORTANT: Data order must be ascending (date-wise)
from dateutil.relativedelta import relativedelta
class CountryPB(PythonData):
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource("data.quantpedia.com/backtesting_data/economic/country_pb.csv", SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config, line, date, isLiveMode):
data = CountryPB()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split = line.split(';')
data.Time = datetime.strptime(split[0], "%Y") + relativedelta(years=1)
self.symbols = ['Argentina','Australia','Austria','Belgium','Brazil','Canada','Chile','China','Egypt','France','Germany','Hong Kong','India','Indonesia','Ireland','Israel','Italy','Japan','Malaysia','Mexico','Netherlands','New Zealand','Norway','Philippines','Poland','Russia','Saudi Arabia','Singapore','South Africa','South Korea','Spain','Sweden','Switzerland','Taiwan','Thailand','Turkey','United Kingdom','United States']
index = 1
for symbol in self.symbols:
data[symbol] = float(split[index])
index += 1
data.Value = float(split[1])
return data