业绩归因:赚的钱到底从哪来
年底结账,账户一年涨了 25%。你翻着自己的交易记录,觉得那几笔重仓买得挺准,心里已经开始盘算明年要不要把仓位加一倍。
先别急。同一年,你天天看的那个宽基指数涨了 22%。
这个数字一摆出来,故事就完全变了。你真正靠自己挣到的,只有 3 个百分点——而且这 3 个点里,可能还有一大半来自「你恰好重仓了当年最强的那个行业」,跟你在这个行业里挑了哪几只票,关系没你想的那么大。
业绩归因要回答的就是这一件事:这笔钱,具体是谁挣的。
先说人话:把功劳拆开
想象你开了家餐厅,第一年就赚钱了。你的第一反应大概是「我的菜做得好」。
可仔细想想,这一年里发生了这么几件事:整条商业街的人流比去年翻了一倍;你所在的这个品类(比如烤肉)刚好是这一年最火的;隔壁那家老店年中关门了,客人分流过来一批;另外你自己确实把出品稳住了,回头客不少。
现在问题来了:明年你想在另一条街开第二家店,能不能复制这份成功?
如果你不知道这四份功劳各占多少,你就没法回答。 全靠人流红利,换条街就完了;全靠品类风口,风口一过就完了;只有那份「出品稳」是你真正带得走的东西。
投资里一模一样。你账户里那条向上的曲线,至少混着这么几股力量:整个市场在涨、你重仓的行业在涨、你在行业里挑的那几只比同行强、以及——纯粹的运气。这四样长得完全一样,都表现为账户数字变大。不把它们拆开,你就是在拿一个自己不理解的结果,去下下一次的注。
投资层:不做归因,你会怎么犯错
把 beta 当成 alpha
先把两个词说清楚,很简单。beta 是「跟着大盘一起动的那部分」——大盘涨你也涨,跟你选了什么关系不大;alpha 是「不跟大盘的那部分」,也就是扣掉市场之后,真正属于你的超出部分。这两个概念本身值得单独展开,可以看 beta 与 alpha。
不做归因最典型的错误,就是把 beta 记在自己账上。牛市里人人都是股神,这句玩笑话的技术含义是:牛市里 beta 那一份非常大,足以淹没掉所有人的水平差异。等市场停下来,beta 归零,剩下的才是你自己的东西——很多人第一次看清自己的水平,是在这个时候。
判断的第一道门槛,是先找一个合适的基准(跟你风格接近的一个指数或组合,用来当参照系),把它减掉。这一步的讲究不少,基准与超额 那篇专门谈这个。
把「押对了行业」当成「选股准」
这是更隐蔽的一层。假设你去年重仓某个行业,那个行业整体涨了 40%,你选的几只票涨了 35%。你的账户很好看,但真相是:你在那个行业里的选股是负贡献——闭着眼睛买这个行业的平均,反而比你挑的强。
你从这一年真正应该学到的经验是「我的行业判断可能有点东西」,而不是「我会挑票」。如果你搞反了,明年你会在一个自己没优势的地方加大投入。
把交易摩擦忘掉
买卖是要花钱的。手续费、印花税、买卖价差、下大单把价格推动带来的冲击——这些统称交易成本。它们在账户曲线上不会单独显示,只会让你的曲线整体矮一截。
一套年化超额 5% 的方法,如果换手很高(频繁买进卖出),成本很可能就吃掉三四个点。这时候归因表里的「成本」那一行,比选股那一行重要得多。这块的坑非常多,见 交易成本假设。
把运气当成能力
最后一份最难对付:运气不会在任何归因表里单独列出来。 它藏在每一项里面。你唯一能做的是问「这一项稳不稳」——它是靠很多笔小胜积累的,还是靠一两笔大赚顶起来的?换个时间段还在不在?
系统层:真实系统里的归因长什么样
Brinson 四象限:把「配置」和「选股」分开
qlib 里有一个专门做这件事的模块(qlib/backtest/profit_attribution.py)。开门见山说一句:这个文件的第一行注释写着它维护得不算好,所以下面讲的是它体现的方法本身,不是推荐你直接拿去用。方法比代码活得久,这也是归因这个话题的常态——它是个几十年的老方法。
它的做法叫 Brinson 归因,核心是一个二乘二的表格。你先把所有股票按某个维度分组(默认用行业,也可以用市值等其它字段,这些都是可配置参数),然后组装四个数:
Q1 = (bench_group_weight_df * bench_group_ret_df).sum(axis=1)
Q2 = (port_group_weight_df * bench_group_ret_df).sum(axis=1)
Q3 = (bench_group_weight_df * mod_port_group_ret_df).sum(axis=1)
Q4 = (port_group_weight_df * mod_port_group_ret_df).sum(axis=1)
看着像天书,翻成人话其实特别直白。每个数都是「每个组分配了多少钱 × 这个组挣了多少」再加总,区别只在于这两个东西各自用谁的:
| 用基准的组内收益 | 用你的组内收益 | |
|---|---|---|
| 用基准的组权重 | Q1:你什么都不做 | Q3:只有选股是你的 |
| 用你的组权重 | Q2:只有配置是你的 | Q4:你的真实结果 |
有了这四个数,功劳就能减出来:
"RAA": Q2 - Q1, # 配置贡献
"RSS": Q3 - Q1, # 选股贡献
"RIN": Q4 - Q3 - Q2 + Q1, # 两者的交互
"RTotal": Q4 - Q1, # 总超额
- RAA(配置) = 你把钱在各行业之间怎么摆,带来了多少超额。它假装你在每个行业里都只拿了这个行业的平均水平,这样就把「挑票」的影响剥掉了。
- RSS(选股) = 你在每个行业内部挑的那几只,比这个行业平均强多少。它假装你的行业权重跟基准一样,这样就把「押行业」的影响剥掉了。
- RIN(交互) = 一个容易被忽略但很有意思的项:你恰好在自己选股最强的那个行业里超配了。这份钱既不能算纯配置,也不能算纯选股,它是两者相乘出来的。
- 三项加起来正好等于 RTotal,这是个恒等式,不是巧合。
还有个实现细节值得看,因为它暴露了这套方法的一个隐含约定。如果某个行业你一股都没买,你在这个行业的收益是没有定义的,代码把它填成了基准在这个行业的收益:
mod_port_group_ret_df[mod_port_group_ret_df.isna()] = bench_group_ret_df
意思是「你没碰的地方,就当你拿了个平均」。这么处理是为了让配置项和选股项能干净分离——空仓一个行业属于配置决策,不该被记进选股账上。
换个维度分组,就变成风格归因
同一套框架,把「行业」换成别的字段,问的问题就完全不一样了。qlib 支持另一种分组方式:不按类别分,而是按数值切档。
split_points = np.percentile(bench_values[~bench_values.isna()], np.linspace(0, 100, group_n + 1))
注意 bench_values——分档的切点是拿基准的成分算的,不是拿你自己的持仓算的。 这一点是整个设计的关键:只有用同一把尺子,才谈得上「相对基准偏了多少」。
按市值切五档,你就能看出自己的钱是不是系统性地压在小盘上;按某个估值指标切档,你就能看出自己是不是一直在买便宜货。很多人以为自己在「精选个股」,归因一做才发现,自己八成的超额来自一个长期的风格暴露——那不是选股能力,那是在赌一种风格不要熄火。
成本单独占一行
qlib 的报告层还做了另一种拆法。风险分析表里并排两行:
analysis["excess_return_without_cost"] = risk_analysis(report_normal_df["return"] - report_normal_df["bench"])
analysis["excess_return_with_cost"] = risk_analysis(
report_normal_df["return"] - report_normal_df["bench"] - report_normal_df["cost"]
)
一行不扣成本,一行扣。两行之差,就是交易摩擦从你身上拿走的全部。 曲线图那边同样是成对画的:累计收益画两条(含成本、不含成本),累计超额也画两条,连回撤都分别算两遍。
这种「什么都画两遍」的做法看着啰嗦,用意很实在:让你一眼看见成本这一层有多厚。如果两条线越拉越开,说明问题不在你的判断,在你的换手频率上——那是 调仓频率 的事,改选股逻辑没用。
小账户版:按标签分组
上面那套需要成分股权重数据,个人很难拿到。freqtrade 走的是另一条更接地气的路——它不做数学分解,它做分组统计。
回测跑完,它会一口气产出好几张表:按品种一张,按入场标签一张(你给每种买入理由起的名字),按离场原因一张(止盈、止损、跟踪止损、信号反转、超时强平各算各的),还有一张是入场标签和离场原因交叉的。每张表都是同一个套路:按这个字段分组,每组算一行指标,最后追加一行 TOTAL。
for tags, group in results.groupby(tag_type):
tabular_data.append(_generate_result_line(group, min_date, max_date, starting_balance, tags))
它还能按日、按周、按月再拆一遍。
朴素,但威力不小。这几张表能直接回答一串你平时不敢问自己的问题:
- 去掉表现最好的那一个品种,整体还赚钱吗?(如果不赚,你做的其实是一笔集中押注,不是一套方法。)
- 你标了三种入场理由,哪一种真的赚钱?很可能只有一种在挣钱,另外两种一直在亏,只是被第一种盖住了。
- 你的钱是止盈拿到的,还是止损扛回来的?如果绝大部分利润来自「超时强平」这类兜底出口,说明你的离场规则基本没起作用,赚钱靠的是持有时间碰巧够长。
- 按月拆开,是每个月都赚一点,还是全年就靠某两个月?
归因表把总数分完了,但没有一项叫「运气」。 RAA + RSS + RIN 恒等于 RTotal,任何一分钱都会被归到某一项名下——包括纯粹撞上的那些。所以「选股贡献 +8%」这行字,本身不构成「我会选股」的证据。你还得追问:这 8% 是几百笔交易累出来的,还是三笔?换个年份还在不在?分组数量是不是多到总有几个能凑出好看的数?归因给的是分解,不是证明。
失效边界与常见误用
基准选错,整张表全废。 归因的每一个数都是相对基准算的。拿一个宽基指数去归因一套只做小盘股的方法,你会得到一个巨大的「配置贡献」,可那不是能力,那只是两个东西压根不可比。
分组维度决定了你能看见什么。 只按行业分组,风格暴露就完全隐形——你会把「一直押小盘」误读成选股能力。反过来,同时按太多维度切,每组只剩几只股票,得出的数噪音大于信号。
归因是描述,不是因果。 表里写「配置贡献 +5%」,说的是「按这套分组口径算出来是 5%」,不是「你的行业判断创造了 5%」。同一段业绩换个分组方式,功劳分布会明显不一样,这是这类方法的固有性质,不是 bug。
单期归因不能简单相加。 收益是乘起来的,归因项是每期分别算的,把多期的分解结果直接加总会和实际总超额对不上。上游代码给出的是逐日的分解序列,怎么跨期汇总是另一个问题,处理不好就会自己骗自己。
别只看归因,不看波动。 一样是 +5% 的超额,一路稳着来的和坐了两趟过山车的,价值完全不同。归因回答「钱从哪来」,风险指标回答「路上有多颠」,两个都得看。夏普这类比率在 夏普比率与索提诺比率 里有详细拆解。
把它当成打分工具。 归因最大的用处不是给自己发奖状,是决定下一步把精力花在哪。选股贡献长期为负,说明你该少挑票多买指数;成本吃掉一半超额,说明你该降换手而不是继续优化信号。它是一张诊断单,不是成绩单。
小结
- 账户涨了不等于你厉害。收益至少混着四股力量:市场整体、行业或风格配置、组内选股、交易成本,外加一份没法单列的运气。
- Brinson 那套二乘二的本质,是拿「基准权重/你的权重」和「基准收益/你的收益」交叉出四个数,再相减得到配置贡献、选股贡献和两者的交互项,三项相加恒等于总超额。
- 换个分组维度,同一套框架就从行业归因变成风格归因——很多所谓的选股能力,一分组就现出原形,是长期押着某种风格。
- 成本值得单独占一行。含成本与不含成本的两条曲线一拉开,问题出在判断上还是出在换手上,一眼就分明。
- 拿不到成分权重也能做:按品种、按入场理由、按离场原因、按月份分组各统计一遍,同样能看出你的钱到底集中在哪。
- 归因给的是分解不是证明。所有钱都会被分完,包括运气分到的那份;判断真假要靠样本量和跨期稳定性。
一句话记住这篇:先搞清楚这次的钱是谁挣的,再决定下次听谁的。
本文所引源码均来自上述快照版本,是阅读代码得出的机制说明,不是运行结果;具体行为请以你手上那个版本为准。回 量化与 AI 投研概念库 看其它概念各自在说什么;本站内容为投资教育,不构成任何投资建议,边界见 免责声明。
本文为投资教育整理,关键数据与结论请结合下列权威来源验证。