QUANT RESEARCH / FIELD NOTES 004
从一个股票评分,到一条经得起验证的研究流水线。
研究协议 · 时点数据 · 因子评价 · 样本外验证 · 执行反馈
含完整 Python 标准库示例;合成数据仅用于机制教学。

本文面向量化研究与 OMS/EMS、FIX 接入研发人员,按“直觉 → 公式 → 代码 → 实证边界 → 陷阱”整理因子挖掘流程。核心不是寻找历史表现最好的公式,而是寻找在未参与筛选的数据上仍有增量预测力、且执行后可能保留经济价值的信号。

整理日期:2026-11-07。示例为合成数据,仅验证计算机制,不代表真实市场实证结论,不构成投资建议或交易授权。本文未接入真实账户、券商系统或生产数据。

一、结论:因子不是交易指令

时点数据因子 / 预测信号目标组合 / 调仓 / OMS母单 / 算法 / 子单 / Broker回报 / TCA

因子(Factor) 是一个可复现的特征,例如过去 20 个交易日涨幅。信号(Signal) 是用于决策的预测或评分;组合(Portfolio) 是在约束下分配资金的结果。三者不能混为一谈。

1.1 上游给什么

证券与交易日、价格/成交量、财报与公告时间、历史股票池、行业分类、数据快照版本。数据必须带“何时可知”的语义。

1.2 下游要什么

instrument, as_of, available_at, value, factor_version, snapshot_id;再经预测模型与组合优化,形成目标权重、股数、有效期与执行基准。

从子单回溯,应能找到调仓批次 → 目标组合 → 信号版本 → 因子与快照。因子值不是 FIX 标准订单字段,不应凭空塞入协议;通常通过内部关联 ID 追踪。

二、先写研究协议,再看回测

“20 日动量可能反映信息缓慢扩散”是待检验假设,不是事实;A 股不同阶段也可能出现反转。先锁定方向和研究口径,避免看到结果再编故事。

协议字段本文实验口径真实研究还要补
标的 / 区间40 个虚构标的、260 个合成交易时点历史成分、退市标的、明确起止日期
假设 / 频率20 日涨幅越高,后续 5 日收益是否越高;每 5 日评价为什么是该期限,事先固定备选参数范围
观察 / 成交t 收盘后计算,理想化 t+1 收盘入场实际信号延迟、委托窗口、成交与未成交规则
成本 / 指标只检验排序,未模拟成本;Rank IC、非年化 ICIR、五组收益净收益、换手、容量、回撤、成交率与成本敏感性
验证状态代码机制演示;没有训练、调参或真实样本外证据按时间分训练/验证/冻结测试,滚动前推与纸面交易

三、数据治理:首先保证当时可知

时点一致(Point-in-Time,PIT):在决策时点,只使用已经公开且已被系统接收的数据。财报所属季度≠公告时间;公告时间≠供应商可用时间;后来更正的数据也不能覆盖当时快照。

1
2
available_at ≤ decision_time < order_time
factor(i,t) = f(当时可用的数据与历史窗口)
  • 股票池:按历史日期重建。用今天仍上市的股票回测过去,会产生幸存者偏差(Survivorship Bias)。
  • 复权:计算收益须一致处理拆分、分红与公司行动;成交、涨跌停与股数用对应时点的真实价格口径。避免把含未来公司行动的绝对复权价当作当时可知的价格特征。
  • 日历与缺失:20 个交易日不等于 20 个自然日。不能把停牌日删掉后,把“最近 20 条记录”误当统一市场窗口;更不能跨停牌前向填充成可成交价。
  • 预处理:去极值、标准化、中性化要留版本。跨时间拟合的均值、阈值和模型只能在训练区间估计;当日截面处理可使用当时完整可用截面,但要考虑数据到齐时间。

常见处理顺序:缺失规则 → 去极值 → 截面标准化 → 按研究目标中性化 → 再标准化。顺序不是唯一标准,必须固定并披露。

