第三章:基础因子构建——估值、规模、动量与波动率

各位同学,欢迎来到实战环节。前两章我们聊了量化投资的底层逻辑和数据准备,今天终于要动手了——构建最基础的四大类因子。

说实话,很多新手一上来就追求复杂的机器学习模型,结果往往不如人意。我个人习惯是:先把基础因子做扎实,再谈高阶模型。你想想看,地基不稳,房子再漂亮也是危楼。

3.1 估值因子:PE / PB / PS

估值因子,说白了就是判断股票「贵不贵」的指标。我刚开始做量化时,以为PE越低越好,结果踩了不少坑。

3.1.1 市盈率(PE)

PE = 股价 / 每股收益。这个指标最常用,但要注意:

  • 静态PE:用去年年报的EPS,数据滞后
  • 滚动PE(TTM):用最近四个季度的EPS,更及时
  • 动态PE:用预测的EPS,有主观性
避坑指南:我曾经在回测中直接用静态PE,结果发现每年4月年报披露后,因子值突然跳变,导致策略信号紊乱。后来我改用TTM口径,问题就解决了。

3.1.2 市净率(PB)

PB = 股价 / 每股净资产。这个指标对金融、周期行业特别有效。为什么?因为这些行业的资产容易估值,净资产比较靠谱。

但要注意,PB为负的公司要剔除——净资产为负,说明公司已经资不抵债了,这种股票再便宜也别碰。

3.1.3 市销率(PS)

PS = 股价 / 每股销售收入。这个指标适合:

  • 互联网公司(还没盈利,PE无效)
  • 零售企业(收入稳定,利润波动大)
  • 高成长行业(利润被研发费用侵蚀)

代码实现其实很简单,我们来看一下:

import pandas as pd
import numpy as np

def calc_valuation_factors(df):
    """
    df: 包含 price, eps, bvps, sps 的DataFrame
    返回:PE, PB, PS 因子
    """
    # 剔除异常值
    df = df[df['eps'] > 0]  # 负PE的公司剔除
    
    df['PE_TTM'] = df['close'] / df['eps_ttm']
    df['PB'] = df['close'] / df['bvps']
    df['PS'] = df['close'] / df['sps']
    
    # 去极值处理(我习惯用MAD方法)
    for factor in ['PE_TTM', 'PB', 'PS']:
        median = df[factor].median()
        mad = (df[factor] - median).abs().median()
        upper = median + 5 * mad
        lower = median - 5 * mad
        df[factor] = df[factor].clip(lower, upper)
    
    return df[['PE_TTM', 'PB', 'PS']]
小技巧:去极值时,我推荐用MAD(中位数绝对偏差)而不是标准差。因为因子值往往有厚尾分布,标准差会被极端值拉偏,MAD更稳健。

3.2 规模因子:市值与流通市值

规模因子是量化投资中最经典的因子之一。Fama-French三因子模型里就有它。说白了就是:小盘股长期跑赢大盘股

但这里有个细节:用总市值还是流通市值?

类型 定义 适用场景
总市值 股价 × 总股本 学术研究、全市场选股
流通市值 股价 × 流通股本 A股实战(考虑限售股)

我个人建议:在A股市场,优先用流通市值。为什么?因为A股有很多限售股,它们短期内不会在市场上交易,用总市值会高估实际可交易的规模。

嗯,这里要注意:流通市值因子需要取对数,因为市值分布是右偏的。取完对数后,因子值更接近正态分布,模型效果更好。

def calc_size_factors(df):
    """
    计算规模因子
    """
    df['log_mkt_cap'] = np.log(df['total_market_cap'])
    df['log_float_cap'] = np.log(df['float_market_cap'])
    
    # 标准化处理
    for factor in ['log_mkt_cap', 'log_float_cap']:
        df[factor] = (df[factor] - df[factor].mean()) / df[factor].std()
    
    return df[['log_mkt_cap', 'log_float_cap']]

