跳至正文
MODULE 03特征工程与因子挖掘5 讲 · FACTOR RESEARCH
LECTURE 09共 31 讲预计阅读 45 分钟

第 9 讲:经典量价因子库工程化实现 ​

本讲核心目标:
深入理解 A 股量价因子的行为金融学微观机理(为什么 A 股中短周期呈现剧烈的反转特征?为什么散户情绪主导隔夜跳空?);系统掌握动量反转、历史波动率、极端收益(MAX 彩票异象)、非流动性冲击(Amihud)与隔夜/日内动量解耦五大类经典特征的数学推导;使用 Polars 纯向量化流水线,高吞吐计算生产级量价特征矩阵。


1. 量价因子的本质:市场微观结构与行为金融学映射 ​

量价特征(Price-Volume Features)是量化投资中数据最完整、信噪比最高、调仓反应最灵敏的基石信号。

1.1 A 股的“短期反转” vs 美股的“中期动量”:截然相反的微观动力学 ​

  • 美股市场(机构定价为主):
    杰加迪什与蒂特曼(Jegadeesh & Titman, 1993, Journal of Finance)发现了著名的**“中期动量效应(Momentum Effect)”**——过去 3~12 个月表现优异的股票在未来 1~6 个月会继续跑赢基准。其底层机理在于机构投资者的“盈余公告反应不足(PEAD)”和长期持续建仓行为。
  • A 股市场(高换手博弈与散户处置效应):
    A 股在 5 日、10 日、20 日中短周期上呈现极强的截面反转特征(Short-Term Mean Reversion)。
    • 处置效应(Disposition Effect):散户投资者倾向于急于卖出微小盈利的股票(落袋为安),而长期死扛亏损股票。当一只股票短期暴涨 20% 时,会触发海量获利盘汹涌砸盘。
    • 过度反应(Overreaction):德邦特与塞勒(De Bondt & Thaler, 1985, Journal of Finance)指出,市场参与者对短期突发事件存在认知过度反应。短期冲顶往往源于流动性情绪踩踏,随后必有均值回归。
    • 买方实操铁律:在 A 股做中低频多因子选股,5 日与 20 日动量必须取负号作为反转因子(Reversion Alpha)使用!

2. 五大量价核心因子群数学推导 ​

课程图示
可放大查看细节,点击百分比恢复原始大小。

2.1 动量与反转体系(Momentum & Reversion) ​

设股票在 t 日的后复权收盘价为 Pclose,t,过去 K 个交易日的累计收益率定义为:

RetK(t)=Pclose,tPclose,t−K−1

在 A 股中频多因子库中,短期反转因子统一取负号映射:

FactorRevK(t)=−RetK(t)(K∈{5,10,20})

2.2 隔夜与日内动量解耦(Overnight vs Intraday Decoupling) ​

Lou, Yan, & Zhang (2019, Journal of Financial Economics) 提出的收益分解理论指出:全天总收益由**隔夜跳空(非交易时段信息积累)与日内连续交易收益(算法拆单执行)**驱动,二者驱动主体截然不同:

1+Rtotal,t=(1+Rovernight,t)×(1+Rintraday,t)

其中开盘、收盘均使用同一后复权口径:

Rovernight,t=Popen,tadjPclose,t−1adj−1,Rintraday,t=Pclose,tadjPopen,tadj−1
  • 隔夜收益高(Overnight Alpha 偏弱):往往是夜间自媒体情绪发酵,散户在 09:15-09:25 集合竞价盲目顶板抢筹,主力资金开盘即顺势出货,未来往往强烈反转。
  • 日内收益持续走强(Intraday Alpha 偏强):通常代表公募或险资机构在连续交易时段使用算法切片(TWAP/VWAP)稳步吃单,资金属性更加沉稳,信号具有更长的半衰期延续性。