四、因子工厂的五条挖掘路径

  1. 人工假设:价值、盈利质量、动量、波动率、流动性。优点是可解释、搜索空间小;风险是经济故事不等于可交易的超额收益。
  2. 算子组合:对收益、成交量使用窗口均值、标准差、差分、排名、相关等,枚举公式。注意量纲、除零、窗口和算子的时序方向。
  3. 机器学习(Machine Learning,ML):用树模型等学习已有特征与未来标签的映射;也可学习新表示。特征重要性不是因果性,也不是独立因子有效性的证明。
  4. 遗传规划(Genetic Programming,GP):以公式树进行交叉与变异。要限制复杂度、换手与相关性,并记录所有尝试,而非只记冠军。
  5. 大语言模型(Large Language Model,LLM):辅助提出机制、把文字转公式、检查实现;通过预设验证集反馈。反复把测试结果喂回模型,会把测试集变成训练过程的一部分。

入门建议:先手写一个简单基线,把数据与评估链路跑通;再扩展搜索。自动化越强,试验越多,假发现与测试集污染的风险越大。

4.1 因子如何变成最终信号

1
2
z(i,t,k) = 第 k 个因子经固定规则处理后的值
score(i,t) = Σ a(k,t) × z(i,t,k)

先用固定等权作为基线;复杂权重只能用当时训练资料估计。比较因子值相关性之外,还要看因子收益相关性、风险暴露、边际增益与新增换手。多个“低相关”因子也不保证稳定。

五、因子评价:IC、ICIR、分组与中性化

5.1 直觉 → 公式

信息系数(Information Coefficient,IC) 是在每个日期的股票截面上,计算因子与未来收益的相关性。Pearson IC 看数值线性关系;秩信息系数(Rank IC) 即 Spearman 相关,看排名关系,并对并列值使用平均秩。

1
2
3
4
x(i,t) = C(i,t) / C(i,t−20) − 1
y(i,t) = C(i,t+6) / C(i,t+1) − 1
RankIC(t) = corr(rank_i x(i,t), rank_i y(i,t))
ICIR = mean_t[IC(t)] / sample_std_t[IC(t)]

C 是同一收益口径的收盘价格;t 是交易日序号。例中信号在 t 收盘后产生,理想化在 t+1 收盘成交,持有 5 个收盘到收盘区间,因此出场为 t+6。不是用已知 t 收盘价无摩擦地回填成交。

纠正两个常见简称:这里的比值应明确叫 ICIR,不要与组合超额收益的信息比率(Information Ratio,IR)混用。本文 ICIR 不年化;有自相关、标签重叠时,不能机械乘 √252 或把普通 t 检验当可靠证据。

5.2 分组与成本

每个评价日按因子从低到高分为 Q1–Q5,比较下一持有期各组等权收益。单调性有助理解,但不要求每一期都严格单调;还要看跨期稳定性、样本量和置信区间。

Q5−Q1 是研究价差,不是默认可做空的 A 股组合。 长仓策略应比较高分组与适当基准,并控制行业、市值和市场风险。五组都涨,可能只是市场上涨。

1
2
单边换手 TO(t) = ½ Σ_i |w_target(i,t) − w_pretrade(i,t)|
净收益 ≈ 毛收益 − Σ_i c_i × |Δw_i| − 其他未包含费用

权重以含现金的组合净值为分母;上述换手与交易费用求和仅遍历证券,不把现金腿再次计入。这里将半数证券权重绝对变化定义为单边换手,建仓/清仓也沿用此定义;持仓漂移后再计算差额。若 c 为相同的每方向成本率,完全换仓的买卖成本为 2c×TO。冲击、价差若已含于成交价,不再重复扣费;佣金、税费与融资借券按适用日期、市场和方向建模。

5.3 中性化:剥离暴露,不是免费增强

1
2
x(i,t) = α(t) + β(t) log(市值(i,t)) + Σ γ(j,t) 行业哑变量(i,j,t) + ε(i,t)
用残差 ε 作为中性化后的因子,再评价 IC 与组合。

回归须避免哑变量共线性、检查小行业样本。中性化前后都报告:它改变研究问题,可能去掉有效经济暴露,也可能揭示“新因子”只是小市值换了名字。