3.3 动量因子:过去N日收益

动量因子,就是「强者恒强,弱者恒弱」。我见过很多策略,核心逻辑就是买过去涨得好的股票,卖过去跌得惨的股票。

但动量因子有个大坑:反转效应。短期动量(比如1个月)和长期动量(比如12个月)效果不错,但中期(3-6个月)往往会出现反转。

为什么会这样?行为金融学解释是:投资者对信息的反应不足(短期动量),然后过度反应(中期反转),最后慢慢修正(长期动量)。

代码实现:

def calc_momentum_factors(df, price_col='close'):
    """
    计算不同周期的动量因子
    """
    # 过去5日收益(短期)
    df['mom_5d'] = df[price_col].pct_change(5)
    
    # 过去20日收益(约1个月)
    df['mom_20d'] = df[price_col].pct_change(20)
    
    # 过去60日收益(约3个月)
    df['mom_60d'] = df[price_col].pct_change(60)
    
    # 过去120日收益(约6个月)
    df['mom_120d'] = df[price_col].pct_change(120)
    
    # 过去250日收益(约1年)
    df['mom_250d'] = df[price_col].pct_change(250)
    
    return df[[col for col in df.columns if col.startswith('mom_')]]
避坑指南:我曾经在回测中用了过去20日的动量因子,结果发现每个月末调仓时,因子值刚好处于反转期,策略收益惨淡。后来我改用过去60日动量,效果就好多了。所以,一定要做参数敏感性分析,别死守一个周期。

3.4 波动率因子

波动率因子,衡量的是股票价格的不确定性。低波动率股票往往能跑赢高波动率股票——这就是著名的「低波动异象」。

计算波动率的方法有很多,我常用的是:

  • 历史波动率:过去N日收益率的标准差
  • 已实现波动率:日内高频数据的波动
  • GARCH模型:考虑波动率聚集效应

在实战中,历史波动率就够用了。但要注意:波动率有聚集效应——高波动之后往往跟着高波动,低波动之后跟着低波动。所以,直接用原始波动率做因子,会有自相关的问题。

我的处理方法是:对波动率取对数,然后做差分,得到波动率的变化量。这样既能捕捉波动率的变化趋势,又能消除自相关。

def calc_volatility_factors(df, price_col='close'):
    """
    计算波动率因子
    """
    # 日收益率
    df['daily_ret'] = df[price_col].pct_change()
    
    # 过去20日波动率(月波动率)
    df['vol_20d'] = df['daily_ret'].rolling(20).std() * np.sqrt(252)
    
    # 过去60日波动率(季波动率)
    df['vol_60d'] = df['daily_ret'].rolling(60).std() * np.sqrt(252)
    
    # 波动率变化量
    df['vol_change'] = np.log(df['vol_20d']) - np.log(df['vol_60d'].shift(20))
    
    return df[['vol_20d', 'vol_60d', 'vol_change']]
小技巧:年化波动率 = 日波动率 × √252。这个252是A股的年化交易日数,美股用252,港股用247。别搞混了。

3.5 知识体系总览

说了这么多,我们来画一张图,把今天的内容串起来:

基础因子构建知识体系 四大基础因子 估值因子 PE(市盈率) PB(市净率) PS(市销率) 规模因子 总市值(取对数) 流通市值(取对数) A股优先用流通市值 动量因子 短期动量(5-20日) 中期动量(60日) 长期动量(120-250日) 波动率因子 历史波动率 波动率变化量 注意聚集效应 数据处理:去极值 → 标准化 → 中性化

这张图把今天讲的四大因子串起来了。你会发现,每个因子都有自己独特的逻辑和坑。做量化投资,不是堆砌因子,而是理解每个因子背后的经济学含义

好了,今天的实战内容就到这里。记住:基础因子是量化投资的基石,花时间把它们做扎实,后面的机器学习模型才能发挥真正的威力。