2.3 波动与极端收益体系(MAX 彩票异象) ​

  1. 历史年化波动率(Historical Volatility, HV20):σ20,t=25219∑k=019(rt−k−r¯)2
  2. 巴利等人的 MAX 异象(Bali, Cakici, Whitelaw, 2011, Journal of Financial Economics):MAX20,t=maxk∈[0,19]{rt−k}
    • 行为经济学机理:人类天生具有“买彩票(Lottery-like Assets)”的心理偏差。过去 20 天内曾经发生过单日暴涨 10%~20% 涨停板的股票,会吸引大量非理性散户追捧,导致其被严重高估。
    • 因子定义:MAX 因子在截面上具有显著的负向 Alpha(即 MAX 越大的股票,未来越倾向于跑输大盘)。实盘中取负号:FactorMAX=−MAX20,t。

2.4 流动性与冲击成本体系(Amihud Illiquidity Ratio) ​

阿米胡德(Yakov Amihud, 2002, Journal of Financial Markets)提出了著名的非流动性指标,衡量每单位成交金额对股票价格的平均绝对冲击幅度:

Illiq20,t=120∑k=019|rt−k|Turnovert−k×10−8

其中分母为亿元成交额。

  • Illiq 越大,表明该股票盘口越浅、微观流动性越差,微小买盘就能引起巨大价格跳跃。
  • 注意:Amihud 因子天生与股票市值(Size)存在强负相关性(小盘股天然成交额小)。在送入选股模型前,必须执行市值与行业中性化,否则该因子将蜕变为微盘股的隐蔽代名词!

3. 动手实操:Polars 纯向量化量价特征工程流水线 ​

下面的 Python 代码严格遵循机构级买方规范,使用 Polars 原生表达式多核并行计算 15 个量价特征。

在项目根目录运行 PYTHONPATH=tutorial python3 -m examples.lecture_09_price_volume_factors。依赖安装、共享模块和数据契约见项目说明。

py
"""
lecture_09_price_volume_engine.py
工业级 A 股全市场量价因子工程化计算引擎 (基于 Polars 表达式)
"""
import time
import numpy as np
import polars as pl
import pandas as pd

class AsharePriceVolumeFactorEngine:
    def __init__(self):
        pass

    def compute_all_factors(self, daily_bars_df: pl.DataFrame) -> pl.DataFrame:
        """
        全量批量计算量价特征矩阵 (纯多核向量化)
        输入必须包含: [symbol, trade_date, open, high, low, close, close_hfq, volume, turnover]
        """
        print("[*] 正在启动 Polars 多核并行量价特征流水线...")
        t0 = time.perf_counter()

        prices = ["open", "high", "low", "close", "close_hfq"]
        if daily_bars_df.select(pl.any_horizontal([
            (~pl.col(c).is_finite() | (pl.col(c) <= 0)).fill_null(True) for c in prices
        ]).any()).item():
            raise ValueError("原始和复权价格必须为有限正数")
        if daily_bars_df.select(pl.struct("symbol", "trade_date").is_duplicated().any()).item():
            raise ValueError("行情的股票日期键必须唯一")
        # 转换为惰性执行图
        lazy_df = daily_bars_df.lazy().sort(["symbol", "trade_date"])

        factors_lazy = lazy_df.with_columns([
            # ================= 1. 动量与反转体系 =================
            # 1日收益率 (日收益)
            (pl.col("close_hfq") / pl.col("close_hfq").shift(1).over("symbol") - 1.0).alias("ret_1d"),
            # 5日动量
            (pl.col("close_hfq") / pl.col("close_hfq").shift(5).over("symbol") - 1.0).alias("mom_5d"),
            # 20日动量
            (pl.col("close_hfq") / pl.col("close_hfq").shift(20).over("symbol") - 1.0).alias("mom_20d"),
            # 60日中周期动量
            (pl.col("close_hfq") / pl.col("close_hfq").shift(60).over("symbol") - 1.0).alias("mom_60d"),

            # 5日与20日短期反转因子 (取负号)
            (-(pl.col("close_hfq") / pl.col("close_hfq").shift(5).over("symbol") - 1.0)).alias("rev_5d"),
            (-(pl.col("close_hfq") / pl.col("close_hfq").shift(20).over("symbol") - 1.0)).alias("rev_20d"),

            # ================= 2. 隔夜与日内微观解耦 =================
            # 隔夜收益 (开盘 / 前收 - 1)
            ((pl.col("open") * pl.col("close_hfq") / pl.col("close")) / pl.col("close_hfq").shift(1).over("symbol") - 1.0).alias("ret_overnight"),
            # 日内收益 (收盘 / 开盘 - 1)
            (pl.col("close") / pl.col("open") - 1.0).alias("ret_intraday"),

            # ================= 3. 波动与极端收益体系 (MAX) =================
            # 20日年化波动率
            (
                (pl.col("close_hfq") / pl.col("close_hfq").shift(1).over("symbol") - 1.0)
                .rolling_std(window_size=20)
                .over("symbol") * (252.0 ** 0.5)
            ).alias("volatility_20d"),

            # 日内真实振幅 (High / Low - 1)
            (pl.col("high") / pl.col("low") - 1.0).alias("amplitude_daily"),
            # 20日平均振幅
            ((pl.col("high") / pl.col("low") - 1.0).rolling_mean(20).over("symbol")).alias("amplitude_20d"),

            # MAX 异象因子: 过去 20 日单日最大收益率 (取负号,预期负 Alpha)
            (
                -(pl.col("close_hfq") / pl.col("close_hfq").shift(1).over("symbol") - 1.0)
                .rolling_max(window_size=20)
                .over("symbol")
            ).alias("neg_max_20d"),

            # ================= 4. 流动性与换手活跃度体系 =================
            # 20日成交量移动平均
            (pl.col("volume").rolling_mean(20).over("symbol")).alias("vol_ma20"),
            # 成交量爆发比率 (今日量 / 20日均量)
            (pl.col("volume") / (pl.col("volume").rolling_mean(20).over("symbol") + 1e-8)).alias("vol_surge_ratio"),

            # Amihud 冲击成本因子 (过去20天 |收益| / 成交额 的均值)
            (
                (
                    (pl.col("close_hfq") / pl.col("close_hfq").shift(1).over("symbol") - 1.0).abs() /
                    pl.when(pl.col("turnover") > 0).then(pl.col("turnover") / 1e8).otherwise(None) # 换算为亿元成交额
                )
                .rolling_mean(window_size=20)
                .over("symbol")
            ).alias("amihud_illiq_20d")
        ])

        # 触发物理计算
        res_df = factors_lazy.collect()
        elapsed = time.perf_counter() - t0
        print(f"✅ 量价特征计算完毕!总耗时: {elapsed:.3f} 秒 (包含 {len(res_df.columns)} 个特征字段)")
        return res_df

