第 5 章 · 02 反转与价值:BAB、账面市值比、F-score 本节摘要:本节精读 quant-wiki docs/repo/static/strategies 下三个「非动量」脚本: (BAB,做多低 beta、做空高 beta,并把两腿都缩放到 beta=1)、 (HML 价值因子,按市净率 PB 排序分 5 组)、 (F-score 基本面 9 分制叠加短期反转,多「基本面强 + 短期跌」、空「基本面弱 + 短期涨」)。三者代表了「反转与价值」这一族系的三个支点:低风险异常(BAB)、价值溢价(HML)、基本面质量叠加均值回归(F-score+反转)。本节讲清每个脚本的信号构造、关键代码与陷阱,重点是 BAB 的「零 beta 组合」杠杆缩放与 F-score 的 9 分制定义。
本节摘要:本节精读 quant-wiki docs/repo/static/strategies 下三个「非动量」脚本:
betting-against-beta-factor-in-stocks.py(BAB,做多低 beta、做空高 beta,并把两腿都缩放到 beta=1)、value-book-to-market-factor.py(HML 价值因子,按市净率 PB 排序分 5 组)、combining-fundamental-fscore-and-equity-short-term-reversals.py(F-score 基本面 9 分制叠加短期反转,多「基本面强 + 短期跌」、空「基本面弱 + 短期涨」)。三者代表了「反转与价值」这一族系的三个支点:低风险异常(BAB)、价值溢价(HML)、基本面质量叠加均值回归(F-score+反转)。本节讲清每个脚本的信号构造、关键代码与陷阱,重点是 BAB 的「零 beta 组合」杠杆缩放与 F-score 的 9 分制定义。
内容来源:quant-wiki docs/repo/static/strategies,真实 Python 策略脚本,知识结构化整理。
⚠️ 学习提示:BAB 与价值因子都属于「反 CAPM 直觉」的异象——理论上高风险应有高收益,实证上却常是低风险/低估值股票长期跑赢。理解这些异象背后的行为金融解释(彩票偏好、杠杆约束),比记回测更重要。
阅读完本节,你应当能够:
动量是「强者恒强」,反转与价值则是「弱者回归」与「便宜者跑赢」。本节三个脚本分别从三个角度切入:
| 脚本 | 信号 | 经济逻辑 | 信号频率 |
|---|---|---|---|
| BAB | 个股对市场的 beta | 杠杆约束 + 彩票偏好 | 月频 |
| 账面市值比 HML | 市净率 PB | 风险补偿 + 行为偏差 | 月频(年频更新) |
| F-score + 反转 | 9 分基本面 + 1 个月反转 | 质量筛选 + 均值回归 | 月频(季频更新 F) |
💡 心法:价值因子是「慢变量」(财报每季度才更新一次),短期反转是「快变量」(每日价格波动)。F-score 把慢变量当作「方向筛选」,把快变量当作「择时触发」,这是「基本面 + 量价」叠加的经典范式。
CAPM 预测「beta 越高、预期收益越高」,但 Frazzini 与 Pedersen(2014)实证发现相反:风险调整后,低 beta 股票反而跑赢高 beta 股票。原因有二:(1) 杠杆约束——很多机构(养老金、共同基金)不能加杠杆,只能通过买高 beta 股票博取高收益,压低了高 beta 股的风险补偿;(2) 彩票偏好——散户偏爱高波动「彩票型」股票,推高其价格、压低其未来收益。
脚本用 1 年(12×21 日)滚动窗口,对每只股票回归其对 SPY 的收益,得到 beta:
market_returns = (market_closes[:-1] - market_closes[1:]) / market_closes[1:] stock_returns = (stock_closes[:-1] - stock_closes[1:]) / stock_closes[1:] cov = np.cov(stock_returns[::-1], market_returns[::-1])[0][1] market_variance = np.var(market_returns) beta[symbol] = cov / market_variance
注意这里用 np.cov(...)[0][1] / np.var(...) 而不是 linregress,效果等价但更直接。[::-1] 是因为 RollingWindow 的数据是倒序的(最新在前),回归前需要反转成正序。
BAB 最巧妙的设计在于「缩放到 beta=1」。脚本把股票按 beta 升序排,取最低 decile 做多、最高 decile 做空,然后:
long_mean_beta = np.mean([x[1] for x in self.long]) short_mean_beta = np.mean([x[1] for x in self.short]) self.long_lvg = 1 / long_mean_beta # 低 beta 组加杠杆 self.short_lvg = 1 / short_mean_beta # 高 beta 组去杠杆 # 杠杆上限 2 倍 self.long_lvg = min(self.leverage_cap, self.long_lvg) self.short_lvg = min(self.leverage_cap, self.short_lvg)
直觉:低 beta 组的 beta 比如 0.5,要让它达到 beta=1,就得加 2 倍杠杆;高 beta 组的 beta 比如 1.5,要让它达到 beta=1,就得只持 2/3 仓位。这样多空两腿对市场的净 beta 都为 1,组合整体「赌的不是市场方向,而是低/高 beta 的相对强弱」。
⚠️ 陷阱:杠杆缩放是 BAB 的灵魂,但也放大了对 beta 估计误差的敏感性。1 年回看窗口的 beta 估计本身噪声很大,极端值(比如 beta 接近 0)会导致
1/beta爆炸,所以脚本用leverage_cap = 2兜底。复现时务必检查 beta 的极端值处理。
HML(High Minus Low)是 Fama-French 三因子里的「价值因子」:做多高账面市值比(低 PB)的股票、做空低账面市值比(高 PB)的股票。脚本用 QC 提供的 ValuationRatios.PBRatio 作为排序键:
sorted_by_pb = sorted(top_by_market_cap, key=lambda x: x.ValuationRatios.PBRatio, reverse=False) quintile = int(len(sorted_by_pb) / 5) self.long = [i.Symbol for i in sorted_by_pb[:quintile]] # 最低 PB(最便宜) self.short = [i.Symbol for i in sorted_by_pb[-quintile:]] # 最高 PB(最贵)
| 要素 | 实现 |
|---|---|
| 宇宙 | 前 3000 大市值(NYSE/AMEX/NASDAQ) |
| 排序键 | ValuationRatios.PBRatio(PB,越低越便宜) |
| 分组 | 5 分位(quintile) |
| 多空 | 100% 多 + 100% 空(市场中性) |
| 再平衡 | 月末,但脚本里用 self.month 计数器只在每年第 12 月触发一次 |
⚠️ 陷阱:脚本里的
self.month计数器逻辑(12 月才设selection_flag = True,其余月份跳过)实际上是季度/年度再平衡的工程实现——价值因子信号变化慢(财报每季度才更新 PB),没必要每月调仓。这与动量脚本的「每月必调」形成对照,体现了信号频率与再平衡频率的匹配。
💡 迁移提示:HML 的核心是「按 PB 排序分 5 组」这一行。你可以把 PB 换成 PE、PS、PCF、股息率,得到不同的价值因子变体。Fama-French 原版还区分了「小盘价值」与「大盘价值」,脚本里简化成了全市场 HML。
F-score 来自 Piotroski(2000),用 9 个二元信号(每项 0 或 1 分)衡量公司的基本面强弱,总分 0-9:
| 类别 | 信号 | 计分条件 |
|---|---|---|
| 盈利能力 | ROA | ROA > 0 |
| 经营现金流 CFO | CFO > 0 | |
| ROA 变化 | ROA 同比上升 | |
| 应计项目 | CFO > ROA(盈利质量) | |
| 财务状况 | 杠杆 | 长期负债/总资产同比下降 |
| 流动性 | 流动比率同比上升 | |
| 股本发行 | 未发行新股 | |
| 经营效率 | 毛利率 | 毛利率同比上升 |
| 资产周转率 | 总资产周转率同比上升 |
脚本用 StockData 类缓存「去年同期」的指标做同比对比,逻辑严谨。最终把公司分为「基本面强」(F-score ≥ 7)、「中」(4-6)、「弱」(≤ 3)三档。
F-score 单独作为选股信号太弱(财报每季度才更新一次,变化慢),所以脚本叠加了 1 个月的短期反转(过去 21 日收益)作为触发器:
# 多: 基本面强 + 短期跌(价值反弹) self.long = [x[0] for x in score_performance.items() if x[1][0] >= 7 and x[1][1] < 0] # 空: 基本面弱 + 短期涨(泡沫回归) self.short = [x[0] for x in score_performance.items() if x[1][0] <= 3 and x[1][1] > 0]
这一叠加体现了「基本面定方向、量价定时点」的范式:只在「基本面好但短期被错杀」的股票上做多,只在「基本面差但短期被炒作」的股票上做空。
这个脚本的 FineSelectionFunction 有一长串数据非空过滤(ROA、CFO、长期负债、总资产、流动比率、股本数、毛利率、营收等),任何一项缺失就剔除该股。这反映了基本面策略的痛点——财报字段缺失很常见,不严格过滤会导致 F-score 计算错误。
💡 心法:F-score + 反转的组合,本质是「质量因子 × 反转因子」的交互项。交互项的好处是降低了单纯反转的「价值陷阱」风险(基本面烂的股票短期跌可能是永久性下跌,不该做多)。
| 维度 | BAB | HML | F-score+反转 |
|---|---|---|---|
| 信号类型 | 风险因子(beta) | 价值因子(PB) | 质量×反转 |
| 信号频率 | 月频 | 年频 | 月频(季频更新 F) |
| 关键工程 | 零 beta 杠杆缩放 | PB 排序分 5 组 | 9 分计分 + 同比对比 |
| 主要陷阱 | beta 估计噪声 | PB 极端值 | 财报字段缺失 |
| 经济逻辑 | 杠杆约束 | 风险补偿 | 质量 + 均值回归 |
⚠️ 共通陷阱:三个脚本都依赖 QC 的
FineFundamental数据。复现到自己的框架时,财报数据的「时点性」(point-in-time)是最大坑——你不能用财报发布日之后才更新的数据回测当时的信号,否则会引入前视偏差。BAB 脚本因为只用价格数据,反而最容易复现。
杠杆 = 1/组内平均 beta),杠杆上限 2 倍。下一节,我们看统计套利的核心——配对交易与 ETF 配对:协整、价差均值回归、配对切换。