返回交易笔记
TR Day 11

价值因子完整回测 — B/M, E/P, CF/P

价值因子三种主流 metric(B/M / E/P / CF/P)的差异与取舍 + Asness 2013 跨资产价值证据 + 2014-2020 失落十年成因辩论

2026-05-20
Phase 1: 基础与工具链
ValueBookToMarketEarningsYieldCashflowYieldAsness2013LostDecade

日期: 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 用它

  1. Book Value 比 Earnings 稳定(不会因为单季亏损翻负)
  2. 季度披露及时性较好
  3. 历史回看,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/ME/PCF/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-FrenchB/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,但至少不未来函数

严肃量化的解决方案

  1. 数据源支持 PIT(Sharadar SF1, Polygon Financials, Compustat PIT, FactSet PIT)— 每条记录带 announcement date
  2. 自建 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)
  • 商品期货
  • 外汇

结论

  1. Value 和 Momentum 在每个市场都有 positive premium(不是美股独有)
  2. Value 和 Momentum 在所有市场都呈 negative correlation(约 -0.4 到 -0.6)
  3. 因此 value + momentum 50/50 组合的 Sharpe 远高于单因子(约 1.5x)
  4. 跨资产 value 因子之间正相关 → 存在全球 value 风险因子
  5. 跨资产 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 ValueRussell 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 应该崩,结果只崩了一年
③ 量化 crowdingAQR / 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 设计

选择
UniverseSP500 子集(同 Day 10,约 50 只大盘股)
MetricBook-to-Market (1/PB) + Earnings Yield (1/PE) + CashFlow Yield (1/P/CFO)
排序方式月末截面 z-score 综合
持仓方式Top decile(top 10%)等权 long-only
再平衡月度(每月最后交易日)
财报 lag90 天保守 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 LOSPYVUGVTV
2014-2024 CAGR8-10%~12%~14%~9%
Sharpe0.55-0.7~0.85~0.85~0.6
MDD-32% 左右-24%-32%-27%

注:实际数字会因 universe 选择波动,但方向应一致:value 显著弱于 SPY 和 VUG,与 VTV 接近。

7.2 怎么解读

如果你看到这个数字,第一反应可能是「value 这么烂,下次别学了」。这个反应是错的。正确的解读:

  1. 单因子 standalone 的 Sharpe 在 0.3-0.7 是正常的——不是因子失效,是单因子本来就不如组合
  2. value 的真正价值在 multi-factor 组合里——它和 momentum 负相关,是 diversifier
  3. 这十年的 underperformance 不代表未来十年——金融周期会均值回归,2021-2022 我们已经看到迹象
  4. 对 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.850.750.80
互相相关-0.3 to -0.5--

核心数字:组合 Sharpe ≈ 0.85,比单因子最强者(momentum 0.7)还高,MDD 更小。这就是 AMP 2013 的实证主张。Day 14 我们专题做这个组合。


八、常见坑清单

写在这里是为了一年后再看时不犯同样的错:

  1. trailing PE vs forward PE:forward PE 用的是分析师预测,存在 selection bias(分析师倾向于乐观),且数据滞后于 announcement。用 trailing
  2. 负 E/P 排除还是保留:负盈利公司在排序里如果保留会偏向「赢者诅咒」(最便宜的全是亏损股)。建议直接排除负 E/P 公司,但保留负 NI 公司在 CF/P 排序里(OCF 可能为正)。
  3. 行业偏差:直接 B/M 排序会重仓金融、公用事业、能源。学术做法是 Fama-MacBeth 行业中性回归,工业做法是按 GICS 行业内部 z-score 后再合并。今天没做,明确记账
  4. 财报披露日期 vs 财年结束日期:用前者,但 yfinance 给不出,所以用 fiscal_end + 90d 保守 proxy。
  5. 股本变化:回购、增发都会改变 shares outstanding。yfinance 只给最新值,回测早期会偏差。严肃做法用 Sharadar 的 historical share count。
  6. ADR / dual-listing 重复:如果 universe 包括 BABA 和 9988.HK,会重复。我们 SP500 子集没这问题,但跨市场要注意。
  7. Survivorship bias:SP500 现在的成分不等于 2014 年的成分。Day 10 我们就遇到过这个问题。严肃做法用 historical SP500 constituents
  8. Value trap(价值陷阱):「便宜的有便宜的理由」——很多 high B/M 公司是行业死亡前夜(如 2007 年的报纸、2018 年的零售)。改进方法:
    • 加 quality 因子过滤(ROE、利润率)
    • 加 momentum 反向过滤(剔除 12M 跌得最惨的)
    • 加财务健康过滤(debt/equity, interest coverage)
  9. Look-back length:用 TTM 4 季度还是用 latest single quarter?前者平滑但滞后,后者敏感但 noise 大。TTM 是行业惯例
  10. 数据源 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 字 今日完成度:理论 ✓ / 实操(你自己执行)/ 笔记 ✓