价值因子完整回测 — B/M, E/P, CF/P
价值因子三种主流 metric(B/M / E/P / CF/P)的差异与取舍 + Asness 2013 跨资产价值证据 + 2014-2020 失落十年成因辩论
日期: 2026-05-20 方向: 因子投资 / 价值 阶段: Phase 1: 基础与工具链 标签: #Value #BookToMarket #EarningsYield #CashflowYield #Asness2013 #LostDecade
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | 价值因子三种主流 metric(B/M / E/P / CF/P)的差异与取舍 + Asness 2013 跨资产价值证据 + 2014-2020 失落十年成因辩论 |
| 实操 | 用 yfinance 取 SP500 子集财报,构造月度 value portfolio,2014-2024 回测,与 Day 10 momentum 对比 |
| 产出 | TR-DAY11 笔记 + 可运行 value 回测脚本 + value vs momentum 相关性分析 |
一、价值因子的核心思想:为什么「便宜」会赢
价值投资的直觉来自 Graham 1934:股票是某种现金流权利的凭证,它的内在价值(intrinsic value)由这些现金流的折现决定。当市场给出的报价 < 内在价值时,你就以折扣价拿到了未来现金流——长期看,价格会向内在价值收敛,你从这个折扣中获利。
但这个故事有个尴尬的事实:没有人能精确算出 intrinsic value。所以学术界和量化界做了一个简化:用便宜的 proxy 代替 intrinsic value,然后系统性地按这个 proxy 排序股票。
| 比较方法 | 直觉 | 学术界视角 | 量化界视角 |
|---|---|---|---|
| DCF 估值 | 现金流贴现到今天 | 理论正确但参数敏感 | 不可量化,弃用 |
| 可比公司估值 | 看同行多少倍 PE | 主观选择同行 | 不可系统化,弃用 |
| multiple 排序 | 全市场 PE 由低到高排 | Fama-French 用的就是这个 | ✓ 可批量化,这就是 value factor |
核心认知:value factor 不是「我会算估值」,而是「截面上便宜的股票平均比贵的股票表现好」。它放弃了精确性,换来了可批量化的统计规律。这就是 Fama-French 1993 三因子模型里 HML(High Minus Low B/M) 的逻辑。
为什么「便宜」会赢的两派解释
学术界打了 30 年仗也没完全打完:
| 流派 | 解释 | 暗含假设 |
|---|---|---|
| Risk-based(Fama-French) | value 股是 distressed 公司,承受了额外风险,所以应该有 risk premium | 市场是有效的,超额收益对应额外风险 |
| Behavioral(Lakonishok, Shleifer, Vishny 1994) | 投资者过度外推,把过去差的公司错杀,便宜了 | 市场有系统性偏差 |
| Institutional(Asness) | career risk → 基金经理不敢买被骂的股票 → 价格被压 | 市场结构性偏差 |
对我们的实操意义:不管哪派对,做法是一样的——按便宜程度排序,做多便宜端,做空(或不持有)贵端。但风险派暗含一个警告:value 跑赢不是免费午餐,它伴随着 distress risk,所以会有较长时间 underperform(这就是 2014-2020 失落十年的伏笔)。
二、三种主流 value metric:B/M, E/P, CF/P
2.1 B/M:Book-to-Market
公式:B/M = Book Value of Equity / Market Cap
- Book Value 来自资产负债表(净资产 = 总资产 - 总负债 - 优先股)
- Market Cap 是当前股价 × 流通股数
- B/M > 1:账面价值 > 市值(市场给折扣)
- B/M < 1:账面价值 < 市值(市场给溢价)
为什么 Fama-French 1993 用它:
- Book Value 比 Earnings 稳定(不会因为单季亏损翻负)
- 季度披露及时性较好
- 历史回看,HML 因子在 1963-1990 美股有显著超额收益(年化约 5-6%)
问题:
- 会计价值 ≠ 经济价值:商誉、品牌、知识产权、用户基础这些「无形资产」在 GAAP 里要么不计入要么按历史成本计入。Apple 的 book value 远小于市值,但它真的「贵」吗?这正是科技股时代 B/M 失效的核心原因。
- 行业偏差:金融、公用事业天然 high B/M(资产重),科技、医药天然 low B/M(资产轻)。直接按 B/M 排序 = 重仓金融股 + 弃投科技股,这是 2014-2020 灾难的来源。
2.2 E/P:Earnings Yield
公式:E/P = Net Income (TTM) / Market Cap = 1 / P/E
- 直觉:每 $1 市值能换多少利润
- E/P = 5% ≈ P/E = 20
- 通常用 trailing 12-month(TTM)net income,避免分析师 bias
优点:
- 直接反映「这家公司每年赚多少」
- 跨行业可比性比 B/M 强一些
- 容易解释(PE 是大众语言)
缺点:
- 负盈利怎么办:很多创业期公司、周期低谷期公司净利润为负,E/P < 0 不能直接进入排序。常见处理:
- 排除负 E/P(损失信息)
- 把负 E/P 都赋值为 0(更糟,因为 0 看起来比 +0.01 更便宜)
- 转换为 P/E 然后取倒数:负 P/E → 排在最贵端(合理)
- 用 winsorize(截尾):把极端值压缩到 1st/99th 百分位
- 盈利可被操纵:折旧加速/减速、应收账款 timing、一次性损益——所有这些都让 net income 比 cash flow 更容易被「美化」。
2.3 CF/P:Cash Flow Yield
公式:CF/P = Operating Cash Flow (TTM) / Market Cap
- 用 OCF 而不是 net income
- OCF 来自现金流量表,反映真正流入的钱
为什么 CF/P 在最近 20 年的 robustness 上 > E/P > B/M:
| 维度 | B/M | E/P | CF/P |
|---|---|---|---|
| 抗会计操纵 | 中 | 弱 | 强 |
| 跨行业可比 | 弱(行业偏差大) | 中 | 中 |
| 数据获取难度 | 易 | 易 | 中(cash flow statement) |
| 覆盖创业期公司 | 弱(book 低) | 弱(earnings 负) | 较好(OCF 比 NI 早转正) |
| 在科技股时代有效性 | 大幅失效 | 中等失效 | 相对稳健 |
| Asness(AQR)的偏好 | 历史经典但已弱化 | 中等 | 首选 |
Asness 2019「The Long Run is Lying to You」 反思过:经典 HML(B/M)已经在某种程度上「死了」,但用 cash-based metric(CF/P, EV/EBITDA, FCF yield)的 value factor 仍然有效。这是一个重要的 nuance:「价值死了」更准确的说法是「B/M 死了」。
2.4 实操选择
| 场景 | 推荐 metric |
|---|---|
| 学术复现 Fama-French | B/M |
| 单 metric 投研,覆盖大盘 | CF/P |
| 做不可知论组合 | B/M + E/P + CF/P 等权重 z-score |
| 买入散户能听懂的产品 | E/P(PE 倒数,营销友好) |
我们今天的回测路径:先做 B/M 复现 Fama-French 经典版本看它「失落十年」长什么样,再做 CF/P 看是否更稳健。
三、Point-in-Time(PIT)问题:未来函数的杀手
回测里最隐蔽的 bug 来源是未来函数——你在回测中用了 t 时刻还不该知道的信息,导致历史业绩被人为提升。Value factor 是未来函数的重灾区,因为它依赖财报数据,而财报有两个关键日期:
| 日期 | 含义 |
|---|---|
| Fiscal Period End(财年/财季结束) | 比如 2023-12-31 是 FY2023 Q4 末 |
| Filing Date / Announcement Date(披露日) | 财报真正公开的日期,通常滞后 30-90 天 |
错误做法:在 2024-01-01 用 FY2023 Q4 的财报数据。 正确做法:等到 announcement date(比如 2024-02-15)之后,才能在该 ticker 的 universe 里使用这份数据。
| 错误 | 后果 |
|---|---|
| 用 fiscal end + 1 day 就开始用财报 | 比真实早 30-90 天知道盈利信息 → 显著高估回测收益 |
| 用 calendar quarter end 当 announcement | 季报 announcement 集中在月末后 4-6 周,差异巨大 |
| 用 last reported value 不滚动 | 旧数据 stale,但至少不未来函数 |
严肃量化的解决方案:
- 数据源支持 PIT(Sharadar SF1, Polygon Financials, Compustat PIT, FactSet PIT)— 每条记录带 announcement date
- 自建 lag:保守地把 fiscal end 加 90 天作为 announcement proxy(今天我们用这招)
yfinance 的局限:
- yfinance 拿到的财报是「最新版本」,没有 announcement date 字段
- 季报数据完整性参差,有时只有近 4 季
- restatements(财报重述)后的数据会替换原始数据 → look-ahead bias
对策略:
- 学习/原型期:yfinance + 90-day lag 就够(精度可以容忍)
- 实盘 / 严肃回测:必须上 Sharadar 或 Polygon Financials(约 $50-200/月)
四、Asness, Moskowitz, Pedersen 2013:Value and Momentum Everywhere
这是过去 15 年因子投资最重要的论文之一。AMP 三人在 Journal of Finance 上发表后,重塑了大型机构对 value 和 momentum 的理解。
4.1 核心发现
他们在 8 个市场 / 资产类别同时检验 value 和 momentum:
- 美股、英股、欧股、日股
- 政府债券(10y)
- 商品期货
- 外汇
结论:
- Value 和 Momentum 在每个市场都有 positive premium(不是美股独有)
- Value 和 Momentum 在所有市场都呈 negative correlation(约 -0.4 到 -0.6)
- 因此 value + momentum 50/50 组合的 Sharpe 远高于单因子(约 1.5x)
- 跨资产 value 因子之间正相关 → 存在全球 value 风险因子
- 跨资产 momentum 因子之间也正相关 → 存在全球 momentum 风险因子
4.2 为什么 value 和 momentum 负相关
| 直觉 | 解释 |
|---|---|
| 时间尺度不同 | momentum 是 3-12M 短期 trend,value 是基本面 mean-reversion,两者本质相反方向 |
| 投资者画像不同 | momentum 跟 retail/trend follower,value 跟 fundamental analyst |
| 经济周期解释 | 在某些 regime 下 value 强(复苏期),另一些下 momentum 强(扩张/泡沫期) |
4.3 对我们的影响
今天 Day 11 单做 value,明天 Day 12 单做 low-vol,下周 Day 14 我们会专题做 value + momentum 组合——根据 AMP 2013,这个组合的 Sharpe 应该比 Day 10 的 momentum-only 显著提升。
操作启示:
- 不要纠结「value 在 2014-2020 不行」就放弃 value
- value 在组合里的作用是 diversifier,不是 standalone alpha source
- 即便 value 单独 Sharpe = 0.3,加进 momentum 组合后边际贡献可能让组合 Sharpe 从 0.8 → 1.1
五、Value 的失落十年(2014-2020):发生了什么
5.1 数据有多惨
Russell 1000 Value vs Russell 1000 Growth 2014-01-01 到 2020-12-31:
| 指标 | Russell 1000 Value | Russell 1000 Growth | 差距 |
|---|---|---|---|
| 累计收益 | +63% | +245% | -182% |
| 年化收益 | +7.2% | +19.5% | -12.3% |
| 最大回撤 | -27% | -19% | - |
直观感受:你的同事每年问你「你做的什么策略?」你说 value,结果他买 QQQ 七年回报你三倍——这种心理压力让大量基金经理在 2020 年放弃了 value。
5.2 成因辩论
| 假说 | 证据 | 反对意见 |
|---|---|---|
| ① 科技股结构性变化 | FAANG 的 cash flow 像 utility 一样稳定,但 GAAP earnings 被研发费用低估 | 这是「value metric 坏了」,不是「value 概念坏了」 |
| ② 低利率支撑长 duration 资产 | 2014-2021 利率从 2.5% → 0%,长期 DCF 中 growth stock 现值大幅上升 | 但 2022 利率快速回升,growth 应该崩,结果只崩了一年 |
| ③ 量化 crowding | AQR / GMO 等 value 大基金管理资产规模过大,alpha 被吃掉 | 但 2020-2021 散户 retail 才是主导,机构 crowding 不是核心 |
| ④ Value 已 priced in | 学术界发现因子后,市场会套利消失 | 但 momentum 同样被发现,仍然有效 |
| ⑤ Active manager 行为 | 价值经理被赎回 → 被迫卖 value 股 → value 股越跌越多 | 部分解释,但不充分 |
最有说服力的综合解释:①+② 的组合——科技股的特性使 B/M 这个 metric 系统性低估了它们的真实价值,叠加低利率让它们估值进一步抬升,于是「便宜的(金融、能源、零售)」继续便宜了七年。
5.3 2021 年开始的反弹
- 2021 年通胀回归,能源股大涨(XLE +55%),价值股跑赢成长股 ~10%
- 2022 年加息,成长股大跌,价值股相对抗跌
- 2023-2024 AI 热潮回到成长股
- 整体看:2021-2024 部分追回但远未填平 2014-2020 缺口
对 PM 的启示:因子投资必须能熬住因子的回撤期。如果你给客户产品的设计不能承受 7 年 underperformance(很可能不能),就不要单押一个因子。这是为什么所有严肃量化产品都是 multi-factor。
六、完整代码:SP500 子集 value 月度回测
6.1 设计
| 项 | 选择 |
|---|---|
| Universe | SP500 子集(同 Day 10,约 50 只大盘股) |
| Metric | Book-to-Market (1/PB) + Earnings Yield (1/PE) + CashFlow Yield (1/P/CFO) |
| 排序方式 | 月末截面 z-score 综合 |
| 持仓方式 | Top decile(top 10%)等权 long-only |
| 再平衡 | 月度(每月最后交易日) |
| 财报 lag | 90 天保守 lag(fiscal end + 90d 才可用) |
| 回测期 | 2014-01-01 to 2024-12-31 |
| 成本 | 单边 5bp(同 Day 10) |
| 基准 | SPY(大盘) + VUG(成长) + VTV(价值 ETF) |
6.2 代码
# tr_day11_value_factor.py
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import timedelta
# ---------- 1. Universe ----------
TICKERS = [
'AAPL', 'MSFT', 'GOOGL', 'AMZN', 'NVDA', 'META', 'TSLA', 'BRK-B',
'JPM', 'JNJ', 'V', 'PG', 'XOM', 'UNH', 'HD', 'MA', 'LLY', 'AVGO',
'CVX', 'ABBV', 'PEP', 'KO', 'BAC', 'PFE', 'COST', 'WMT', 'TMO',
'CSCO', 'MRK', 'ACN', 'MCD', 'ADBE', 'CRM', 'ABT', 'NFLX', 'NKE',
'DIS', 'AMD', 'TXN', 'WFC', 'ORCL', 'PM', 'NEE', 'INTC', 'IBM',
'CAT', 'GS', 'UPS', 'BA', 'GE',
]
START = '2013-06-01' # 多取半年用于 90 天 lag
END = '2024-12-31'
# ---------- 2. Price data ----------
print("Downloading prices...")
prices = yf.download(TICKERS, start=START, end=END, auto_adjust=True)['Close']
prices = prices.dropna(axis=1, how='all')
month_end = prices.resample('M').last()
month_ret = month_end.pct_change()
# ---------- 3. Fundamentals ----------
# yfinance 给的是「最新版」财报,没有 announcement date。
# 我们用 fiscal end + 90 天作为可用日期。
def fetch_fundamentals(ticker):
"""Pull quarterly income/balance/cashflow and shares; return long DataFrame."""
try:
t = yf.Ticker(ticker)
bs = t.quarterly_balance_sheet.T # rows = fiscal end, cols = items
is_ = t.quarterly_financials.T
cf = t.quarterly_cashflow.T
# 关键字段(yfinance 字段名 2024 后多次改动,做容错)
def pick(df, keys):
for k in keys:
if k in df.columns:
return df[k]
return pd.Series(index=df.index, dtype=float)
book = pick(bs, ['Stockholders Equity', 'Total Stockholder Equity', 'Common Stock Equity'])
ni = pick(is_, ['Net Income', 'Net Income Common Stockholders'])
ocf = pick(cf, ['Operating Cash Flow', 'Total Cash From Operating Activities'])
df = pd.DataFrame({'book': book, 'ni': ni, 'ocf': ocf})
df.index = pd.to_datetime(df.index)
df = df.sort_index()
# TTM net income 和 TTM OCF(4 季度滚动加和)
df['ni_ttm'] = df['ni'].rolling(4).sum()
df['ocf_ttm'] = df['ocf'].rolling(4).sum()
df['ticker'] = ticker
df['available_date'] = df.index + pd.Timedelta(days=90) # 保守 lag
return df.reset_index().rename(columns={'index': 'fiscal_end'})
except Exception as e:
print(f" {ticker}: {e}")
return pd.DataFrame()
print("Fetching fundamentals (slow, ~1-2 min)...")
fund_list = [fetch_fundamentals(t) for t in TICKERS]
fund = pd.concat([f for f in fund_list if not f.empty], ignore_index=True)
print(f"Fundamentals rows: {len(fund)}")
# ---------- 4. Build PIT panel ----------
# 在每个月末,对每个 ticker 找最近一份「available_date <= month_end」的财报
def build_pit_panel(month_end_dates, fund, prices):
rows = []
for date in month_end_dates:
for ticker in TICKERS:
if ticker not in prices.columns:
continue
f = fund[(fund['ticker'] == ticker) & (fund['available_date'] <= date)]
if f.empty:
continue
latest = f.sort_values('available_date').iloc[-1]
mcap = prices.loc[:date, ticker].iloc[-1] # 用价格 proxy 市值(缺失股本数据用 P 代替)
# 注意:我们没有 shares outstanding 历史数据;
# 这里取一个简化:用 PB / PE / P/CFO 的倒数作为 yield
# 严格做法是 BookValuePerShare / Price 或 NIperShare / Price
# 这里直接用 latest['book'] / (price * shares) 时 shares 难拿,做近似处理
# → 简化:用 yfinance 当前 info 的 shares 当 proxy(牺牲精度换可运行性)
rows.append({
'date': date, 'ticker': ticker,
'book': latest['book'], 'ni_ttm': latest['ni_ttm'], 'ocf_ttm': latest['ocf_ttm'],
'price': mcap,
})
return pd.DataFrame(rows)
month_end_dates = month_end.index
panel = build_pit_panel(month_end_dates, fund, prices)
# 简化:用「最新 shares outstanding」近似过去(确实有偏差,但量级正确)
shares_now = {}
for t in TICKERS:
try:
info = yf.Ticker(t).info
shares_now[t] = info.get('sharesOutstanding', np.nan)
except Exception:
shares_now[t] = np.nan
panel['shares'] = panel['ticker'].map(shares_now)
panel['mcap'] = panel['price'] * panel['shares']
panel['BM'] = panel['book'] / panel['mcap']
panel['EP'] = panel['ni_ttm'] / panel['mcap']
panel['CFP'] = panel['ocf_ttm'] / panel['mcap']
# ---------- 5. Cross-sectional z-score and combined value score ----------
def zscore(s):
s = s.replace([np.inf, -np.inf], np.nan)
# winsorize 1st/99th
lo, hi = s.quantile(0.01), s.quantile(0.99)
s = s.clip(lo, hi)
return (s - s.mean()) / s.std()
panel['z_BM'] = panel.groupby('date')['BM'].transform(zscore)
panel['z_EP'] = panel.groupby('date')['EP'].transform(zscore)
panel['z_CFP'] = panel.groupby('date')['CFP'].transform(zscore)
panel['value_score'] = panel[['z_BM', 'z_EP', 'z_CFP']].mean(axis=1)
# ---------- 6. Top decile long-only ----------
def top_decile(df):
threshold = df['value_score'].quantile(0.9)
df['weight'] = (df['value_score'] >= threshold).astype(float)
df['weight'] = df['weight'] / df['weight'].sum() if df['weight'].sum() > 0 else 0
return df
panel = panel.groupby('date', group_keys=False).apply(top_decile)
# ---------- 7. Backtest ----------
weights = panel.pivot(index='date', columns='ticker', values='weight').fillna(0)
common = weights.columns.intersection(month_ret.columns)
weights = weights[common]
ret = month_ret[common]
# 月度收益
weights_lag = weights.shift(1).fillna(0) # 月初的权重 = 上月末决定的
port_ret = (weights_lag * ret).sum(axis=1)
# 成本:当月换手 × 5bp
turnover = (weights - weights.shift(1).fillna(0)).abs().sum(axis=1) / 2
port_ret_after_cost = port_ret - turnover * 0.0005
# ---------- 8. Benchmarks ----------
benches = yf.download(['SPY', 'VUG', 'VTV'], start='2014-01-01', end=END, auto_adjust=True)['Close']
bench_ret = benches.resample('M').last().pct_change()
# ---------- 9. Metrics ----------
def metrics(r, name):
r = r.dropna()
cum = (1 + r).cumprod()
cagr = cum.iloc[-1] ** (12 / len(r)) - 1
sharpe = r.mean() / r.std() * np.sqrt(12)
mdd = (cum / cum.cummax() - 1).min()
return pd.Series({'name': name, 'CAGR': cagr, 'Sharpe': sharpe, 'MDD': mdd})
results = pd.DataFrame([
metrics(port_ret_after_cost.loc['2014':], 'Value LO (after cost)'),
metrics(bench_ret['SPY'].loc['2014':], 'SPY'),
metrics(bench_ret['VUG'].loc['2014':], 'VUG (Growth)'),
metrics(bench_ret['VTV'].loc['2014':], 'VTV (Value ETF)'),
])
print(results.to_string(index=False))
# ---------- 10. Plot ----------
fig, axes = plt.subplots(2, 1, figsize=(12, 8))
cum_value = (1 + port_ret_after_cost.loc['2014':]).cumprod()
cum_spy = (1 + bench_ret['SPY'].loc['2014':]).cumprod()
cum_vug = (1 + bench_ret['VUG'].loc['2014':]).cumprod()
cum_vtv = (1 + bench_ret['VTV'].loc['2014':]).cumprod()
axes[0].plot(cum_value, label='Value LO (our portfolio)')
axes[0].plot(cum_spy, label='SPY', alpha=0.7)
axes[0].plot(cum_vug, label='VUG (Growth)', alpha=0.7)
axes[0].plot(cum_vtv, label='VTV (Value ETF)', alpha=0.7)
axes[0].set_title('Cumulative Return: Value vs Growth vs SPY (2014-2024)')
axes[0].legend()
axes[0].grid(alpha=0.3)
# 12M rolling spread: VTV - VUG
roll_vtv = bench_ret['VTV'].rolling(12).apply(lambda x: (1+x).prod() - 1)
roll_vug = bench_ret['VUG'].rolling(12).apply(lambda x: (1+x).prod() - 1)
spread = roll_vtv - roll_vug
axes[1].plot(spread, color='steelblue')
axes[1].axhline(0, color='black', linewidth=0.7)
axes[1].fill_between(spread.index, 0, spread, where=spread > 0, alpha=0.3, color='green', label='Value wins')
axes[1].fill_between(spread.index, 0, spread, where=spread < 0, alpha=0.3, color='red', label='Growth wins')
axes[1].set_title('Rolling 12M Return Spread: VTV - VUG')
axes[1].legend()
axes[1].grid(alpha=0.3)
plt.tight_layout()
plt.savefig('value_vs_growth.png', dpi=120)
print("Saved: value_vs_growth.png")
6.3 代码里的工程妥协说明
我在代码里做了几个有意识的妥协,必须明说,不能装作没事:
| 妥协 | 真实做法 | 影响 |
|---|---|---|
| 用「最新 shares outstanding」当历史每月 shares | 应该用 historical shares from quarterly filing | 回购大的公司会被低估市值(早期 mcap 实际更小,B/M 应更高)。对结果方向无影响,幅度有 5-15% 偏差。 |
| 用 fiscal end + 90 天 lag | 应该用真实 announcement date | 多数公司 announcement 在 30-45 天内,我们多 lag 了 1-2 个月,反而保守了(不会高估业绩) |
| yfinance 的 quarterly 财报只有 ~4-8 季度历史 | 应该用 Sharadar SF1 历史完整 | 早期月份(2014-2015)部分 ticker 没数据 → universe 被截短 |
| 没做行业中性化 | Fama-MacBeth 应该按 GICS 行业 z-score | 我们的 portfolio 会重仓金融能源,淡科技 → 这正是想看的 lost decade 效果 |
| 没把分红再投资进 mcap 分母 | 严格的 PB 应该是 ex-dividend | 偏差 1-3%,可忽略 |
为什么允许这些妥协:今天目标是「学到 value 因子的方向感和坑」,不是「发表论文级别精确度」。如果上线实盘,所有这些都必须用 PIT 数据源修正。
七、预期结果与解读
7.1 预期数字(基于历史 lost decade 模式)
| 指标 | Value LO | SPY | VUG | VTV |
|---|---|---|---|---|
| 2014-2024 CAGR | 8-10% | ~12% | ~14% | ~9% |
| Sharpe | 0.55-0.7 | ~0.85 | ~0.85 | ~0.6 |
| MDD | -32% 左右 | -24% | -32% | -27% |
注:实际数字会因 universe 选择波动,但方向应一致:value 显著弱于 SPY 和 VUG,与 VTV 接近。
7.2 怎么解读
如果你看到这个数字,第一反应可能是「value 这么烂,下次别学了」。这个反应是错的。正确的解读:
- 单因子 standalone 的 Sharpe 在 0.3-0.7 是正常的——不是因子失效,是单因子本来就不如组合
- value 的真正价值在 multi-factor 组合里——它和 momentum 负相关,是 diversifier
- 这十年的 underperformance 不代表未来十年——金融周期会均值回归,2021-2022 我们已经看到迹象
- 对 PM 的启示:如果你给客户卖 value-only 产品,客户会在第三年赎回。所以产品设计上 value 必须包在 multi-factor 里卖
7.3 与 Day 10 momentum 的对比
预期 momentum vs value 的相关性:
| 维度 | Momentum (Day 10) | Value (Day 11) | 组合(50/50) |
|---|---|---|---|
| 单因子 Sharpe | ~0.7 | ~0.5 | ~0.85 |
| MDD | -28% | -32% | -22% |
| 与 SPY 相关性 | 0.85 | 0.75 | 0.80 |
| 互相相关 | -0.3 to -0.5 | - | - |
核心数字:组合 Sharpe ≈ 0.85,比单因子最强者(momentum 0.7)还高,MDD 更小。这就是 AMP 2013 的实证主张。Day 14 我们专题做这个组合。
八、常见坑清单
写在这里是为了一年后再看时不犯同样的错:
- trailing PE vs forward PE:forward PE 用的是分析师预测,存在 selection bias(分析师倾向于乐观),且数据滞后于 announcement。用 trailing。
- 负 E/P 排除还是保留:负盈利公司在排序里如果保留会偏向「赢者诅咒」(最便宜的全是亏损股)。建议直接排除负 E/P 公司,但保留负 NI 公司在 CF/P 排序里(OCF 可能为正)。
- 行业偏差:直接 B/M 排序会重仓金融、公用事业、能源。学术做法是 Fama-MacBeth 行业中性回归,工业做法是按 GICS 行业内部 z-score 后再合并。今天没做,明确记账。
- 财报披露日期 vs 财年结束日期:用前者,但 yfinance 给不出,所以用 fiscal_end + 90d 保守 proxy。
- 股本变化:回购、增发都会改变 shares outstanding。yfinance 只给最新值,回测早期会偏差。严肃做法用 Sharadar 的 historical share count。
- ADR / dual-listing 重复:如果 universe 包括 BABA 和 9988.HK,会重复。我们 SP500 子集没这问题,但跨市场要注意。
- Survivorship bias:SP500 现在的成分不等于 2014 年的成分。Day 10 我们就遇到过这个问题。严肃做法用 historical SP500 constituents。
- Value trap(价值陷阱):「便宜的有便宜的理由」——很多 high B/M 公司是行业死亡前夜(如 2007 年的报纸、2018 年的零售)。改进方法:
- 加 quality 因子过滤(ROE、利润率)
- 加 momentum 反向过滤(剔除 12M 跌得最惨的)
- 加财务健康过滤(debt/equity, interest coverage)
- Look-back length:用 TTM 4 季度还是用 latest single quarter?前者平滑但滞后,后者敏感但 noise 大。TTM 是行业惯例。
- 数据源 restatement:公司发现历史财报错了会重新公布。yfinance 拿到的是 restated 数据 → look-ahead bias。严肃做法只能用 PIT 数据库。
九、PM 视角:「便宜买入」的迁移
我做产品 10 年,「价值因子」的逻辑在金融零售业务里反复出现,只是名字不同:
9.1 收购 / M&A
「现在用户增长慢、估值低的 SaaS 公司收来整合」 = 公司层面的 value 投资。
- 同样的陷阱:便宜可能是因为产品已死,不是被错杀。
- 同样的 quality 过滤:净利率、用户留存、技术债务——这些是 SaaS 的 quality factor。
9.2 股权激励 / 期权定价
「ISO/NSO 行权价低于市场价 = 员工拿到 value 折扣」 = 员工 portfolio 里的 value position。
- 同样的 lost decade 风险:行权价基于授予时估值,但科技股可以连续 5 年下跌,员工的 value option 可能从 OTM 一直 OTM。
- 设计启示:grant schedule 要 multi-vintage 摊薄,等同 multi-factor 多因子分散。
9.3 客户获取成本(CAC)
「LTV/CAC > 3 的渠道继续投,< 1 的砍掉」 = 渠道层面的 value 排序。
- 同样的截面排序:把所有渠道按 LTV/CAC 排,砍后 30%。
- 同样的 lag 问题:LTV 要 12 个月才能看清,CAC 当月可知 → 早期决策也会用滞后数据,这就是 PIT 问题在产品里的表现。
9.4 投资组合心态
我做 PM 的人最大的危险是:职场风险厌恶。和 value manager 在 2020 被赎回一个原理——你做了「正确但当下不流行」的决定(比如 push 长期 ROI 不性感的产品改造),就会被高层认为「不进取」。
对策(学因子投资学到的):
- 拿数据说话,多 metric 综合(不是只一个 KPI)
- 把决策包在 multi-factor 里(不要只做一件事 standalone)
- 解释清楚因子的 underperformance 期是设计的一部分,不是失败
十、明日预告
Day 12: 低波动因子 — Frazzini-Pedersen Betting Against Beta
- 低 beta 股票为什么长期跑赢 high beta(Frazzini-Pedersen 2014)
- BAB(Betting Against Beta)因子的构造:market-neutral leveraged low-beta long + high-beta short
- 实操:用 SP500 子集计算 60 日 rolling beta,构造 low-vol portfolio
- 与 value、momentum 的相关性
- 为什么低 beta anomaly 是「leverage constraint」造成的
- PM 视角:稳定客群 vs 增长客群的 portfolio 构造
实际执行记录
启动一项填一项,时间戳 + 卡点。
- [hh:mm] 跑通 yfinance 拉财报 — 卡点:…
- [hh:mm] 跑通 PIT panel 构造 — 卡点:…
- [hh:mm] 跑通月度回测 — 结果:CAGR=…, Sharpe=…, MDD=…
- [hh:mm] 与 SPY/VUG/VTV 对比 — 与预期偏差…
- [hh:mm] value vs Day 10 momentum 相关性 — 实测 corr=…
- 学到的 / 卡过的:
总字数:约 7,200 字 今日完成度:理论 ✓ / 实操(你自己执行)/ 笔记 ✓