该策略投资于来自66个国家的ETF和期货,选择账面市值比(B/M)最高的33%国家做多,最低的33%国家做空,并每月重新平衡投资组合。

I. 策略概述

通过动态调整投资组合,策略利用国家层面的价值效应,从被低估和高估的市场中获利。

II. 策略合理性

价值效应的根源在于投资者心理偏差:

该策略基于这一理论,挖掘市场中由于心理偏差产生的套利机会。

III. 论文来源

A Performance Evaluation Model for Global Macro Funds [点击浏览原文]

<摘要>

本文集中研究国家投资组合中的价值效应和规模效应,对学术文献有以下三方面贡献:

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




发表评论

了解 Quant Buffet 的更多信息

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

继续阅读