一、Brinson Model 簡介
Brinson Model,解構(gòu)投資組合收益構(gòu)成的方法。Brinson, Hood, and Beebower (1986) 推出該方法,把投資收益分解到兩個(gè)部分,資產(chǎn)配置效果(Allocation Effect)與資產(chǎn)選擇效果(Selection Effect)。
Brinson Model 基于一個(gè)假定的、通常的投資決策框架。首先,明確投資目標(biāo),用基準(zhǔn)指數(shù)(benchmark)來構(gòu)建實(shí)現(xiàn)該目標(biāo)的途徑;然后,解構(gòu)目標(biāo),拆分成若干小目標(biāo),最后,選擇具體的投資標(biāo)的實(shí)現(xiàn)構(gòu)建的子目標(biāo)。以目標(biāo)為導(dǎo)向,拆解落地到具體標(biāo)的構(gòu)建投資組合有幾個(gè)好處:
- 對于管理人而言,區(qū)分了投資目標(biāo)設(shè)定的責(zé)任與具體投資管理的責(zé)任,基準(zhǔn)的表現(xiàn)好壞與投資目標(biāo)設(shè)定有關(guān),相對基準(zhǔn)表現(xiàn)的好壞與投資經(jīng)理的具體執(zhí)行有關(guān);
- 對于銷售或投資顧問而言,將目標(biāo)聚焦到幫助投資者設(shè)定適當(dāng)?shù)耐顿Y目標(biāo),選擇適合的資管產(chǎn)品,而不是以投資經(jīng)理來代替資管產(chǎn)品本身;
- 對于出資人而言,更容易確立投資目標(biāo),建立自己財(cái)產(chǎn)的管理體系,也更容易在投資組合表現(xiàn)不好的光景里接受事實(shí),而不是將憤怒轉(zhuǎn)移到銷售、投顧或投資經(jīng)理,畢竟他們自己是決定基準(zhǔn)目標(biāo)的最終決策人。
管理人或投資顧問幫助投資者設(shè)定符合投資目標(biāo)的資產(chǎn)構(gòu)成與權(quán)重,即長期投資目標(biāo) 「戰(zhàn)略資產(chǎn)配置」(SAA, Strategic Assets Allocation);投資經(jīng)理根據(jù)現(xiàn)實(shí)的環(huán)境與投資機(jī)會對資產(chǎn)權(quán)重進(jìn)行調(diào)整,稱為「戰(zhàn)術(shù)資產(chǎn)配置」(TAA,Tactic Assets Allocation),落實(shí)具體到操作,選擇具體的標(biāo)的來構(gòu)建每一資產(chǎn)子類。
Brinson Model 拆解投資組合的收益構(gòu)成,可方便模型的使用者清晰地觀察到 TAA的決策效果(Allocation Effect)與投資經(jīng)理對具體投資標(biāo)的選擇的效果( Selection Effect)。
1. BHB Model
1986年 最初的 Brinson Model 又名 BHB model,以 Brinson, Hood, and Beebower 三人名字首字母命名。
公式(0) BHB Model:
1.1 Allocation Effect (資產(chǎn)配置效果)
資產(chǎn)配置 (Allocation) ,即 TAA 的過程, 子目標(biāo)權(quán)重偏離基準(zhǔn)目標(biāo)子類資產(chǎn)類別的權(quán)重,資產(chǎn)配置簡單而言即高配或低配指數(shù)資產(chǎn)權(quán)重,以投資組合 100% 持有股票資產(chǎn)為例,假設(shè)其對標(biāo)基準(zhǔn)為「滬深300指數(shù)」,相對「滬深300指數(shù)」權(quán)重股所屬行業(yè)的權(quán)重超配或低配形成的「超額回報(bào)貢獻(xiàn)」為 Allocation Effect。
公式(1) 指數(shù)回報(bào)由行業(yè)回報(bào)貢獻(xiàn)構(gòu)成
公式(2) 投資組合配置行業(yè)的權(quán)重獲得回報(bào)
公式 (3) 因資產(chǎn)配置而形成的超額收益
公式 (4) 第i個(gè)行業(yè)的因資產(chǎn)配置而形成的超額收益貢獻(xiàn)
公式 (5) 資產(chǎn)配置效果 (Allocation Effect)
1.2 Selection Effect (資產(chǎn)選擇效果)
資產(chǎn)選擇(Selection), 即選擇具體的標(biāo)的構(gòu)建子類資產(chǎn)。首先,根據(jù)基準(zhǔn)的子類資產(chǎn)的權(quán)重構(gòu)建一個(gè)「名義基金」,把資產(chǎn)選擇效果從資產(chǎn)配置效果中分離出來,在特定的子類別中考察資產(chǎn)選擇的效果。
公式(6)名義基金的收益
公式(7)名義基金相對基準(zhǔn)的超額收益
公式(8)子類資產(chǎn)的資產(chǎn)選擇效果
公式(9)資產(chǎn)選擇效果 (Selection Effect)
1.3 Interactive(交互效應(yīng))
由于BHB Model 中 Seletion Effect 使用「名義基金」來代替了實(shí)際的組合,因此資產(chǎn)配置效果與資產(chǎn)選擇效果的算數(shù)合計(jì)值不等于投資組合實(shí)際的超額收益,其中還有尾差。
公式(10)資產(chǎn)配置效果、資產(chǎn)選擇效果合計(jì)與組合超額收益不等
公式(11)組合超額收益完全拆解
Interactive (交互效應(yīng)), Brinson, Hood, and Beebower在論文中以 Other 表示,可能 Interactive 一詞更有解釋力,今天人們普遍采用該詞。 Interactive 不是一個(gè)殘差項(xiàng),而是一個(gè)直接計(jì)算可得的值,為。
公式(12)右邊公式簡化
2. BF Model
Brinson-Fachler (BF) model 與 BHB model 的差異主要增加考慮了于子類資產(chǎn)的收益相對基準(zhǔn)的收益。
在 BHB model 中,超配收益為正的子類資產(chǎn)獲得正向的 allocation effect(資產(chǎn)配置效果),超配收益為負(fù)的子類資產(chǎn)獲得負(fù)向的allocation effect(資產(chǎn)配置效果),這些 與是否該子類資產(chǎn)是否跑贏整體基準(zhǔn)收益無關(guān)。BF model 對此進(jìn)行了調(diào)整。
公式(13)
因?yàn)?img class="math-inline" src="https://math.jianshu.com/math?formula=%5Csum_%7Bi%3Di%7D%5E%7Bi%3Dn%7Dw_i%20%3D%20%5Csum_%7Bi%3Di%7D%5E%7Bi%3Dn%7DW_i%3D1" alt="\sum_{i=i}^{i=n}w_i = \sum_{i=i}^{i=n}W_i=1" mathimg="1">,常數(shù)項(xiàng)被介紹進(jìn)來。
公式(14)調(diào)整的子類資產(chǎn)的 Allocation Effect(資產(chǎn)配置效果)
3. Interactive(交互效應(yīng))
BHB、BF 兩個(gè) Brinson model 都存在容易令人困惑的地方—— Interactive Effect(交互效應(yīng))。交互效應(yīng)并不是投資決策的一部分,投資經(jīng)理并不會通過交互效應(yīng)來提升投資組合的價(jià)值,只是計(jì)算資產(chǎn)配置與個(gè)券選擇上因?yàn)闄?quán)重的不同而產(chǎn)生的差。
大多數(shù)的投資決策,首先考慮資產(chǎn)配置,然后考慮個(gè)券選擇。而對于之下而上專注個(gè)股投資對與Brinson model而言并不適用,其投資決策過程中沒有資產(chǎn)配置,那么也就無從考慮「資產(chǎn)配置效果」。
由于 Interacttion 不易被理解,因此消除此項(xiàng)的計(jì)算更為合理。將 Selection Effect (資產(chǎn)選擇效果)的定義稍加修改,從 改為
,即:
公式(15)融合Interactive(交互效應(yīng))的 Selection Effect(資產(chǎn)選擇效果)
投資組合超額收益便完全由Selection Effect(資產(chǎn)選擇效果)與Allocation Effect(資產(chǎn)配置效果)構(gòu)成了:
公式(16)子類資產(chǎn)的 Selection Effect(資產(chǎn)配置效果)
二、Brinson Model 實(shí)現(xiàn)的現(xiàn)實(shí)問題
Brinson Model 給出了拆解收益,分析超額收益的框架方法,實(shí)操應(yīng)用仍有諸多問題需要解決。Brinson Model計(jì)算的是靜態(tài)截面數(shù)據(jù),即一段期間內(nèi)的投資組合收益與基準(zhǔn)收益的比較,要求期初資產(chǎn)持有至期末,期間不涉及權(quán)重調(diào)整。實(shí)際的投資中,必定產(chǎn)生交易,必然期初的權(quán)重會產(chǎn)生變動與調(diào)整。
- 投資組合申贖等產(chǎn)生的現(xiàn)金流變動會影響到期初權(quán)重的調(diào)整;
- 投資組合的投資交易行為會影響到期初權(quán)重的調(diào)整。
為了盡可能消除投資組合的起初權(quán)重調(diào)整,應(yīng)經(jīng)可能將計(jì)算期間拆分至日頻,并進(jìn)行期間累計(jì)。累計(jì)收益的計(jì)算需考慮截面收益的時(shí)間價(jià)值,使得 Allocation Effect 與 Selection Effect 在時(shí)間序列上的匯總與總體的超額收益相等。
1. 計(jì)算期間的問題
Brinson 拆解的是投資收益率的構(gòu)成,在實(shí)際的投資中,并非理想地“買入并持有”,每天都會有每一資產(chǎn)的權(quán)重變動,交易產(chǎn)生的收益,以及收益再投資的問題。
設(shè)想的計(jì)算方案1: 假設(shè)投資組合每日期初100%倉位,與基準(zhǔn)每日進(jìn)行比對,計(jì)算alpha,幾何累計(jì)的方式,將alpha累計(jì)到完整期間。
- 優(yōu)點(diǎn):無期初投資為0的困擾
- 缺陷:投資組合與基準(zhǔn)每日再投資金額不同步,導(dǎo)致累計(jì)的alpha不準(zhǔn)確;
設(shè)想的計(jì)算方案2:投資組合與基準(zhǔn)各自計(jì)算期間全部累計(jì)收益金額,根據(jù)期初投資金額計(jì)算各類資產(chǎn)的權(quán)重與收益率。
- 優(yōu)點(diǎn):計(jì)算簡便,大多數(shù)情況下計(jì)算準(zhǔn)確;
- 缺陷:存在期初資產(chǎn)為0的情形,將導(dǎo)致計(jì)算失敗。
2. 國內(nèi) Benchmark 數(shù)據(jù)源的問題
國內(nèi)指數(shù)成分股存在分紅與拆股的問題,指數(shù)對此不做調(diào)整。通過成分股漲跌幅來推導(dǎo)指數(shù)回報(bào),存在差異。采用全收益指數(shù)可以解決一部分成分股分紅產(chǎn)生的問題,但拆股問題仍然無解。因此,在國內(nèi)計(jì)算Brinson,alpha結(jié)果與實(shí)際投資組合與指數(shù)的差異不相等。
3. Brinson跨期計(jì)算的緩釋方法
思路:
- 期初投資保持一致,假設(shè)投資總金額為1元,分別投向按照成份股的占比投向「投資組合」(Portfolio)與「基準(zhǔn)」(index),分別計(jì)算「投資組合」與「基準(zhǔn)」的跨期總收益,直接進(jìn)行總收益的比對,獲得 alpha 結(jié)果/
- 基準(zhǔn)指數(shù):按照買入并持有的假設(shè),根據(jù)期初成份股權(quán)重,及成分股每日漲跌,推算出期間成分股收益,根據(jù)指數(shù)權(quán)重發(fā)布情況每月一調(diào)整;
- 投資組合:每日累計(jì)的方法計(jì)算投資收益,將每日產(chǎn)生的收益加回至下一日期初,參與下一日收益的計(jì)算,形成“復(fù)利”。 投資組合每日交易中,將買入金額算至期初投資,解決期初投資為0的問題。
- 投資組合期間收益與基準(zhǔn)指數(shù)期間收益進(jìn)行Brinson拆解,解決Brinson跨期計(jì)算。
缺陷: - 基準(zhǔn)指數(shù)收益推算接近于全收益指數(shù),與實(shí)際的指數(shù)收益存在差異,因?yàn)椴鸸膳c合股導(dǎo)致的問題無法解決,指數(shù)權(quán)重?cái)?shù)量越多,發(fā)生次數(shù)越頻繁,實(shí)際的差異也就越大。
三、Brinson Model 在 python 中的實(shí)現(xiàn)
1. Benchmark 的期間收益及資產(chǎn)類別拆分計(jì)算的代碼
class Benchmark:
"""
Benchmark 期間內(nèi)「行業(yè)」每日期初權(quán)重與回報(bào):
根據(jù)指數(shù)公司權(quán)重公布日的權(quán)重(作為「期初權(quán)重」),按照成分股每日漲跌幅,推算每個(gè)交易日的「期初權(quán)重」
目標(biāo):取完整權(quán)重區(qū)間每日成分權(quán)重
方法:取期初公布權(quán)重,按成分股每日收益,假設(shè) buy and hold, 計(jì)算出每日權(quán)重
步驟:
1. _benchmark_components_begin, 取期初公布權(quán)重;
2. _benchmark_components_return, 取期間成分股每日漲跌
3. _benchmark_componentes_weighs, 推算成分股每日權(quán)重
4. _add_industry, 加入行業(yè)分類信息
局限: 獲取的指數(shù)為「全收益」指數(shù), 未扣除分紅影響. 雖然可以考慮使用未除權(quán)價(jià),但不能排除拆股的情形.
:return:
components_w_rtn -> pd.DataFrame
- cols: [weights, rtn, w_rtn]
- index: [reportDate, secuTicker]
"""
def __init__(self,
bench_code: str,
start_date: str | dt.date | pd.Timestamp,
end_date: str | dt.date | pd.Timestamp,
industry: dict,
calendar: Literal['XSHG'] = 'XSHG'
):
"""
bench_code: benchmark code, like 000300 -> 滬深300
date: 起始日期 str -> YYYY-MM-DD
end_date: 截止日期 str -> YYYY-MM-DD
industry_category: 行業(yè)分類 like 申萬一級行業(yè)
calendar: 交易日歷 XSHG -> 上海證券交易所
數(shù)據(jù)來源: JYDB
"""
self.bench_code = bench_code
self.start_date = date_formate(date=start_date, mode='date')
self.end_date = date_formate(date=end_date, mode='date')
self.industry = industry
self.calendar = calendar
@staticmethod
def _algorithm_daily_rtn_weights(ohlc: pd.DataFrame,
init_weights: pd.Series) -> pd.DataFrame:
"""
通過成分股期初權(quán)重,按照每日收益推算每日期初權(quán)重,獲取每日「權(quán)重,收益率,加權(quán)收益率」
目前僅適用「按市值加權(quán)」
參數(shù)要求:
ohlc:
pd.DataFrame,
columns: ['preClose', 'open', 'high', 'low', 'close', 'adj_factor']
index: ['reportDate', secuTicker']
init_weights:
pd.Series
index: ['reportDate', secuTicker']
方法:
假設(shè)期初投資 1元錢, 按期初權(quán)重投資到成分股, 其權(quán)重為分配到的金額投資。
采用「買入并持有的策略」進(jìn)行投資,期末按市值加權(quán)比例收回/補(bǔ)充投資, 使得投資額回到1元。
每日計(jì)算期初期末,每日調(diào)整,獲得每日市值加權(quán)的權(quán)重。
算法:
1. 計(jì)算出 rtn_factor, 收盤價(jià)/前收盤價(jià), 獲得 rtn_factor, 交易所發(fā)布的「前收盤價(jià)」經(jīng)過了除權(quán)調(diào)整;
2. 成分股 rtn_factor 累乘,獲得每日期末凈值(期初投資額公允價(jià)值調(diào)整)
3. 每日期末凈值/成分股凈值,獲得每日期末成分股權(quán)重
4. 成分股權(quán)重從期末調(diào)整至期初 (shift(1)) 得到成分股每日權(quán)重 (期初)
5. 成分股每日權(quán)重(期初)* 成分股漲跌幅 得到成分股「每日加權(quán)收益率」
:return: pd.DataFrame, with cols ['weights', 'rtn', 'w_rtn'] while index ['reportDate', 'secuTicker']
"""
# 1. 計(jì)算出 rtn_factor
rtn_factor = (ohlc['close'] / ohlc['preClose']) \
.unstack() \
.cumprod() \
.stack() \
.rename('rtn_factor')
# 2. 計(jì)算每日權(quán)重
rtn_df = pd.DataFrame(rtn_factor) \
.join(init_weights) \
.join((ohlc['close'] / ohlc['preClose'] - 1).rename('rtn'))
w_factor = (rtn_df['rtn_factor'] * rtn_df['weights']) \
.unstack() \
.shift(1) \
.stack() \
.rename('w_factor')
rtn_df = rtn_df.join(w_factor, how='right')
rtn_df['weights'] = rtn_df.groupby(level=0, group_keys=False)['w_factor'].apply(lambda x: x / x.sum())
# 3. 計(jì)算每日成分股加權(quán)收益率
rtn_df['w_rtn'] = rtn_df['weights'] * rtn_df['rtn']
return rtn_df[['weights', 'rtn', 'w_rtn']]
@staticmethod
def _get_disclose_date(date,
calendar,
direction):
"""
根據(jù) date 獲取成分股權(quán)重公布日
direction:
- previous: 期初成分股權(quán)重公布日
- next: 期末成分股權(quán)重公布日
方法:
- 取 date 上月最后一個(gè)日歷日,即本月首個(gè)日歷日 - 1個(gè)日歷日
- 判斷若非交易日,則取最近一個(gè)交易日
- 若期末,則 date 調(diào)增一個(gè)月
:return:
self.disclose_date
"""
date = date_formate(date=date, mode='date')
cals = xcals.get_calendar(calendar)
direction = direction
if direction == 'previous':
pass
elif direction == 'next':
date = date + relativedelta(months=1)
else:
raise KeyError('direction error!')
# 取 date 本月本月首個(gè)日歷日前一個(gè)最近的交易日
disclose_date = dt(year=date.year, month=date.month, day=1) - relativedelta(days=1)
disclose_date = cals.date_to_session(disclose_date, direction='previous')
return dt.strftime(disclose_date, '%Y-%m-%d')
@staticmethod
def _components_weighted_return(date: str | dt | dt.date | pd.Timestamp,
bench_code: str,
calendar: Literal['XSHG'] = 'XSHG') -> pd.DataFrame:
"""
按日期推算得出該日期所在的指數(shù)權(quán)重公布期間段內(nèi)的權(quán)重
input params:
date: str | dt | dt.date | pd.Timestamp 期間內(nèi)的任意日期
bench_code: 指數(shù)代碼
calendar: 交易日歷, 默認(rèn)采用「上交所」日歷
source: JYDB, exchange-calendars
methodology:
1. 根據(jù) date 獲取期間 start_date 與 end_date
- start_date 為本期期初指數(shù)權(quán)重公布的日期
- end_date 為下期期初指數(shù)權(quán)重公布的日期
2. 取區(qū)間內(nèi)期初權(quán)重 init_weights 與 成分股每日漲跌幅, 推算得出成分股每日期初權(quán)重
scripts:
1. _get_disclose_date 獲取期初成分股權(quán)重披露日期
2. _algorithm_daily_rtn_weights 成分股每日權(quán)重的算法
:return: rtn_df
pd.DataFrame
with cols ['weights', 'rtn', 'w_rtn'] while index ['reportDate', 'secuTicker']
"""
# 取期間的起始日期與終止日期
start_date = date_formate(date=Benchmark._get_disclose_date(date=date,
calendar=calendar,
direction='previous'),
mode='str')
end_date = date_formate(date=Benchmark._get_disclose_date(date=date,
calendar=calendar,
direction='next'),
mode='str')
# 取成期初成分股權(quán)重
components_weight = jydb.query_index_component_weights(index_code=bench_code,
date=start_date) \
.reset_index()
init_weights = components_weight.set_index('secuTicker')['weights']
init_weights = init_weights / init_weights.sum()
# 取期間內(nèi)成分股每日漲跌幅
ohlc = jydb.quote_ohlc_secu(secu_ticker=components_weight['secuTicker'].to_list(),
start_date=start_date,
end_date=end_date) \
.set_index(['reportDate', 'secuTicker']) \
.query('category == "stocks"')
# 推導(dǎo)獲取成分股「每日權(quán)重」與「每日收益」
rtn_df = Benchmark._algorithm_daily_rtn_weights(ohlc=ohlc, init_weights=init_weights)
return rtn_df
@property
def _get_disclosure_batches(self):
"""
獲取 benchmark 成分股披露的批次,已知每月末披露一次
"""
start_date = date_formate(date=self.start_date, mode='date')
end_date = date_formate(date=self.end_date, mode='date')
num_months = (end_date.year - start_date.year) * 12 + (end_date.month - start_date.month) + 1
batches = [start_date + relativedelta(months=n) for n in range(num_months)]
return batches
@property
def bench_period_weighted_return(self):
"""
累積 benchmark 期間加權(quán)回報(bào)率的計(jì)算
"""
# 1. 成分股「每日權(quán)重」 與 「回報(bào)」
Batches = self._get_disclosure_batches
w_rtn = pd.DataFrame()
for date in Batches:
cache_rtn_df = self._components_weighted_return(date=date,
bench_code=self.bench_code,
calendar=self.calendar)
w_rtn = pd.concat([w_rtn, cache_rtn_df])
start_date = date_formate(date=self.start_date, mode='str')
end_date = date_formate(date=self.end_date, mode='str')
w_rtn = w_rtn.query(f'reportDate >= "{start_date}" and reportDate <= "{end_date}"').reset_index()
w_rtn['industry'] = w_rtn['secuTicker'].map(self.industry)
industry_w_rtn = w_rtn.groupby(['reportDate', 'industry'])[['weights', 'w_rtn']].sum()
industry_w_rtn['rtn'] = industry_w_rtn['w_rtn'] / industry_w_rtn['weights']
# 解析
# (1) 假設(shè)期初 1元 本金, Benchmark 期間內(nèi) 一共獲得了多少 return,
B_factor = (industry_w_rtn['w_rtn'].unstack().sum(axis=1) + 1).cumprod()
B = B_factor.iloc[-1] - 1
# (2) 假設(shè)期初 1元,每只票的 contribution 是多少?
industry_w_rtn = industry_w_rtn.join(B_factor.shift(1).rename('adj_factor'))
industry_w_rtn['adj_factor'] = industry_w_rtn['adj_factor'].fillna(1)
contribution = industry_w_rtn['w_rtn'] * industry_w_rtn['adj_factor']
contribution = contribution.unstack().sum()
# (3) 平均 rate of return = contribution / 平均 weights
weights = industry_w_rtn['weights'].unstack().sum()
weights = weights / weights.sum()
rtn = contribution / weights
bench = pd.DataFrame(weights.rename('Wi')) \
.join(rtn.rename('bi')) \
.join(contribution.rename('WB'))
bench['B'] = B
return bench
@property
def bench_daily_weighted_return(self):
"""
Benchmark 期初至期末, 每日的成分股權(quán)重
:return:
pd.DataFrame
with cols ['weights', 'rtn', 'w_rtn'] while index ['reportDate', 'secuTicker']
"""
# 1. 成分股「每日權(quán)重」 與 「回報(bào)」
Batches = self._get_disclosure_batches
w_rtn = pd.DataFrame()
for date in Batches:
cache_rtn_df = self._components_weighted_return(date=date,
bench_code=self.bench_code,
calendar=self.calendar)
w_rtn = pd.concat([w_rtn, cache_rtn_df])
start_date = date_formate(date=self.start_date, mode='str')
end_date = date_formate(date=self.end_date, mode='str')
w_rtn = w_rtn.query(f'reportDate >= "{start_date}" and reportDate <= "{end_date}"').reset_index()
# 2. 行業(yè) 「每日權(quán)重」與 「回報(bào)」
w_rtn['industry'] = w_rtn['secuTicker'].map(self.industry)
industry_w_rtn = w_rtn.groupby(['reportDate', 'industry'])[['weights', 'rtn', 'w_rtn']].sum()
industry_w_rtn['rtn'] = industry_w_rtn['w_rtn'] / industry_w_rtn['weights']
return industry_w_rtn
2. Portfolio 的期間收益及資產(chǎn)類別拆分計(jì)算的代碼:
class Portfolio:
"""
Portfolio 期間內(nèi)股票資產(chǎn)的每日期初權(quán)重與回報(bào):
1. 從 lantern 取投資組合中的股票資產(chǎn), 及其產(chǎn)生的每日收益 assetPL
3. 計(jì)算當(dāng)期收益率:rtn = gainLoss / init, 根據(jù) weight 計(jì)算出 w_rtn,
4. 跨期調(diào)整,w_rtn * adjust_factor
5. 將個(gè)券數(shù)據(jù)聚合到行業(yè),weight 與 w_rtn 按行業(yè)匯總,計(jì)算行業(yè) rtn = w_rtn / weight
** adjust_factor 的計(jì)算:匯總當(dāng)日凈值生成 凈值序列,shift(1).fillna(1),以期初凈值作為跨期因子,將單利調(diào)整為連續(xù)復(fù)利,實(shí)際計(jì)算只需要 r * 期初單位凈值就可以了。
"""
def __init__(self,
fund_code: str,
start_date: str | dt.date | pd.Timestamp,
end_date: str | dt.date | pd.Timestamp,
industry: dict,
calendar: Literal['XSHG'] = 'XSHG'
):
"""
fund_code: benchmark code, like 000300 -> 滬深300
date: 起始日期 str -> YYYY-MM-DD
end_date: 截止日期 str -> YYYY-MM-DD
calendar: 交易日歷 XSHG -> 上海證券交易所
數(shù)據(jù)來源: lantern_db (自建的投資組合數(shù)據(jù)庫)
"""
self.fund_code = fund_code
self.start_date = date_formate(date=start_date, mode='date')
self.end_date = date_formate(date=end_date, mode='date')
self.industry = industry
self.calendar = calendar
@staticmethod
def _load_assets(fund_code: str,
start_date: str,
end_date: str,
industry: dict
):
"""
數(shù)據(jù)庫取值,篩選A股資產(chǎn)
"""
fund_abbr = lantern_api.query_fundAbbr(fund_code=fund_code)[fund_code]
# 1. lantern 數(shù)據(jù)庫取值,并篩選出 stocks
assetPL = gain_loss.AssetPl(fund_abbr, start_date, end_date).fit()
assets = assetPL.gainLoss.query('assetClass == "stocks"').copy()
assets['industry'] = assets['secuTicker'].map(industry)
return assets
@property
def portfolio_period_components_w_rtn(self):
"""
計(jì)算投資組合期間成份的加權(quán)回報(bào)率
"""
start_date = date_formate(date=self.start_date, mode='str')
end_date = date_formate(date=self.end_date, mode='str')
assets = self._load_assets(fund_code=self.fund_code,
start_date=start_date,
end_date=end_date,
industry=self.industry)
cals = xcals.get_calendar(self.calendar)
sessions = cals.sessions_in_range(start=start_date, end=end_date)
assets = assets[assets['reportDate'].apply(lambda x: x in sessions)]
w_rtn = assets.groupby(['reportDate', 'industry'])[['init', 'netPL']].sum()
w_rtn['rtn'] = w_rtn['netPL'] / w_rtn['init']
w_rtn['weights'] = w_rtn.groupby(level=0, group_keys=False)['init'].apply(lambda x: x / x.sum())
w_rtn['w_rtn'] = w_rtn['weights'] * w_rtn['rtn']
# (1) 假設(shè)期初投資為1元,復(fù)利計(jì)算期末是多少錢
p_factor = (w_rtn.groupby(level=0, group_keys=False)['w_rtn'].sum() + 1).cumprod()
p = p_factor.iloc[-1] - 1
# (2) 假設(shè)期初 1元,每只票的 contribution 是多少?
w_rtn = w_rtn.join(p_factor.shift(1).rename('adj_factor'))
w_rtn['adj_factor'] = w_rtn['adj_factor'].fillna(1)
contribution = w_rtn['w_rtn'] * w_rtn['adj_factor']
contribution = contribution.unstack().sum()
# (3) 平均 rate of return = contribution / 平均 weights
weights = w_rtn['weights'].unstack().sum()
weights = weights / weights.sum()
rtn = contribution / weights
port = pd.DataFrame(weights.rename('wi')) \
.join(rtn.rename('pi')) \
.join(contribution.rename('wp'))
port['P'] = p
return port
3. BF 模型計(jì)算的代碼
class Brinson_Model:
"""
只支持完整月度,或完整月度累積的分析,不支持區(qū)間分析,原因:jydb 每月最后一個(gè)交易日提供的 benchmarks 成分構(gòu)成
"""
def __init__(self,
fund_code: str,
bench_code: str,
start_date: str | dt.date | pd.Timestamp,
end_date: str | dt.date | pd.Timestamp,
industry: Literal['申萬'] = '申萬',
calendar: Literal['XSHG'] = 'XSHG',
model: Literal['BHB', 'BF'] = 'BF'
):
"""
date: str -> 'YYYY-MM-DD' | dt.datetime | pd.Timestamp
end_date: str -> 'YYYY-MM-DD' | dt.datetime | pd.Timestamp
"""
self.fund_code = fund_code
self.bench_code = bench_code
self.start_date = start_date
self.end_date = end_date
self.industry = jydb.query_stock_industry(standard=industry, level=1, secu_category=1)
self.calendar = calendar
self.model = model
self.cache = cache
self.industry_a_stocks = None
self.trading_days = None
self.bench_components_weights = None
@property
def benchmark(self):
"""
取 benchmark 然后聚合
"""
bench = Benchmark(bench_code=self.bench_code,
start_date=self.start_date,
end_date=self.end_date,
industry=self.industry,
calendar=self.calendar)
bench = bench.bench_period_weighted_return.sort_index()
return bench
@property
def portfolio(self):
"""
取 portfolio
"""
port = Portfolio(fund_code=self.fund_code,
start_date=self.start_date,
end_date=self.end_date,
industry=self.industry,
calendar=self.calendar)
port = port.portfolio_period_components_w_rtn.sort_index()
return port
@property
def _model_data(self):
"""
"""
benchmark = self.benchmark
portfolio = self.portfolio
industry = list(set(self.industry.values()))
ind = pd.Index(data=industry, name='industry')
model_df = pd.DataFrame(index=ind).sort_index()
model_df = model_df.join(benchmark, how='left').join(portfolio, how='left')
model_df[['B', 'P']] = model_df[['B', 'P']].fillna(method='ffill').fillna(method='bfill')
model_df = model_df.fillna(0)
cols = ['Wi', 'wi', 'bi', 'pi', 'B', 'WB', 'wp', 'P']
return model_df[cols].fillna(0)
@property
def BF_model(self):
"""
BF_mode without interactive effect
"""
model_data = self._model_data
Wi = model_data['Wi']
bi = model_data['bi']
B = model_data['B']
wi = model_data['wi']
pi = model_data['pi']
P = model_data['P']
allocation = (bi - B) * (wi - Wi)
selection = wi * (pi - bi)
alpha = allocation + selection
result = pd.DataFrame(index=model_data.index)
result = result \
.join(allocation.rename('allocation')) \
.join(selection.rename('selection')) \
.join(alpha.rename('alpha'))
B = round(list(set(B))[0] * 1e2, 2)
P = round(list(set(P))[0] * 1e2, 2)
alpha = P - B
result_detail = result.sort_values('alpha', ascending=False).round(4) * 1e2
result = result.sum().round(4) * 1e2
result = {
'detail': result_detail,
'alpha': [P, B, alpha],
'result': result,
}
return result