第 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)
设股票在
在 A 股中频多因子库中,短期反转因子统一取负号映射:
2.2 隔夜与日内动量解耦(Overnight vs Intraday Decoupling)
Lou, Yan, & Zhang (2019, Journal of Financial Economics) 提出的收益分解理论指出:全天总收益由**隔夜跳空(非交易时段信息积累)与日内连续交易收益(算法拆单执行)**驱动,二者驱动主体截然不同:
其中开盘、收盘均使用同一后复权口径:
- 隔夜收益高(Overnight Alpha 偏弱):往往是夜间自媒体情绪发酵,散户在 09:15-09:25 集合竞价盲目顶板抢筹,主力资金开盘即顺势出货,未来往往强烈反转。
- 日内收益持续走强(Intraday Alpha 偏强):通常代表公募或险资机构在连续交易时段使用算法切片(TWAP/VWAP)稳步吃单,资金属性更加沉稳,信号具有更长的半衰期延续性。
2.3 波动与极端收益体系(MAX 彩票异象)
- 历史年化波动率(Historical Volatility, HV20):
- 巴利等人的 MAX 异象(Bali, Cakici, Whitelaw, 2011, Journal of Financial Economics):
- 行为经济学机理:人类天生具有“买彩票(Lottery-like Assets)”的心理偏差。过去 20 天内曾经发生过单日暴涨 10%~20% 涨停板的股票,会吸引大量非理性散户追捧,导致其被严重高估。
- 因子定义:
因子在截面上具有显著的负向 Alpha(即 越大的股票,未来越倾向于跑输大盘)。实盘中取负号: 。
2.4 流动性与冲击成本体系(Amihud Illiquidity Ratio)
阿米胡德(Yakov Amihud, 2002, Journal of Financial Markets)提出了著名的非流动性指标,衡量每单位成交金额对股票价格的平均绝对冲击幅度:
其中分母为亿元成交额。
越大,表明该股票盘口越浅、微观流动性越差,微小买盘就能引起巨大价格跳跃。 - 注意: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 │
└─────────┴─────────┴─────────┴────────┴───┴────────┴────────┴────────┴────────┘- 极致吞吐性能:
- 12.5 万行高密度面板数据,仅耗时 0.062 秒 即完成 24 个复合量价特征的计算;
- 复权双轨制应用:
- 隔夜收益率采用
open * (close_hfq / close) / close_hfq.shift(1) - 1.0,严格消除了除权除息跳空对隔夜因子的机械污染。
- 隔夜收益率采用
4. 本讲小结与课后动手作业
核心知识图谱梳理:
- A 股反转哲学:散户处置效应与过度反应导致中短周期反转远强于动量;
- 隔夜与日内解耦:隔夜跳空反映散户非理性情绪溢价,日内走势反映大资金算法建仓,具有完全不同的 Alpha 属性;
- MAX 异象的彩票心理:极端暴涨历史具有负向风险溢价,应作为负向特征纳入多因子库;
- Amihud 冲击成本:量化微观流动性浅度,必须配合市值中性化使用。
课后动手实训:
实操任务:
- 打开
tutorial/examples/lecture_09_price_volume_factors.py。- 尝试添加一个新因子:过去 20 天的真实波动幅度均值(Average True Range, ATR20),计算公式为
。 - 运行引擎,打印标的
000001.SZ的 ATR20 结果。