六、排除假发现:样本外与多重检验

  1. 先去重:检查公式等价、因子值相关和收益相关;比较相对简单基线的新增预测力。
  2. 按时间切分:训练期拟合,验证期选方案,冻结测试期只用于最终评价。看过测试结果再改公式,原测试就不再是干净样本外。
  3. 隔离跨界标签:若训练样本的未来收益窗口跨入验证期,就应剔除。隔离长度按真实标签终点、特征发布时间和持有期确定,不是随意留几天。
  4. 滚动前推(Walk-forward):只用过去重新估计,再评价下一段;拼接的是各段真正未见的数据结果,不是各段最优历史曲线。
  5. 记录多重试验:搜索多少公式、参数、股票池和持有期都应留痕;采用多重检验控制、块自助法或合适的稳健标准误。没有一个 IC 门槛或 t 值能通吃所有研究。
  6. 稳健性与容量:分年份、行情与行业检查,扰动参数,改变成本和延迟,排查少数日期/小盘股驱动;报告日期数、每日股票数与缺失率,不能把股票×日期数当独立样本量。
  7. 纸面交易(Paper Trading):规则冻结后,持续记录真实可见信号与仿真委托;核对信号衰减、可成交性及成本,仍不等于已获实盘授权。

通过条件应在研究前预设:增量效应、稳定性、统计不确定性、成本后收益与容量同时评估。IC 为负也可能形成反向信号,但方向必须在训练/验证阶段决定,不能用测试集反向“救活”。

七、最小实验:验证计算机制,不宣称 Alpha

示例只依赖 Python 标准库,使用固定种子生成独立随机游走(Synthetic Data),不植入预测规律。它不是 A 股回测,也没有选模、真实样本外证据或交易成本模拟。

研究口径:40 个虚构标的、260 个合成交易时点;t 收盘后计算 20 日动量,理想化在 t+1 收盘入场,于 t+6 收盘退出;每 5 日评价一次。相邻持有区间端点相接,但因子历史窗口重叠,因此 IC 序列仍不能假设独立。

7.1 完整可运行代码

将下面代码保存为独立的 factor_demo.py。输出文件位于脚本所在目录,不依赖博客源码目录或本机其他文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
"""Synthetic teaching experiment, not an A-share backtest. Python standard library only."""
import json
import math
import platform
import random
import statistics
from pathlib import Path

SEED = 20261107
N, DAYS, LOOKBACK, HOLD = 40, 260, 20, 5

def ranks(values):
    order = sorted(range(len(values)), key=values.__getitem__)
    result = [0.0] * len(values)
    start = 0
    while start < len(order):
        end = start + 1
        while end < len(order) and values[order[end]] == values[order[start]]:
            end += 1
        for index in order[start:end]:
            result[index] = (start + end - 1) / 2 + 1
        start = end
    return result

def corr(x, y):
    mx, my = statistics.mean(x), statistics.mean(y)
    numerator = sum((a - mx) * (b - my) for a, b in zip(x, y))
    denominator = math.sqrt(sum((a - mx) ** 2 for a in x) * sum((b - my) ** 2 for b in y))
    return numerator / denominator if denominator else None

