一、Brinson Model 簡介
Brinson Model,解構投資組合收益構成的方法。Brinson, Hood, and Beebower (1986) 推出該方法,把投資收益分解到兩個部分,資產(chǎn)配置效果(Allocation Effect)與資產(chǎn)選擇效果(Selection Effect)。
Brinson Model 基于一個假定的、通常的投資決策框架。首先,明確投資目標,用基準指數(shù)(benchmark)來構建實現(xiàn)該目標的途徑;然后,解構目標,拆分成若干小目標,最后,選擇具體的投資標的實現(xiàn)構建的子目標。以目標為導向,拆解落地到具體標的構建投資組合有幾個好處:
- 對于管理人而言,區(qū)分了投資目標設定的責任與具體投資管理的責任,基準的表現(xiàn)好壞與投資目標設定有關,相對基準表現(xiàn)的好壞與投資經(jīng)理的具體執(zhí)行有關;
- 對于銷售或投資顧問而言,將目標聚焦到幫助投資者設定適當?shù)耐顿Y目標,選擇適合的資管產(chǎn)品,而不是以投資經(jīng)理來代替資管產(chǎn)品本身;
- 對于出資人而言,更容易確立投資目標,建立自己財產(chǎn)的管理體系,也更容易在投資組合表現(xiàn)不好的光景里接受事實,而不是將憤怒轉(zhuǎn)移到銷售、投顧或投資經(jīng)理,畢竟他們自己是決定基準目標的最終決策人。
管理人或投資顧問幫助投資者設定符合投資目標的資產(chǎn)構成與權重,即長期投資目標 「戰(zhàn)略資產(chǎn)配置」(SAA, Strategic Assets Allocation);投資經(jīng)理根據(jù)現(xiàn)實的環(huán)境與投資機會對資產(chǎn)權重進行調(diào)整,稱為「戰(zhàn)術資產(chǎn)配置」(TAA,Tactic Assets Allocation),落實具體到操作,選擇具體的標的來構建每一資產(chǎn)子類。
Brinson Model 拆解投資組合的收益構成,可方便模型的使用者清晰地觀察到 TAA的決策效果(Allocation Effect)與投資經(jīng)理對具體投資標的選擇的效果( 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 的過程, 子目標權重偏離基準目標子類資產(chǎn)類別的權重,資產(chǎn)配置簡單而言即高配或低配指數(shù)資產(chǎn)權重,以投資組合 100% 持有股票資產(chǎn)為例,假設其對標基準為「滬深300指數(shù)」,相對「滬深300指數(shù)」權重股所屬行業(yè)的權重超配或低配形成的「超額回報貢獻」為 Allocation Effect。
公式(1) 指數(shù)回報由行業(yè)回報貢獻構成
公式(2) 投資組合配置行業(yè)的權重獲得回報
公式 (3) 因資產(chǎn)配置而形成的超額收益
公式 (4) 第i個行業(yè)的因資產(chǎn)配置而形成的超額收益貢獻
公式 (5) 資產(chǎn)配置效果 (Allocation Effect)
1.2 Selection Effect (資產(chǎn)選擇效果)
資產(chǎn)選擇(Selection), 即選擇具體的標的構建子類資產(chǎn)。首先,根據(jù)基準的子類資產(chǎn)的權重構建一個「名義基金」,把資產(chǎn)選擇效果從資產(chǎn)配置效果中分離出來,在特定的子類別中考察資產(chǎn)選擇的效果。
公式(6)名義基金的收益
公式(7)名義基金相對基準的超額收益
公式(8)子類資產(chǎn)的資產(chǎn)選擇效果
公式(9)資產(chǎn)選擇效果 (Selection Effect)
1.3 Interactive(交互效應)
由于BHB Model 中 Seletion Effect 使用「名義基金」來代替了實際的組合,因此資產(chǎn)配置效果與資產(chǎn)選擇效果的算數(shù)合計值不等于投資組合實際的超額收益,其中還有尾差。
公式(10)資產(chǎn)配置效果、資產(chǎn)選擇效果合計與組合超額收益不等
公式(11)組合超額收益完全拆解
Interactive (交互效應), Brinson, Hood, and Beebower在論文中以 Other 表示,可能 Interactive 一詞更有解釋力,今天人們普遍采用該詞。 Interactive 不是一個殘差項,而是一個直接計算可得的值,為。
公式(12)右邊公式簡化
2. BF Model
Brinson-Fachler (BF) model 與 BHB model 的差異主要增加考慮了于子類資產(chǎn)的收益相對基準的收益。
在 BHB model 中,超配收益為正的子類資產(chǎn)獲得正向的 allocation effect(資產(chǎn)配置效果),超配收益為負的子類資產(chǎn)獲得負向的allocation effect(資產(chǎn)配置效果),這些 與是否該子類資產(chǎn)是否跑贏整體基準收益無關。BF model 對此進行了調(diào)整。
公式(13)
因為,常數(shù)項
被介紹進來。
公式(14)調(diào)整的子類資產(chǎn)的 Allocation Effect(資產(chǎn)配置效果)
3. Interactive(交互效應)
BHB、BF 兩個 Brinson model 都存在容易令人困惑的地方—— Interactive Effect(交互效應)。交互效應并不是投資決策的一部分,投資經(jīng)理并不會通過交互效應來提升投資組合的價值,只是計算資產(chǎn)配置與個券選擇上因為權重的不同而產(chǎn)生的差。
大多數(shù)的投資決策,首先考慮資產(chǎn)配置,然后考慮個券選擇。而對于之下而上專注個股投資對與Brinson model而言并不適用,其投資決策過程中沒有資產(chǎn)配置,那么也就無從考慮「資產(chǎn)配置效果」。
由于 Interacttion 不易被理解,因此消除此項的計算更為合理。將 Selection Effect (資產(chǎn)選擇效果)的定義稍加修改,從 改為
,即:
公式(15)融合Interactive(交互效應)的 Selection Effect(資產(chǎn)選擇效果)
投資組合超額收益便完全由Selection Effect(資產(chǎn)選擇效果)與Allocation Effect(資產(chǎn)配置效果)構成了:
公式(16)子類資產(chǎn)的 Selection Effect(資產(chǎn)配置效果)
二、Brinson Model 實現(xiàn)的現(xiàn)實問題
Brinson Model 給出了拆解收益,分析超額收益的框架方法,實操應用仍有諸多問題需要解決。Brinson Model計算的是靜態(tài)截面數(shù)據(jù),即一段期間內(nèi)的投資組合收益與基準收益的比較,要求期初資產(chǎn)持有至期末,期間不涉及權重調(diào)整。實際的投資中,必定產(chǎn)生交易,必然期初的權重會產(chǎn)生變動與調(diào)整。
- 投資組合申贖等產(chǎn)生的現(xiàn)金流變動會影響到期初權重的調(diào)整;
- 投資組合的投資交易行為會影響到期初權重的調(diào)整。
為了盡可能消除投資組合的起初權重調(diào)整,應經(jīng)可能將計算期間拆分至日頻,并進行期間累計。累計收益的計算需考慮截面收益的時間價值,使得 Allocation Effect 與 Selection Effect 在時間序列上的匯總與總體的超額收益相等。
1. 計算期間的問題
Brinson 拆解的是投資收益率的構成,在實際的投資中,并非理想地“買入并持有”,每天都會有每一資產(chǎn)的權重變動,交易產(chǎn)生的收益,以及收益再投資的問題。
設想的計算方案1: 假設投資組合每日期初100%倉位,與基準每日進行比對,計算alpha,幾何累計的方式,將alpha累計到完整期間。
- 優(yōu)點:無期初投資為0的困擾
- 缺陷:投資組合與基準每日再投資金額不同步,導致累計的alpha不準確;
設想的計算方案2:投資組合與基準各自計算期間全部累計收益金額,根據(jù)期初投資金額計算各類資產(chǎn)的權重與收益率。
- 優(yōu)點:計算簡便,大多數(shù)情況下計算準確;
- 缺陷:存在期初資產(chǎn)為0的情形,將導致計算失敗。
2. 國內(nèi) Benchmark 數(shù)據(jù)源的問題
國內(nèi)指數(shù)成分股存在分紅與拆股的問題,指數(shù)對此不做調(diào)整。通過成分股漲跌幅來推導指數(shù)回報,存在差異。采用全收益指數(shù)可以解決一部分成分股分紅產(chǎn)生的問題,但拆股問題仍然無解。因此,在國內(nèi)計算Brinson,alpha結果與實際投資組合與指數(shù)的差異不相等。
3. Brinson跨期計算的緩釋方法
思路:
- 期初投資保持一致,假設投資總金額為1元,分別投向按照成份股的占比投向「投資組合」(Portfolio)與「基準」(index),分別計算「投資組合」與「基準」的跨期總收益,直接進行總收益的比對,獲得 alpha 結果/
- 基準指數(shù):按照買入并持有的假設,根據(jù)期初成份股權重,及成分股每日漲跌,推算出期間成分股收益,根據(jù)指數(shù)權重發(fā)布情況每月一調(diào)整;
- 投資組合:每日累計的方法計算投資收益,將每日產(chǎn)生的收益加回至下一日期初,參與下一日收益的計算,形成“復利”。 投資組合每日交易中,將買入金額算至期初投資,解決期初投資為0的問題。
- 投資組合期間收益與基準指數(shù)期間收益進行Brinson拆解,解決Brinson跨期計算。
缺陷: - 基準指數(shù)收益推算接近于全收益指數(shù),與實際的指數(shù)收益存在差異,因為拆股與合股導致的問題無法解決,指數(shù)權重數(shù)量越多,發(fā)生次數(shù)越頻繁,實際的差異也就越大。
三、Brinson Model 在 python 中的實現(xiàn)
1. Benchmark 的期間收益及資產(chǎn)類別拆分計算的代碼
class Benchmark:
"""
Benchmark 期間內(nèi)「行業(yè)」每日期初權重與回報:
根據(jù)指數(shù)公司權重公布日的權重(作為「期初權重」),按照成分股每日漲跌幅,推算每個交易日的「期初權重」
目標:取完整權重區(qū)間每日成分權重
方法:取期初公布權重,按成分股每日收益,假設 buy and hold, 計算出每日權重
步驟:
1. _benchmark_components_begin, 取期初公布權重;
2. _benchmark_components_return, 取期間成分股每日漲跌
3. _benchmark_componentes_weighs, 推算成分股每日權重
4. _add_industry, 加入行業(yè)分類信息
局限: 獲取的指數(shù)為「全收益」指數(shù), 未扣除分紅影響. 雖然可以考慮使用未除權價,但不能排除拆股的情形.
: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:
"""
通過成分股期初權重,按照每日收益推算每日期初權重,獲取每日「權重,收益率,加權收益率」
目前僅適用「按市值加權」
參數(shù)要求:
ohlc:
pd.DataFrame,
columns: ['preClose', 'open', 'high', 'low', 'close', 'adj_factor']
index: ['reportDate', secuTicker']
init_weights:
pd.Series
index: ['reportDate', secuTicker']
方法:
假設期初投資 1元錢, 按期初權重投資到成分股, 其權重為分配到的金額投資。
采用「買入并持有的策略」進行投資,期末按市值加權比例收回/補充投資, 使得投資額回到1元。
每日計算期初期末,每日調(diào)整,獲得每日市值加權的權重。
算法:
1. 計算出 rtn_factor, 收盤價/前收盤價, 獲得 rtn_factor, 交易所發(fā)布的「前收盤價」經(jīng)過了除權調(diào)整;
2. 成分股 rtn_factor 累乘,獲得每日期末凈值(期初投資額公允價值調(diào)整)
3. 每日期末凈值/成分股凈值,獲得每日期末成分股權重
4. 成分股權重從期末調(diào)整至期初 (shift(1)) 得到成分股每日權重 (期初)
5. 成分股每日權重(期初)* 成分股漲跌幅 得到成分股「每日加權收益率」
:return: pd.DataFrame, with cols ['weights', 'rtn', 'w_rtn'] while index ['reportDate', 'secuTicker']
"""
# 1. 計算出 rtn_factor
rtn_factor = (ohlc['close'] / ohlc['preClose']) \
.unstack() \
.cumprod() \
.stack() \
.rename('rtn_factor')
# 2. 計算每日權重
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. 計算每日成分股加權收益率
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 獲取成分股權重公布日
direction:
- previous: 期初成分股權重公布日
- next: 期末成分股權重公布日
方法:
- 取 date 上月最后一個日歷日,即本月首個日歷日 - 1個日歷日
- 判斷若非交易日,則取最近一個交易日
- 若期末,則 date 調(diào)增一個月
: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 本月本月首個日歷日前一個最近的交易日
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ù)權重公布期間段內(nèi)的權重
input params:
date: str | dt | dt.date | pd.Timestamp 期間內(nèi)的任意日期
bench_code: 指數(shù)代碼
calendar: 交易日歷, 默認采用「上交所」日歷
source: JYDB, exchange-calendars
methodology:
1. 根據(jù) date 獲取期間 start_date 與 end_date
- start_date 為本期期初指數(shù)權重公布的日期
- end_date 為下期期初指數(shù)權重公布的日期
2. 取區(qū)間內(nèi)期初權重 init_weights 與 成分股每日漲跌幅, 推算得出成分股每日期初權重
scripts:
1. _get_disclose_date 獲取期初成分股權重披露日期
2. _algorithm_daily_rtn_weights 成分股每日權重的算法
: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')
# 取成期初成分股權重
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"')
# 推導獲取成分股「每日權重」與「每日收益」
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 期間加權回報率的計算
"""
# 1. 成分股「每日權重」 與 「回報」
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) 假設期初 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) 假設期初 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 期初至期末, 每日的成分股權重
:return:
pd.DataFrame
with cols ['weights', 'rtn', 'w_rtn'] while index ['reportDate', 'secuTicker']
"""
# 1. 成分股「每日權重」 與 「回報」
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è) 「每日權重」與 「回報」
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)類別拆分計算的代碼:
class Portfolio:
"""
Portfolio 期間內(nèi)股票資產(chǎn)的每日期初權重與回報:
1. 從 lantern 取投資組合中的股票資產(chǎn), 及其產(chǎn)生的每日收益 assetPL
3. 計算當期收益率:rtn = gainLoss / init, 根據(jù) weight 計算出 w_rtn,
4. 跨期調(diào)整,w_rtn * adjust_factor
5. 將個券數(shù)據(jù)聚合到行業(yè),weight 與 w_rtn 按行業(yè)匯總,計算行業(yè) rtn = w_rtn / weight
** adjust_factor 的計算:匯總當日凈值生成 凈值序列,shift(1).fillna(1),以期初凈值作為跨期因子,將單利調(diào)整為連續(xù)復利,實際計算只需要 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):
"""
計算投資組合期間成份的加權回報率
"""
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) 假設期初投資為1元,復利計算期末是多少錢
p_factor = (w_rtn.groupby(level=0, group_keys=False)['w_rtn'].sum() + 1).cumprod()
p = p_factor.iloc[-1] - 1
# (2) 假設期初 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 模型計算的代碼
class Brinson_Model:
"""
只支持完整月度,或完整月度累積的分析,不支持區(qū)間分析,原因:jydb 每月最后一個交易日提供的 benchmarks 成分構成
"""
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