# =========================================================================
# 单元验证与吞吐量测试
# =========================================================================
if __name__ == "__main__":
    np.random.seed(42)
    n_stocks = 500
    n_days = 250
    total_len = n_stocks * n_days
    print(f"[*] 正在构造规模为 {total_len:,} 行的仿真 A 股行情面板...")

    dates = pd.date_range("2023-01-01", periods=n_days, freq="B")
    symbols = [f"{i:06d}.SZ" for i in range(1, n_stocks + 1)]

    records = []
    for s in symbols:
        ret = np.random.normal(0.0003, 0.02, size=n_days)
        prices = 10.0 * np.exp(np.cumsum(ret))
        opens = prices * (1 + np.random.normal(0, 0.004, size=n_days))
        highs = np.maximum(opens, prices) * (1 + np.abs(np.random.normal(0, 0.005, size=n_days)))
        lows = np.minimum(opens, prices) * (1 - np.abs(np.random.normal(0, 0.005, size=n_days)))
        vols = np.random.uniform(500000, 5000000, size=n_days)
        turnovers = vols * prices

        for i in range(n_days):
            records.append({
                "symbol": s,
                "trade_date": dates[i].date(),
                "open": opens[i],
                "high": highs[i],
                "low": lows[i],
                "close": prices[i],
                "close_hfq": prices[i], # 合成行情无分红拆股
                "volume": vols[i],
                "turnover": turnovers[i]
            })

    mock_bars = pl.DataFrame(records)

    # 运行特征引擎
    engine = AsharePriceVolumeFactorEngine()
    features_df = engine.compute_all_factors(mock_bars)

    # 抽样打印结果
    sample_sym = "000001.SZ"
    sample_res = features_df.filter(pl.col("symbol") == sample_sym).tail(3)
    
    print("\n" + "=" * 90)
    print(f"标的 {sample_sym} 最新特征计算切片抽样:")
    print("=" * 90)
    cols_to_show = ["symbol", "trade_date", "close", "rev_20d", "ret_overnight", "ret_intraday", "volatility_20d", "neg_max_20d", "amihud_illiq_20d"]
    print(sample_res.select(cols_to_show))

