一、Brinson Model 简介
Brinson Model,解构投资组合收益构成的方法。Brinson, Hood, and Beebower (1986) 推出该方法,把投资收益分解到两个部分,资产配置效果(Allocation Effect)与资产选择效果(Selection Effect)。
Brinson Model 基于一个假定的、通常的投资决策框架。首先,明确投资目标,用基准指数(benchmark)来构建实现该目标的途径;然后,解构目标,拆分成若干小目标,最后,选择具体的投资标的实现构建的子目标。以目标为导向,拆解落地到具体标的构建投资组合有几个好处:
- 对于管理人而言,区分了投资目标设定的责任与具体投资管理的责任,基准的表现好坏与投资目标设定有关,相对基准表现的好坏与投资经理的具体执行有关;
- 对于销售或投资顾问而言,将目标聚焦到帮助投资者设定适当的投资目标,选择适合的资管产品,而不是以投资经理来代替资管产品本身;
- 对于出资人而言,更容易确立投资目标,建立自己财产的管理体系,也更容易在投资组合表现不好的光景里接受事实,而不是将愤怒转移到销售、投顾或投资经理,毕竟他们自己是决定基准目标的最终决策人。
管理人或投资顾问帮助投资者设定符合投资目标的资产构成与权重,即长期投资目标 「战略资产配置」(SAA, Strategic Assets Allocation);投资经理根据现实的环境与投资机会对资产权重进行调整,称为「战术资产配置」(TAA,Tactic Assets Allocation),落实具体到操作,选择具体的标的来构建每一资产子类。
Brinson Model 拆解投资组合的收益构成,可方便模型的使用者清晰地观察到 TAA的决策效果(Allocation Effect)与投资经理对具体投资标的选择的效果( Selection Effect)。
1. BHB Model
1986年 最初的 Brinson Model 又名 BHB model,以 Brinson, Hood, and Beebower 三人名字首字母命名。
公式(0) BHB Model:
1.1 Allocation Effect (资产配置效果)
资产配置 (Allocation) ,即 TAA 的过程, 子目标权重偏离基准目标子类资产类别的权重,资产配置简单而言即高配或低配指数资产权重,以投资组合 100% 持有股票资产为例,假设其对标基准为「沪深300指数」,相对「沪深300指数」权重股所属行业的权重超配或低配形成的「超额回报贡献」为 Allocation Effect。
公式(1) 指数回报由行业回报贡献构成
公式(2) 投资组合配置行业的权重获得回报
公式 (3) 因资产配置而形成的超额收益
公式 (4) 第i个行业的因资产配置而形成的超额收益贡献
公式 (5) 资产配置效果 (Allocation Effect)
1.2 Selection Effect (资产选择效果)
资产选择(Selection), 即选择具体的标的构建子类资产。首先,根据基准的子类资产的权重构建一个「名义基金」,把资产选择效果从资产配置效果中分离出来,在特定的子类别中考察资产选择的效果。
公式(6)名义基金的收益
公式(7)名义基金相对基准的超额收益
公式(8)子类资产的资产选择效果
公式(9)资产选择效果 (Selection Effect)
1.3 Interactive(交互效应)
由于BHB Model 中 Seletion Effect 使用「名义基金」来代替了实际的组合,因此资产配置效果与资产选择效果的算数合计值不等于投资组合实际的超额收益,其中还有尾差。
公式(10)资产配置效果、资产选择效果合计与组合超额收益不等
公式(11)组合超额收益完全拆解
Interactive (交互效应), Brinson, Hood, and Beebower在论文中以 Other 表示,可能 Interactive 一词更有解释力,今天人们普遍采用该词。 Interactive 不是一个残差项,而是一个直接计算可得的值,为。
公式(12)右边公式简化
2. BF Model
Brinson-Fachler (BF) model 与 BHB model 的差异主要增加考虑了于子类资产的收益相对基准的收益。
在 BHB model 中,超配收益为正的子类资产获得正向的 allocation effect(资产配置效果),超配收益为负的子类资产获得负向的allocation effect(资产配置效果),这些 与是否该子类资产是否跑赢整体基准收益无关。BF model 对此进行了调整。
公式(13)
因为,常数项被介绍进来。
公式(14)调整的子类资产的 Allocation Effect(资产配置效果)
3. Interactive(交互效应)
BHB、BF 两个 Brinson model 都存在容易令人困惑的地方—— Interactive Effect(交互效应)。交互效应并不是投资决策的一部分,投资经理并不会通过交互效应来提升投资组合的价值,只是计算资产配置与个券选择上因为权重的不同而产生的差。
大多数的投资决策,首先考虑资产配置,然后考虑个券选择。而对于之下而上专注个股投资对与Brinson model而言并不适用,其投资决策过程中没有资产配置,那么也就无从考虑「资产配置效果」。
由于 Interacttion 不易被理解,因此消除此项的计算更为合理。将 Selection Effect (资产选择效果)的定义稍加修改,从 改为 ,即:
公式(15)融合Interactive(交互效应)的 Selection Effect(资产选择效果)
投资组合超额收益便完全由Selection Effect(资产选择效果)与Allocation Effect(资产配置效果)构成了:
公式(16)子类资产的 Selection Effect(资产配置效果)
二、Brinson Model 实现的现实问题
Brinson Model 给出了拆解收益,分析超额收益的框架方法,实操应用仍有诸多问题需要解决。Brinson Model计算的是静态截面数据,即一段期间内的投资组合收益与基准收益的比较,要求期初资产持有至期末,期间不涉及权重调整。实际的投资中,必定产生交易,必然期初的权重会产生变动与调整。
- 投资组合申赎等产生的现金流变动会影响到期初权重的调整;
- 投资组合的投资交易行为会影响到期初权重的调整。
为了尽可能消除投资组合的起初权重调整,应经可能将计算期间拆分至日频,并进行期间累计。累计收益的计算需考虑截面收益的时间价值,使得 Allocation Effect 与 Selection Effect 在时间序列上的汇总与总体的超额收益相等。
1. 计算期间的问题
Brinson 拆解的是投资收益率的构成,在实际的投资中,并非理想地“买入并持有”,每天都会有每一资产的权重变动,交易产生的收益,以及收益再投资的问题。
设想的计算方案1: 假设投资组合每日期初100%仓位,与基准每日进行比对,计算alpha,几何累计的方式,将alpha累计到完整期间。
- 优点:无期初投资为0的困扰
- 缺陷:投资组合与基准每日再投资金额不同步,导致累计的alpha不准确;
设想的计算方案2:投资组合与基准各自计算期间全部累计收益金额,根据期初投资金额计算各类资产的权重与收益率。
- 优点:计算简便,大多数情况下计算准确;
- 缺陷:存在期初资产为0的情形,将导致计算失败。
2. 国内 Benchmark 数据源的问题
国内指数成分股存在分红与拆股的问题,指数对此不做调整。通过成分股涨跌幅来推导指数回报,存在差异。采用全收益指数可以解决一部分成分股分红产生的问题,但拆股问题仍然无解。因此,在国内计算Brinson,alpha结果与实际投资组合与指数的差异不相等。
3. Brinson跨期计算的缓释方法
思路:
- 期初投资保持一致,假设投资总金额为1元,分别投向按照成份股的占比投向「投资组合」(Portfolio)与「基准」(index),分别计算「投资组合」与「基准」的跨期总收益,直接进行总收益的比对,获得 alpha 结果/
- 基准指数:按照买入并持有的假设,根据期初成份股权重,及成分股每日涨跌,推算出期间成分股收益,根据指数权重发布情况每月一调整;
- 投资组合:每日累计的方法计算投资收益,将每日产生的收益加回至下一日期初,参与下一日收益的计算,形成“复利”。 投资组合每日交易中,将买入金额算至期初投资,解决期初投资为0的问题。
- 投资组合期间收益与基准指数期间收益进行Brinson拆解,解决Brinson跨期计算。
缺陷: - 基准指数收益推算接近于全收益指数,与实际的指数收益存在差异,因为拆股与合股导致的问题无法解决,指数权重数量越多,发生次数越频繁,实际的差异也就越大。
三、Brinson Model 在 python 中的实现
1. Benchmark 的期间收益及资产类别拆分计算的代码
class Benchmark:
"""
Benchmark 期间内「行业」每日期初权重与回报:
根据指数公司权重公布日的权重(作为「期初权重」),按照成分股每日涨跌幅,推算每个交易日的「期初权重」
目标:取完整权重区间每日成分权重
方法:取期初公布权重,按成分股每日收益,假设 buy and hold, 计算出每日权重
步骤:
1. _benchmark_components_begin, 取期初公布权重;
2. _benchmark_components_return, 取期间成分股每日涨跌
3. _benchmark_componentes_weighs, 推算成分股每日权重
4. _add_industry, 加入行业分类信息
局限: 获取的指数为「全收益」指数, 未扣除分红影响. 虽然可以考虑使用未除权价,但不能排除拆股的情形.
: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: 行业分类 like 申万一级行业
calendar: 交易日历 XSHG -> 上海证券交易所
数据来源: 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:
"""
通过成分股期初权重,按照每日收益推算每日期初权重,获取每日「权重,收益率,加权收益率」
目前仅适用「按市值加权」
参数要求:
ohlc:
pd.DataFrame,
columns: ['preClose', 'open', 'high', 'low', 'close', 'adj_factor']
index: ['reportDate', secuTicker']
init_weights:
pd.Series
index: ['reportDate', secuTicker']
方法:
假设期初投资 1元钱, 按期初权重投资到成分股, 其权重为分配到的金额投资。
采用「买入并持有的策略」进行投资,期末按市值加权比例收回/补充投资, 使得投资额回到1元。
每日计算期初期末,每日调整,获得每日市值加权的权重。
算法:
1. 计算出 rtn_factor, 收盘价/前收盘价, 获得 rtn_factor, 交易所发布的「前收盘价」经过了除权调整;
2. 成分股 rtn_factor 累乘,获得每日期末净值(期初投资额公允价值调整)
3. 每日期末净值/成分股净值,获得每日期末成分股权重
4. 成分股权重从期末调整至期初 (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):
"""
根据 date 获取成分股权重公布日
direction:
- previous: 期初成分股权重公布日
- next: 期末成分股权重公布日
方法:
- 取 date 上月最后一个日历日,即本月首个日历日 - 1个日历日
- 判断若非交易日,则取最近一个交易日
- 若期末,则 date 调增一个月
: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:
"""
按日期推算得出该日期所在的指数权重公布期间段内的权重
input params:
date: str | dt | dt.date | pd.Timestamp 期间内的任意日期
bench_code: 指数代码
calendar: 交易日历, 默认采用「上交所」日历
source: JYDB, exchange-calendars
methodology:
1. 根据 date 获取期间 start_date 与 end_date
- start_date 为本期期初指数权重公布的日期
- end_date 为下期期初指数权重公布的日期
2. 取区间内期初权重 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()
# 取期间内成分股每日涨跌幅
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 期间内 一共获得了多少 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. 行业 「每日权重」与 「回报」
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 的期间收益及资产类别拆分计算的代码:
class Portfolio:
"""
Portfolio 期间内股票资产的每日期初权重与回报:
1. 从 lantern 取投资组合中的股票资产, 及其产生的每日收益 assetPL
3. 计算当期收益率:rtn = gainLoss / init, 根据 weight 计算出 w_rtn,
4. 跨期调整,w_rtn * adjust_factor
5. 将个券数据聚合到行业,weight 与 w_rtn 按行业汇总,计算行业 rtn = w_rtn / weight
** adjust_factor 的计算:汇总当日净值生成 净值序列,shift(1).fillna(1),以期初净值作为跨期因子,将单利调整为连续复利,实际计算只需要 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 -> 上海证券交易所
数据来源: lantern_db (自建的投资组合数据库)
"""
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
):
"""
数据库取值,筛选A股资产
"""
fund_abbr = lantern_api.query_fundAbbr(fund_code=fund_code)[fund_code]
# 1. lantern 数据库取值,并筛选出 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:
"""
只支持完整月度,或完整月度累积的分析,不支持区间分析,原因: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