Brinson Model 詳解含 Python 多期歸因?qū)嵺`

一、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è)好處:

  1. 對于管理人而言,區(qū)分了投資目標(biāo)設(shè)定的責(zé)任與具體投資管理的責(zé)任,基準(zhǔn)的表現(xiàn)好壞與投資目標(biāo)設(shè)定有關(guān),相對基準(zhǔn)表現(xiàn)的好壞與投資經(jīng)理的具體執(zhí)行有關(guān);
  2. 對于銷售或投資顧問而言,將目標(biāo)聚焦到幫助投資者設(shè)定適當(dāng)?shù)耐顿Y目標(biāo),選擇適合的資管產(chǎn)品,而不是以投資經(jīng)理來代替資管產(chǎn)品本身;
  3. 對于出資人而言,更容易確立投資目標(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:

Alpha = Allocation Effect + Selection Effect + Interactive Effect

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)成

B = \sum_{i=1}^{i=n}W_iB_i

公式(2) 投資組合配置行業(yè)的權(quán)重獲得回報(bào)

B_s = \sum_{i=1}^{i=n}w_iB_i

公式 (3) 因資產(chǎn)配置而形成的超額收益

B_s - B = \sum_{i=1}^{i=n}w_iB_i - \sum_{i=1}^{i=n}W_iB_i = \sum_{i=1}^{i=n}(w_i-W_i)B

公式 (4) 第i個(gè)行業(yè)的因資產(chǎn)配置而形成的超額收益貢獻(xiàn)

A_i = (w_i - W_i)B_i

公式 (5) 資產(chǎn)配置效果 (Allocation Effect)

\sum_{i=1}^{i=n}A_i = B_s - B

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)名義基金的收益

R_s = \sum_{i=1}^{i=n}W_iR_i

公式(7)名義基金相對基準(zhǔn)的超額收益

R_s - B =\sum_{i=1}^{i=n}W_iR_i - \sum_{i=1}^{i=n}W_iB_i = \sum_{i=1}^{i=n}W_i\times(R_i-B_i)

公式(8)子類資產(chǎn)的資產(chǎn)選擇效果

S_i = W_i(R_i- B_i)

公式(9)資產(chǎn)選擇效果 (Selection Effect)

\sum_{i=1}^{i=n}=R_s-B

1.3 Interactive(交互效應(yīng))

由于BHB Model 中 Seletion Effect 使用「名義基金」來代替了實(shí)際的組合,因此資產(chǎn)配置效果與資產(chǎn)選擇效果的算數(shù)合計(jì)值不等于投資組合實(shí)際的超額收益,其中還有尾差。

公式(10)資產(chǎn)配置效果、資產(chǎn)選擇效果合計(jì)與組合超額收益不等

Selection + Allocation = (R_s - B) + (B_s - B) =R_s+B_s-2B \neq R-B

公式(11)組合超額收益完全拆解

\underbrace{R_s - B}_{Selection} + \underbrace{B_s - B}_{Allocation} + \underbrace{R-R_s -B_s+B }_{Interaction} = R-B

Interactive (交互效應(yīng)), Brinson, Hood, and Beebower在論文中以 Other 表示,可能 Interactive 一詞更有解釋力,今天人們普遍采用該詞。 Interactive 不是一個(gè)殘差項(xiàng),而是一個(gè)直接計(jì)算可得的值,為子類資產(chǎn)實(shí)際與基準(zhǔn)權(quán)重的差\times子類資產(chǎn)實(shí)際與基準(zhǔn)回報(bào)的差。

R-R_s-B_s+B=\sum_{i=i}^{i=n}w_iR_i-\sum_{i=1}^{i=n}W_iR_i-\sum_{i=1}^{i=n}w_iB_i+\sum_{i=1}^{i=n}W_iB_i

公式(12)右邊公式簡化

\sum_{i=1}^{i=n}(w_i-W_i)(R_i-B_i) = \sum_{i=1}^{i=n}I_i

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)

B_s - B = \sum_{i=1}^{i=n}(w_i-W_i)B_i = \sum_{i=1}^{i=n}(w_i-W_i)(B_i-B)

因?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)B被介紹進(jìn)來。

公式(14)調(diào)整的子類資產(chǎn)的 Allocation Effect(資產(chǎn)配置效果)

A_i = (w_i-W_i)(B_i-B)

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)選擇效果)的定義稍加修改,從 R_s - B 改為 R-B_s,即:

公式(15)融合Interactive(交互效應(yīng))的 Selection Effect(資產(chǎn)選擇效果)
R-B_s = \sum_{i=1}^{i=n}w_iR_i-\sum_{i=1}^{i=n}w_iB_i = \sum_{i=1}^{i=n}w_i(R_i-B_i)
投資組合超額收益便完全由Selection Effect(資產(chǎn)選擇效果)與Allocation Effect(資產(chǎn)配置效果)構(gòu)成了:

Selection + Allocation = (R-B_s)+(B_s-B)=R-B

公式(16)子類資產(chǎn)的 Selection Effect(資產(chǎn)配置效果)
S_i = w_i(R_i-B_i)

二、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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

  • 個(gè)人簡歷 數(shù)據(jù)科學(xué)家,量化工程師,金融科技領(lǐng)域資深專家中山大學(xué)數(shù)據(jù)科學(xué)與計(jì)算機(jī)學(xué)院博士后,華中科技大學(xué)金融學(xué)博士,...
    shenciyou閱讀 1,060評論 0 2
  • 自我介紹 各位領(lǐng)導(dǎo)、老師、同事,大家上午好。我叫肖子龍,我的學(xué)校導(dǎo)師是鄭子彬教授,企業(yè)導(dǎo)師是孟曉總經(jīng)理。今天我要做...
    shenciyou閱讀 2,551評論 0 1
  • 項(xiàng)目管理術(shù)語英漢對照表2018-7-20 A Abstract Resource 抽象資源 Abstraction...
    007明_陽閱讀 6,674評論 0 51
  • 下面開始講smartbeta,簡稱sb。但是這個(gè)sb可不傻,它是聰明的beta。 beta是什么呢?就是風(fēng)險(xiǎn)! 這...
    bluescorpio閱讀 501評論 0 1
  • 1954年馬科維茨Harry Markowitz 在博士論文口試中,弗里德曼Milton Friedman對他說 ...
    慢魚愛立刻閱讀 5,067評論 2 5

友情鏈接更多精彩內(nèi)容