核心运行输出与性能解析: ​

[*] 正在构造规模为 125,000 行的仿真 A 股行情面板...
[*] 正在启动 Polars 多核并行量价特征流水线...
✅ 量价特征计算完毕!总耗时: 0.062 秒 (包含 24 个特征字段)

==========================================================================================
标的 000001.SZ 最新特征计算切片抽样:
==========================================================================================
shape: (3, 9)
┌─────────┬─────────┬─────────┬────────┬───┬────────┬────────┬────────┬────────┐
│ symbol  ┆ trade_d ┆ close   ┆ rev_20 ┆ … ┆ ret_in ┆ volati ┆ neg_ma ┆ amihud │
│ ---     ┆ ate     ┆ ---     ┆ d      ┆   ┆ traday ┆ lity_2 ┆ x_20d  ┆ _illiq │
│ str     ┆ ---     ┆ f64     ┆ ---    ┆   ┆ ---    ┆ 0d     ┆ ---    ┆ _20d   │
│         ┆ date    ┆         ┆ f64    ┆   ┆ f64    ┆ ---    ┆ f64    ┆ ---    │
│         ┆         ┆         ┆        ┆   ┆        ┆ f64    ┆        ┆ f64    │
╞═════════╪═════════╪═════════╪════════╪═══╪════════╪════════╪════════╪════════╡
│ 000001. ┆ 2023-12 ┆ 10.1905 ┆ 0.0377 ┆ … ┆ 0.0007 ┆ 0.2906 ┆ -0.044 ┆ 0.0667 │
│ SZ      ┆ -13     ┆ 61      ┆ 76     ┆   ┆ 62     ┆ 92     ┆ 125    ┆ 26     │
│ 000001. ┆ 2023-12 ┆ 10.5599 ┆ -0.011 ┆ … ┆ 0.0035 ┆ 0.3162 ┆ -0.044 ┆ 0.0677 │
│ SZ      ┆ -14     ┆ 75      ┆ 169    ┆   ┆ 15     ┆ 99     ┆ 125    ┆ 99     │
│ 000001. ┆ 2023-12 ┆ 10.6490 ┆ -0.005 ┆ … ┆ 0.0055 ┆ 0.3138 ┆ -0.044 ┆ 0.0652 │
│ SZ      ┆ -15     ┆ 49      ┆ 63     ┆   ┆ 62     ┆ 19     ┆ 125    ┆ 89     │
└─────────┴─────────┴─────────┴────────┴───┴────────┴────────┴────────┴────────┘
  1. 极致吞吐性能:
    • 12.5 万行高密度面板数据,仅耗时 0.062 秒 即完成 24 个复合量价特征的计算;
  2. 复权双轨制应用:
    • 隔夜收益率采用 open * (close_hfq / close) / close_hfq.shift(1) - 1.0,严格消除了除权除息跳空对隔夜因子的机械污染。

4. 本讲小结与课后动手作业 ​

核心知识图谱梳理: ​

  1. A 股反转哲学:散户处置效应与过度反应导致中短周期反转远强于动量;
  2. 隔夜与日内解耦:隔夜跳空反映散户非理性情绪溢价,日内走势反映大资金算法建仓,具有完全不同的 Alpha 属性;
  3. MAX 异象的彩票心理:极端暴涨历史具有负向风险溢价,应作为负向特征纳入多因子库;
  4. Amihud 冲击成本:量化微观流动性浅度,必须配合市值中性化使用。

课后动手实训: ​

实操任务:

  1. 打开 tutorial/examples/lecture_09_price_volume_factors.py。
  2. 尝试添加一个新因子:过去 20 天的真实波动幅度均值(Average True Range, ATR20),计算公式为 TRt=max(Hight−Lowt,|Hight−Closet−1|,|Lowt−Closet−1|)。
  3. 运行引擎,打印标的 000001.SZ 的 ATR20 结果。

QUANTML · Quantitative Research Handbook