def run():
    assert N >= 5 and N % 5 == 0
    rng = random.Random(SEED)
    # Rows are synthetic trading-session indices, not actual exchange dates.
    prices = [[100.0] * N]
    for _ in range(DAYS - 1):
        prices.append([p * math.exp(rng.gauss(0, 0.015)) for p in prices[-1]])
    rows = []
    for t in range(LOOKBACK, DAYS - HOLD - 1, HOLD):
        factor = [prices[t][i] / prices[t - LOOKBACK][i] - 1 for i in range(N)]
        # Signal after close t; idealized entry close t+1, exit close t+1+HOLD.
        label = [prices[t + 1 + HOLD][i] / prices[t + 1][i] - 1 for i in range(N)]
        ic = corr(ranks(factor), ranks(label))
        order = sorted(range(N), key=factor.__getitem__)
        groups = [statistics.mean(label[i] for i in order[g * (N // 5):(g + 1) * (N // 5)]) for g in range(5)]
        rows.append({'signal_session': t, 'entry_session': t + 1,
                     'exit_session': t + 1 + HOLD, 'n': N, 'rank_ic': ic,
                     'group_returns': groups, 'top_minus_bottom': groups[-1] - groups[0]})
    ics = [row['rank_ic'] for row in rows if row['rank_ic'] is not None]
    std = statistics.stdev(ics)
    return {'disclaimer': 'SYNTHETIC random walk; no evidence of real factor profitability; no execution/cost simulation',
            'seed': SEED, 'python': platform.python_version(), 'data_version': 'synthetic-randomwalk-v1',
            'parameters': {'stocks': N, 'sessions': DAYS, 'lookback': LOOKBACK, 'hold': HOLD},
            'periods': len(rows), 'observations': sum(row['n'] for row in rows),
            'mean_rank_ic': statistics.mean(ics), 'sample_std_rank_ic': std,
            'icir_not_annualized': statistics.mean(ics) / std if std else None,
            'mean_group_returns': [statistics.mean(row['group_returns'][g] for row in rows) for g in range(5)],
            'rows': rows}

if __name__ == '__main__':
    assert ranks([3, 1, 1]) == [3, 1.5, 1.5]
    assert abs(corr(ranks([1, 2, 3]), ranks([6, 4, 2])) + 1) < 1e-12
    assert corr([1, 1], [2, 3]) is None
    result = run()
    assert result == run()
    assert all(r['entry_session'] > r['signal_session'] for r in result['rows'])
    assert all(a['exit_session'] == b['entry_session'] for a, b in zip(result['rows'], result['rows'][1:]))
    output = Path(__file__).with_name('factor_demo_result.json')
    output.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding='utf-8')
    print(json.dumps({k: v for k, v in result.items() if k != 'rows'}, ensure_ascii=False, indent=2))

运行命令:

1
python3 factor_demo.py

脚本在自身所在目录输出 factor_demo_result.json,并验证平均秩、零方差保护、可复现性、信号/成交顺序和非重叠持有区间。

7.2 已运行的合成结果

以下是原教学实验在 Python 3.14.5 上的结果快照,不是市场业绩;不同 Python 版本应以本地重跑结果为准。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
  "seed": 20261107,
  "python": "3.14.5",
  "data_version": "synthetic-randomwalk-v1",
  "periods": 47,
  "observations": 1880,
  "mean_rank_ic": -0.006810107380942878,
  "sample_std_rank_ic": 0.16014798583795,
  "icir_not_annualized": -0.0425238403424809,
  "mean_group_returns": [
    -0.0022556940031587007,
    0.0022915752952459164,
    0.0019438211998860898,
    0.0001805986047939019,
    -0.001997564622292686
  ]
}

47 个评价期 × 40 个标的,共 1880 条截面观测;这不等于 1880 个独立时间样本。平均 Rank IC 约 -0.00681,非年化 ICIR 约 -0.04252,五组收益没有形成稳定单调排序。这里只描述这一次合成运行,不能据此对真实动量因子做有效或无效的推断。

7.3 从机制演示到真实研究,还缺什么

没有真实证券日历、历史成分与退市、分红、停牌、涨跌停、T+1、费用或成交容量;没有训练/验证/冻结测试切分,没有显著性检验,也没有构造净值组合。代码没有通过真实 A 股策略验收。

接入真实数据前,先冻结公式、方向、股票池、持有期和缺失策略;不可删除未来停牌/退市股票来美化收益。策略阶段应记录买不进、卖不出、现金滞留与退出估值政策。

八、A 股执行约束与子单闭环

本文以普通 A 股股票 教学,不把 ETF、债券或其他市场的规则混用。以下是建模事项,不是完整或现行交易规则清单;真实研究须按交易所、板块、证券状态与生效日期核对。

  • T+1:当日买入的普通 A 股通常不能当日卖出;已有可卖持仓与当日新增持仓应分开处理。
  • 涨跌停、停牌:有报价不等于可成交;涨停买不到、跌停卖不掉要进入状态与流动性模型,而非用收盘价默认成交。
  • 股数与价格:申报数量、最小变动价位、零股和板块规则须按标的主数据处理,不统一硬编码成“全部 100 股”。
  • 空头:低分组卖空必须考虑融券资格、券源和费用,不能假定随时可借。
  • 费用与容量:税费按日期和方向;佣金最低收费、价差、冲击与参与率限制要反映订单大小。高 IC 但高换手、短半衰期,可能执行后没有优势。

8.1 给熟悉子单 / FIX 的工程师

订单管理系统(Order Management System,OMS)收到的不是“买高分股”四个字,而是目标差额、限额、时效与关联版本。一个信号可以对应多笔母子单;子单取消不等于因子失效,也不等于母单目标取消。

1
2
3
研究意图 → 目标组合 → 调仓批次 → 母单 → 子单 → 净成交
净成交 / 未成交 / 费用 → 交易成本分析(Transaction Cost Analysis,TCA)
TCA → 校准回测的延迟、成交率、冲击与机会成本

金融信息交换协议(Financial Information eXchange,FIX)中,35 为消息类型、39 为订单状态(OrdStatus)、150 为执行类型(ExecType);具体以版本与券商字典为准。按 ExecID 去重,CumQty 是累计成交,不能逐条相加;撤单已发≠已撤,撤单拒绝≠原单拒绝。

例如高分股只成交目标的 30%,研究回测若默认 100% 成交,就混淆了“信号有没有预测力”和“实际抓到了多少收益”。对未成交机会成本与成交选择偏差也要评估,不能只统计已成交股票的表现。

九、自测与研究验收清单

① 财报日期是 3 月 31 日,4 月 29 日公告。4 月 1 日可用吗? A 可用 / B 不可用 / C 看后来涨没涨

答案 B。 至少要等公告与系统可用时间,且保留当时版本;报告期末不是信息可用时点。

② ICIR 为正,是否已证明成本后能赚钱? A 是 / B 否 / C 只要超过固定门槛就行

答案 B。 ICIR 衡量截面预测相关性的时间稳定性,不包含仓位、风险、交易成本和可成交性,也没有通用合格线。

③ 每天评价未来 5 日收益,应如何处理重叠? A 按完全独立做检验 / B 检查依赖并使用合适推断 / C 删除所有负收益

答案 B。 可用不重叠评价辅助检查,或对依赖结构使用稳健标准误/块自助法;即使标签不重叠也不保证 IC 独立。

④ 子单剩余量已撤销,应如何理解? A 母单目标自动减少 / B 以目标、净成交和有效在途重新核算 / C 把 CumQty 再累加一次

答案 B。 子单撤销释放预占,但不自动取消投资目标;累计量不能重放累加。

9.1 交付真实因子研究前,逐项核对

  • 假设、方向、股票池、持有期和基准事前写清。
  • 可用时间、历史成分、公司行动与缺失策略可追溯。
  • 公式、依赖、随机种子、快照和所有搜索尝试有版本。
  • 训练/验证/测试隔离;跨界标签被剔除;测试不反复调参。
  • 报告 IC 分布、样本量、不确定性、分组、风险与增量贡献。
  • 净收益、换手、容量、成本敏感性与未成交政策完整。
  • 信号→目标→订单→回报可关联;纸面交易规则已冻结。

十、参考资料与适用边界

  1. Harvey、Liu、Zhu:… and the Cross-Section of Expected Returns。已核对 NBER 摘要:大量因子搜索需要更严格的多重检验框架。论文讨论的特定 t 值结论不是本文的通用验收阈值。
  2. Microsoft Qlib 原始仓库。已核对 README:研究链路涵盖数据、建模、回测、组合与执行,并列出自动因子研究工作。这里只作学习入口,未运行、安装或验证其示例收益。
  3. 上海证券交易所规则入口、深圳证券交易所规则入口。用于下一步按板块、标的、日期逐条核验;本文未逐条核验其现行条文,故未给出固定税率、费率或涨跌幅数字。

本文根据因子挖掘教学材料重新整理。示例只验证计算流程;尚无真实市场、成本后收益或实盘有效性证据。交易规则仍须按板块、标的与生效日期核验。