2026-05-01 · quant
量化交易
从因子研究到生产执行的量化交易全栈工程。覆盖市场微结构、数据管线、因子构造、组合优化、回测方法论、执行算法、做市策略、高频架构到生产运维。面向策略研究员与工程师。






























风险提示:本系列为工程参考,不构成投资建议。文中出现的策略骨架、因子、参数、阈值都是为了说明工程链路而保留的最小例子,不代表任何可直接上线的方法。
写过策略代码的人多半都遇到过这种场景:在 Jupyter 里跑出年化 35%、夏普 2.6、最大回撤 8%,曲线漂亮得不真实;接到模拟盘里第一个月就跌了 4%,再上实盘第一周直接被滑点和手续费吃掉一半收益。然后开始返工:是不是用了未来函数?是不是停牌没处理?是不是涨跌停日成交假设过乐观?是不是回测里没扣印花税?是不是复权方式搞错了?是不是把因子值用了今天才发布的财报?
这些问题不是策略写得不好,是工程链路没接好。量化交易的难点从来不是想到一个能赚钱的想法,而是把一个有效的想法在不被工程坑吃掉的前提下落到实盘。这条链路从一根 K 线进入数据库,到一笔订单从交易所成交回报回流,要走过至少八段,每一段都有自己的输入输出、失败模式与不变量。
这个系列的目标,是把这条链路上每一段的工程方法讲清楚。我们不讲”明天涨什么”,我们讲:
这是【量化交易】系列的第 1 篇,承担两件事:第一,把整条链路的骨架画清楚,让后续 28 篇每篇都能挂回这张图;第二,给出一份”研究 → 上线”的流程清单,让读者在自己写第一条策略之前就知道每一步的卡点在哪。
量化交易(Quantitative Trading)这个词在工业界被用得很宽:它既包括”用 Python 跑回测、买基金”的散户量化,也包括”FPGA 解 ITCH 包做高频做市”的机构 HFT,中间还有股票多因子、CTA 趋势跟踪、统计套利、可转债套利、加密做市、跨期跨品种套利。这些事看起来差异很大,但都满足三个共同特征:
这三条同时成立的系统,就是量化系统。少一条都不算:
主观投资(discretionary trading)与量化的边界并不在”用不用电脑”,而是在”决策是否可复现”。一位基金经理把所有买卖逻辑写在 Excel 里逐单录入,仍然是主观;一套基于神经网络的策略,如果同样的输入永远给出同样的输出,就是量化,不论里面藏着多少黑盒。
把量化系统拆成四个层次,对后续每一篇都有用:
| 层次 | 含义 | 输出物 | 典型角色 |
|---|---|---|---|
| 因子(Factor) | 对某种市场异象的可计算度量 | 截面数列:每个标的每个时点一个数 | 研究员 |
| 信号(Signal) | 把因子转换成”对未来收益的预测” | 预期收益向量 + 不确定度 | 策略研究员 |
| 策略(Strategy) | 信号 + 风险 + 执行约束 → 目标持仓 | 目标持仓与目标交易 | 策略工程师 |
| 系统(System) | 把策略落地的全部基础设施 | 一条能稳定运转的流水线 | 量化工程师 / SRE |
很多团队的事故都来自层次混淆。研究员认为”因子做得好就行了”,工程师认为”系统稳定就行了”,没人对”信号到策略到系统”那一段负责,于是因子在研究环境是有效的,进了实盘就失效。本系列在每一篇都会标注它在哪个层次:
工程文章不预测市场,但”系统设计为什么这么设”绕不开这个问题。市场里能被量化系统稳定收割的钱,大致来自下面几类:
不同钱对应不同的工程要求:HFT 拼时延,统计套利拼协整稳定性,基本面量化拼数据与因子库,做市拼库存控制与风控速度。后续每个具体策略类的篇章都会先回答”它在赚什么钱”,再讲怎么实现。
工程方法不是万能。下面这些问题,再好的系统也解决不了:
工程只能保证”如果策略在历史上是有效的,工程不会把它的有效性吃掉”。能不能找到一个真正有效的策略,是另一回事。
把整个量化系统的数据流拆开,从左到右一定经过这八段:
数据 → 特征 → 因子 → 信号 → 组合 → 执行 → 风控 → 复盘
↑
└── 反馈到数据/特征/信号
每一段都有清晰的输入输出,每一段都有自己的失败模式:
| 段 | 输入 | 输出 | 主要失败模式 |
|---|---|---|---|
| 数据 | 交易所原始行情、基本面、参考数据 | 干净、对齐、PIT 安全的表 | 时间错位、复权错、停牌缺失 |
| 特征 | 干净表 | 标的 × 时点的数值矩阵 | 未来函数、横截面泄漏 |
| 因子 | 特征矩阵 | 经过中性化、标准化的因子 | 极端值、行业暴露、风格暴露 |
| 信号 | 因子 + 模型 | 预期收益与不确定度 | 过拟合、时序泄漏、样本不平衡 |
| 组合 | 信号 + 风险模型 + 约束 | 目标持仓 | 协方差病态、约束不可行 |
| 执行 | 目标持仓 - 当前持仓 | 实际成交 | 滑点、冲击成本、未成交残单 |
| 风控 | 全链路状态 | 放行/限制/熔断 | 漏拦、误杀、规则缺口 |
| 复盘 | 实际成交、行情、信号 | 归因报表与反馈 | 归因方法错误、口径不一致 |
下面这张系统架构图把这八段画成层,并标出每段对应的本系列章节:
图里有三种箭头:
很多团队画系统图只画前向,会漏掉两件事:风控的控制流和成交回报的回流。只要有一处回流被切断,复盘就会和实盘对不上,时间一长策略就成了黑盒。
数据这一段决定整条链路的下限。它的工作不是”把行情存进去”,而是回答一个问题:你能不能在 t 时刻只用 t-ε 之前真实可得的数据做决策。这个能力叫 PIT(point-in-time)安全。
一份不 PIT 安全的数据有很多种死法:
数据段的不变量大致有四条:
asof,与”事件发生时间戳”event_time
分开存;WHERE asof <= t,不允许出现
WHERE event_time = t;asof,不能用”当前最新的复权”。第 5、6、7 篇会把这四条不变量在工程上怎么做钉死。
特征是从数据里算出来的”中间变量”,因子是把特征变成”对截面有信息量的数列”。两者的边界没那么清晰,但工程上有个朴素的区分:
工程上有几个反复犯的错误:
第 7、9 篇专门讲因子工厂、第 8 篇讲另类数据特征,每一类问题都会给出代码级的处理方案。
信号是把因子翻译成”预期收益”。最简单的是单因子线性映射,复杂的是 LightGBM 或 LSTM 给出条件分布。但不论模型多复杂,输出都要落到两个量上:
alpha_i;sigma_i,或更一般地,整张协方差矩阵
Sigma。组合优化的工作是把 alpha 和
Sigma 在一组约束下变成目标持仓
w*:
maximize w^T alpha - 0.5 * lambda * w^T Sigma w - cost(w, w_prev)
subject to sum(w) = 1(或 0,市场中性)
|w_i| <= w_max
beta_exposure(w) ∈ [-b, b]
industry_exposure(w) ∈ [-h, h]
turnover(w, w_prev) <= T
这是教科书写法。工程上需要回答更朴素的问题:
Sigma
怎么估计才能不病态?历史协方差直接用,标的多了立马奇异;w_prev?第 15、16、17 篇会给出具体的工程化方案,特别是
cost
函数的写法——它既要包含手续费、印花税,也要包含冲击成本的二次项,不然优化器会算出一份在”没有市场摩擦”假设下漂亮、在真实市场里瞬间被冲击吃掉的目标持仓。
执行是被低估得最厉害的一段。研究员写完信号交给”执行模块”的那一刻,往往觉得自己的工作做完了;但一份目标持仓和一份实际成交之间的差距,常常比信号本身的 Alpha 还大。
执行段要回答的问题:
这些问题对应不同的执行算法:VWAP(成交量加权平均价)、TWAP(时间加权平均价)、POV(参与率)、IS(实施差额,Implementation Shortfall)、Adaptive。第 23、24 篇会讲执行算法与智能订单路由(SOR),并给出”执行成本怎么测、怎么归因”的方法。
执行段的不变量有两条最关键:
client_order_id,与回报匹配;这两条在第 27 篇 OMS/EMS 架构里展开。
风控分三层,对应三个不同的时间尺度:
工程上最常见的风控故障:
第 16、19、25 篇会分别讲撮合层、组合层、做市层的风控做法。本系列的核心立场是:风控必须是独立通道,必须有”安全默认是拒绝”的能力,必须能在毫秒级熔断。
复盘的工作不是事后写报告,是把实盘结果反哺回研究流程。一份合格的复盘要回答:
第 22 篇会给出一套”实盘 - 回测差异分解”的口径模板。这一段的工程要点是口径一致:复盘用的费率、滑点假设、撮合假设必须和回测引擎完全一致,否则你比的是两套系统而不是策略本身的有效性。
普通系统里的时间通常只有一种:墙上时钟。量化系统里至少有六种,必须分清:
| 时间名 | 含义 | 典型来源 |
|---|---|---|
| 事件时间(event_time) | 事件在现实中发生的时间 | 交易所撮合时间、财报披露时间 |
| 采集时间(capture_time) | 数据被你的系统首次采到的时间 | 行情接收器入队时间 |
| 可见时间(asof) | 市场上一般参与者能看到该数据的时间 | 撮合时间 + 网络延迟 |
| 决策时间(decision_time) | 策略基于已知数据做出决策的时间 | 信号生成时刻 |
| 下单时间(submit_time) | 订单离开本系统的时间 | 网卡发出时间 |
| 成交时间(exec_time) | 交易所撮合该订单的时间 | 成交回报中的时间字段 |
回测里只能用
asof <= decision_time,不能用
event_time <= decision_time。事件时间小于决策时间不代表那个时点你能看到,这是新手最常踩的坑。
未来函数(look-ahead bias)是指一个本应只能用 t 时刻及以前数据计算的量,事实上偷用了 t 之后的数据。常见形态:
pandas.rolling(20).mean()
默认右对齐,没问题;但 rolling(20, center=True)
就是中心对齐,会用到未来 10 个点;(x - x.mean()) / x.std()
做归一化,均值方差用了全样本,等于偷看未来;fillna(method='bfill')
用未来值回填;report_date 联到行情上,但市场看到财报要等
announce_date(通常晚 30–90 天);工程上防止未来函数有三层:
asof,查询封装强制
asof <= t;center=True,禁止整体标准化(用
expanding 或滚动窗口替代);复权和停牌看起来是数据问题,本质是时间问题。
复权有三种:不复权(原始价)、前复权、后复权。前复权是最常用的,但它的复权因子依赖未来的所有除权除息事件。一个回测如果用”今天最新的前复权数据”在历史时点上做信号,整段历史的价格已经被未来事件污染了。
正确做法:保留原始价 + 一张 (asof, factor)
表,回测查询时按 asof
取当时已知的复权因子。等价的实现是后复权(以历史某点为基准向后累乘),它有一个好性质——历史值固定不变,后续除权事件只追加新数据,不修改老数据。
停牌带来的问题更隐蔽:
pct_change
算出 0% 收益,假装连续;第 6 篇会给出停牌处理的几条标准动作。
研究、回测、实盘三套环境对时间的处理是不同的,工程上必须对齐:
| 环境 | 时间推进方式 | 数据可见性 | 撮合假设 |
|---|---|---|---|
| 研究 | 事后批处理 | 全样本可见 | 不撮合 |
| 回测 | 按 bar 推进 | asof <= t |
模拟撮合(滑点/延迟模型) |
| 实盘 | 真实墙上时钟 | 真实可见 | 真实撮合 |
研究阶段允许用全样本,但只能用于”探索”,不能用于评价策略表现;回测阶段必须严格按时间推进;实盘则没有”重来一次”的机会。
第 19 篇专门讲回测引擎的时间引擎怎么写,第 20 篇讲常见的时间相关坑(前视、生存者偏差、再平衡时机偏差)。
量化系统的设计思路在买方与卖方之间差异很大:
简单粗暴的对照:
| 维度 | 买方 | 卖方 |
|---|---|---|
| 目标函数 | 风险调整后收益 | 流动性提供 + 价差 + 佣金 |
| 时延要求 | 多数策略秒级到日级,少数毫秒 | 几乎全部微秒级 |
| 持仓方向 | 主动选 Alpha | 被动跟随客户/市场 |
| 风控重点 | 组合层风险 | 库存与单笔风险 |
| 系统重点 | 研究 → 回测 → 组合优化 → 执行 | 行情 → 撮合/做市 → 风控 → 清结算 |
买方系统重在研究链路:数据平台、因子库、回测引擎、组合优化器、归因分析。执行通常用券商提供的算法或外部 EMS,不必自己造。
卖方系统重在低延迟链路:行情解码、撮合、做市报价、风控网关、订单路由、监管报送。研究在卖方更多体现为定价模型与做市参数调优,不是 Alpha 挖掘。
两者中间还有一类——量化做市与高频套利,介于买方与卖方之间,靠极低延迟在订单簿上做被动或半主动报价。第 25、26 篇会专门讲做市与 HFT 架构。
本系列以买方视角为主线(第 1–22 篇),同时覆盖做市/HFT 与系统工程(第 23–28 篇)。读者可以按自己角色选择重点。
下面这张表是多市场量化的”地理常识”,必须记住:
| 维度 | A 股 | 港股 | 美股 | 国内期货 | 加密(中心化) |
|---|---|---|---|---|---|
| 撮合机制 | 集合竞价 + 连续竞价 | 同左 | 连续竞价 + 收盘竞价 | 集合 + 连续 | 连续竞价(订单簿) |
| 交易制度 | T+1 现货 | T+0 | T+0 | T+0 | T+0 |
| 涨跌停 | ±10%(创业板/科创板 ±20%,ST ±5%) | 无 | 无 | 各品种不同 | 多数无 |
| 最小变动价位 | 0.01 元 | 0.001/0.005/0.01 等 | 0.01 美元(个别 0.0001) | 各品种不同 | 各币对不同 |
| 卖空 | 融券受限,标的池小 | 可做空 | 可做空 | 可做空(双向) | 可永续合约做空 |
| 交易费率 | 佣金(万 1–3)+ 印花税(卖出千 1)+ 过户费 | 印花税 + 佣金 + 交易征费 | 极低佣金 + SEC 费 | 万 0.5 左右 | 0.02%–0.1% |
| 数据 Level 2 | 可买(券商/数据商) | 可买 | NASDAQ ITCH / NYSE Pillar | 可买 | API 公开 |
| 交易时间 | 9:30–11:30 / 13:00–15:00 | 9:30–12:00 / 13:00–16:00 | 9:30–16:00 ET | 多时段含夜盘 | 7×24 |
每一栏背后都对应回测里的一行代码:
w_prev 要分”可卖部分”和”今日新买部分”;A 股是国内量化的主战场,有几个其他市场少见的约束,单独列出:
第 2 篇会把这些约束铺开讲,并给出回测引擎里对应的开关。
加密市场的工程难点和传统市场不一样:
第 14 篇专门讲加密策略的工程化,包括跨所搬砖、资金费率套利、做市等。
研究流程图把”一个想法到一笔实盘订单”中间的所有关键卡点串起来:
整个流程有 12 个步骤,6 道关键卡点(G1–G6)。每一道卡点都是一次”go / no-go” 决定,不通过就回到上一步或者放弃。
1. 假设
2. 因子构造
asof 字段;卡点 G1:因子 PIT 校验通过。把因子计算往前挪一天,结果应该完全一致;如果不一致说明哪里偷看了未来。
3. 单因子检验
卡点 G2:Rank IC 显著(t > 2)、IC_IR > 0.5、衰减半衰期 ≥ 调仓周期。不通过则回到第 2 步调整因子定义。
4. 多因子合成
5. 组合构建
6. 回测
卡点 G3:扣除真实费率与冲击成本后,样本外 Sharpe 仍然显著为正、最大回撤可控、收益分布无极端尾部依赖。
7. Walk-forward 验证
8. 容量估计
impact ∝ σ * sqrt(Q/ADV);9. 模拟盘
卡点 G4:模拟盘与回测的日收益相关性 > 0.8,日均偏差 < 一定阈值,滑点偏差能解释清楚来源。
10. 灰度上线
卡点 G5:灰度期内监控指标无系统性漂移,无非预期事件。
11. 全量上线
12. 复盘归因
卡点 G6:归因模型对实盘 - 回测差异的解释力 > 阈值,否则视为偶然或存在未识别风险。
| 步骤 | 常见卡点 | 处理 |
|---|---|---|
| 1 | 假设含混无法证伪 | 写一句话假设 |
| 2 | 因子值含未来数据 | PIT 校验脚本 |
| 3 | IC 显著但分组不单调 | 检查极值与行业暴露 |
| 4 | 多因子合成后 IC 反而下降 | 因子相关性筛选 |
| 5 | 优化器无解 / 极端解 | 约束放宽 + 协方差收缩 |
| 6 | 回测漂亮、滑点崩 | 加更激进的冲击模型 |
| 7 | 参数曲面陡 | 视为过拟合,丢弃 |
| 8 | 容量太小 | 直接放弃或按容量缩规模 |
| 9 | 模拟与回测偏差大 | 检查回测假设、对齐口径 |
| 10 | 灰度期 IC 衰减 | 暂停,研究是否结构性失效 |
| 11 | 实盘异常 | 熔断 |
| 12 | 归因解释力不足 | 视为偶然,暂停加仓 |
【量化交易】系列覆盖以下内容:
下面这些不在本系列覆盖范围:
【量化交易】与【金融科技工程】是互补的。简单划线:
两套系统在交易所/经纪商侧汇合:撮合是金融科技的工作,发单与回报是量化交易的工作。
与【分布式系统】系列也有交叉:消息总线、时序数据库、共识算法,量化系统都用得上,但本系列不重复讲分布式基础,需要时引用 【分布式系统】系列。
整个系列共 28 篇,不同读者可以按下面四条路径选读。
目标:建立量化的世界观,能跑通第一个回测。
读完这 7 篇能写出一个”不犯严重错误的回测”。
目标:理解多种策略类型与适用边界。
策略路径读完,对”市面上的量化策略大概在做什么”有结构性认知。
目标:把策略变成稳定运行的系统。
工程路径是给量化工程师准备的”系统书”。
目标:进入低延迟领域。
HFT 路径要求对网络、内核、硬件加速有基础。
文章末尾给三段最小可运行代码,分别覆盖数据加载、因子计算、回测骨架。它们的目的是让读者立刻有”代码触感”,后续每一篇都会在更具体的场景上扩展。
下面这段用 polars 读一份本地 parquet 行情,强制按
asof 过滤。环境:Python 3.11、polars 1.x。
import polars as pl
from datetime import date
def load_pit_safe(
path: str,
decision_date: date,
universe: list[str] | None = None,
) -> pl.DataFrame:
"""读取 PIT 安全的日频行情。
要求 parquet 必须包含字段:
- symbol: str
- event_date: date 该 bar 对应的交易日
- asof: datetime 数据被市场看到的时间
- open / high / low / close / volume
- adj_factor: float 截至 asof 已知的复权因子
"""
df = (
pl.scan_parquet(path)
.filter(pl.col("asof") <= pl.lit(decision_date))
.group_by(["symbol", "event_date"])
.agg([
pl.col("open").last(),
pl.col("high").last(),
pl.col("low").last(),
pl.col("close").last(),
pl.col("volume").last(),
pl.col("adj_factor").last(),
pl.col("asof").last(),
])
.sort(["symbol", "event_date"])
)
if universe is not None:
df = df.filter(pl.col("symbol").is_in(universe))
return df.collect()要点:
asof <= decision_date 过滤,再按
(symbol, event_date) 取
last。这样保证拿到的是”在 decision_date
之前最新可见”的版本,不是事件时间为 decision_date
的最新值;adj_factor 跟着 asof
走,回测查询时直接用,不要拿”今天最新的复权因子”;PIT 校验脚本可以这样写:把 decision_date
往前挪一天再跑一遍,结果应该是上一版的子集。如果出现”挪前一天反而多出一些行”,说明
asof 字段被弄脏了。
下面是一个最简单的”过去 20 日动量、行业中性化、截面 z-score” 因子。环境:numpy 1.26、pandas 2.x、scipy 1.11。
import numpy as np
import pandas as pd
def neutralize_by_group(values: pd.Series, groups: pd.Series) -> pd.Series:
"""对每个组内做去均值。"""
return values - values.groupby(groups).transform("mean")
def zscore_clip(s: pd.Series, n_std: float = 3.0) -> pd.Series:
"""先做 MAD 截尾,再 z-score。"""
med = s.median()
mad = (s - med).abs().median() * 1.4826
if mad == 0 or np.isnan(mad):
return pd.Series(np.zeros(len(s)), index=s.index)
clipped = s.clip(med - n_std * mad, med + n_std * mad)
return (clipped - clipped.mean()) / (clipped.std(ddof=0) + 1e-12)
def momentum_factor(
px: pd.DataFrame, # 行=日期, 列=symbol, 值=后复权收盘价
industry: pd.Series, # index=symbol, 值=行业代码
lookback: int = 20,
) -> pd.DataFrame:
"""过去 lookback 日动量 + 行业中性 + z-score。"""
# 关键:shift(1) 保证当日因子只用 t-1 及以前的价格
ret = px.pct_change(lookback).shift(1)
out = pd.DataFrame(index=ret.index, columns=ret.columns, dtype=float)
for dt, row in ret.iterrows():
valid = row.dropna()
if len(valid) < 50:
continue
ind = industry.reindex(valid.index)
neutralized = neutralize_by_group(valid, ind)
out.loc[dt, valid.index] = zscore_clip(neutralized).values
return out要点:
shift(1) 是防止信号偷看同一根 bar
的收盘——典型的未来函数;clip(quantile(0.01), quantile(0.99))
也行;< 50)跳过这一天,避免噪音放大。下面是一段向量化回测骨架。它做出三个关键工程决定:信号 t → 持仓 t+1、按 t+1 开盘价成交、滑点 + 比例费率扣除。环境:pandas 2.x、numpy 1.26。
import numpy as np
import pandas as pd
def vector_backtest(
factor: pd.DataFrame, # 因子 z-score
open_px: pd.DataFrame, # 开盘价
close_px: pd.DataFrame, # 收盘价
top_pct: float = 0.1, # 多头分位
bot_pct: float = 0.1, # 空头分位
fee_bps: float = 3.0, # 单边费率(基点)
slip_bps: float = 5.0, # 单边滑点(基点)
) -> dict:
"""多空对冲、等权、日频再平衡、市场中性。"""
# 1) 由因子在 t 决定持仓,t+1 开盘成交 → 持仓在 t+1 开始生效
rank = factor.rank(axis=1, pct=True)
long_mask = rank >= (1 - top_pct)
short_mask = rank <= bot_pct
n_long = long_mask.sum(axis=1).replace(0, np.nan)
n_short = short_mask.sum(axis=1).replace(0, np.nan)
weight = pd.DataFrame(0.0, index=factor.index, columns=factor.columns)
weight = weight.where(~long_mask, (1.0).__rdiv__(n_long), axis=0) \
if False else weight # 写法兼容,下面用更直接的方式
weight[long_mask] = (long_mask.div(n_long, axis=0))[long_mask]
weight[short_mask] = -(short_mask.div(n_short, axis=0))[short_mask]
# 2) 信号在 t 形成,权重在 t+1 起效
weight = weight.shift(1)
# 3) t+1 收益 ≈ (close_{t+1} - open_{t+1}) / open_{t+1} 用于持有期内
# 简化为 t+1 close 相对 t close 的收益,再扣除入场滑点
ret_close = close_px.pct_change()
gross = (weight * ret_close).sum(axis=1)
# 4) 换手成本:|w_t - w_{t-1}| * (fee + slip) 单边
turnover = (weight - weight.shift(1)).abs().sum(axis=1)
cost = turnover * (fee_bps + slip_bps) / 1e4
pnl = gross - cost
# 5) 指标
ann = pnl.mean() * 252
vol = pnl.std() * np.sqrt(252)
sharpe = ann / vol if vol > 0 else np.nan
max_dd = (pnl.cumsum() - pnl.cumsum().cummax()).min()
return {
"pnl": pnl,
"turnover": turnover,
"ann_return": ann,
"ann_vol": vol,
"sharpe": sharpe,
"max_drawdown": float(max_dd),
}要点:
weight.shift(1) 是不可省略的一步——信号 t
形成、t+1 开始持有,回测里漏掉这一步等于用未来数据;|w_t - w_{t-1}|
直接近似,单边费率与滑点合并到 bps;向量化回测的最大用途是因子早期筛选——从几千个候选因子里快速挑出几十个可能有效的,再交给事件驱动回测做精测。第 19、20 篇会讲两套引擎怎么配合。
光算因子还不够,必须配套一段 IC 计算来评价它。下面这段计算每日横截面 Spearman 相关,并给出 Rank IC 的均值、标准差、IC_IR、t 统计量与衰减曲线。
import numpy as np
import pandas as pd
from scipy.stats import spearmanr
def rank_ic(
factor: pd.DataFrame, # 行=日期, 列=symbol
fwd_ret: pd.DataFrame, # 同结构,前瞻收益(已对齐到因子日期)
) -> pd.Series:
"""每日横截面 Rank IC。"""
out = {}
for dt in factor.index:
f = factor.loc[dt].dropna()
r = fwd_ret.loc[dt].reindex(f.index).dropna()
f = f.reindex(r.index)
if len(f) < 50:
continue
rho, _ = spearmanr(f.values, r.values)
out[dt] = rho
return pd.Series(out).sort_index()
def ic_summary(ic: pd.Series) -> dict:
n = ic.dropna().shape[0]
mean = ic.mean()
std = ic.std(ddof=1)
return {
"n_days": n,
"ic_mean": mean,
"ic_std": std,
"ic_ir": mean / std if std > 0 else np.nan,
"t_stat": mean / (std / np.sqrt(n)) if std > 0 else np.nan,
"win_rate": (ic > 0).mean(),
}
def ic_decay(
factor: pd.DataFrame,
px: pd.DataFrame,
horizons: list[int] = [1, 5, 10, 20, 60],
) -> pd.DataFrame:
"""衰减曲线:不同前瞻天数下的 Rank IC 均值。"""
rows = []
for h in horizons:
fwd = px.pct_change(h).shift(-h)
ic = rank_ic(factor, fwd)
s = ic_summary(ic)
rows.append({"horizon": h, **s})
return pd.DataFrame(rows)判定准则(仅做参考,具体阈值与策略类型有关):
ic_mean 量级:日频股票多因子典型在
0.02–0.05 之间,超过 0.10 多半是数据问题;ic_ir ≥ 0.5 通常被视为可考虑入库;t_stat ≥ 2 视为统计显著;回测对性能敏感时,用 numba 加速核心循环、用 vectorbt 做大规模参数扫描是常见选择。两者的边界:
第 19 篇会把三种方案在同一个策略上做基准对比,给出选择建议。
回测引擎是所有量化系统的核心基础设施之一。市面上主流方案大致分四类,工程取舍不同。
代表:自己写的 pandas/numpy/polars 流水线、vectorbt。
代表:zipline、backtrader、自研事件循环。
代表:自研 tick 级订单簿重建 + 撮合模拟器。
代表:把信号接到真实行情、模拟下单、影子持仓核算。
四类方案在一个完整研究流水线里通常会组合使用:向量化做粗筛 → 事件驱动做精测 → 影子账户做最终验证。第 19 篇会给出三套环境如何对齐口径。
下面这张表列出本系列默认假设的工具栈。读者用别的工具栈也行,关键是把每一类需求找到对应工具。
| 类别 | 默认工具 | 备选 |
|---|---|---|
| 数据处理(DataFrame) | polars 1.x、pandas 2.x | duckdb |
| 数值计算 | numpy 1.26 | jax |
| 加速 | numba | cython、rust pyo3 |
| 时序数据库 | clickhouse、questdb | influxdb、timescaledb |
| 列存 | parquet(zstd 压缩) | orc |
| 消息总线 | redis stream、kafka | nats、pulsar |
| 回测向量化 | vectorbt | bt、backtesting.py |
| 回测事件驱动 | zipline-reloaded、backtrader | 自研 |
| 组合优化 | cvxpy + ECOS/Clarabel | scipy.optimize、pyportfolioopt |
| ML 模型 | lightgbm、xgboost、catboost | sklearn |
| 深度学习 | pytorch | tensorflow |
| 行情接入 | ctp(期货)、xtquant、ib_insync | 直连交易所 SDK |
| 可视化 | plotly、altair | matplotlib、seaborn |
| 监控 | prometheus + grafana | datadog |
后续每一篇都会标注它使用的工具版本。当某段代码依赖特定版本时(例如 polars 0.x 与 1.x 之间 API 变了),会显式声明。
工程链路上反复看到的几个反模式,列出来作为反例:
漂亮的回测有 N 种来源:未来函数、生存者偏差、过拟合、容量假设、滑点假设过乐观、费率忘记扣、训练样本与检验样本未隔离、参数在几百次试错后挑出来的。任何一种都能让回测看起来比实盘强 5–10 倍。
工程上的对策:每一份回测必须配套交付以下检查:
最危险的反模式。研究环境通常没有风控、没有限速、没有幂等、没有状态恢复。直连实盘的后果是策略 bug 直接变成资金事故。
工程上的对策:研究环境永远不能拿到真实账户的下单凭证。下单链路必须经过 OMS/EMS,OMS/EMS 必须开启所有风控开关。第 27 篇会给出环境隔离的具体方案。
风控如果和策略写在一起,策略 bug 会一并把风控绕过。Knight Capital 2012 年的 4.4 亿美元事故就是典型——老代码模块被误激活,没有独立风控通道兜底。
工程上的对策:风控必须是独立通道,物理上独立的进程、独立的部署、独立的代码仓库(或至少独立的代码路径)。策略再怎么 bug 也不能污染风控状态。
很多新写的系统会把”持仓”算成本地状态:上次目标 + 上次成交 = 当前持仓。问题是网络抖动、回报丢失、撤单回报延迟都会让本地估算和交易所真实持仓漂移。漂移一旦发生,风控就会基于错误数据放行错误订单。
工程上的对策:本地状态只作为辅助,真实持仓以交易所回报为准,每分钟(或每次下单前)做一次对账,发现漂移就停止下单并告警。
实盘表现不好,复盘时把回测的滑点假设调高一点、费率算少一点,让数据”对得上”。这是自欺。
工程上的对策:回测口径和复盘口径字段级一致,由同一份配置文件生成。任何口径修改必须同步两端,并对历史所有跑过的回测重跑一遍。第 22 篇会给出口径管理的工程做法。
夏普比率假设收益正态分布,对厚尾不敏感。一个策略 250 个交易日里 248 天小赚、2 天大亏 50%,年终统计看 Sharpe 仍然不错,但实盘上你扛不住那 2 天。
工程上的对策:评价策略至少看四个指标:Sharpe、最大回撤、Calmar(年化收益/最大回撤)、收益分布的偏度峰度。任何一个不达标都不上线。第 22 篇会展开指标体系。
把整篇文章的工程要点压缩到一份清单,写在墙上每天看一遍,能避免 80% 的事故:
asof,查询永远
WHERE asof <= t;asof
的复权因子表,禁止用”今日最新”复权数据回测;decision_date
往前挪一天,结果必须是子集;client_order_id,与回报匹配;每一条都对应后续某一篇的工程方法。本系列的所有内容,本质上都是在帮你把这份清单从一行字落到一行代码。
量化交易的工程难度,不在任何一段单独的代码,而在八段链路必须同时正确。一根 K 线数据进来时不被时间错位污染、一个因子算出来时不偷看未来、一份目标持仓在执行时不被滑点吃掉、一笔成交回报回来时能精确匹配上原订单、一次崩盘时风控能在毫秒内把链路熔断——这些事任何一件单看都不难,难的是 365 天每天都对。
这个系列的目标,是让读者读完之后,对每一段链路都有”我知道哪里会出事、我知道怎么守住”的信心。后续 27 篇会把上面的每一条不变量从一句话展开到几千字 + 代码 + 实测。第 2 篇先从市场结构开始:A 股、美股、期货、加密各自的撮合机制、订单类型、交易制度,这是所有后续策略代码的底层假设。
书籍
论文
规范与文档
工具与库
把当前热点继续串成多页阅读,而不是停在单篇消费。
2026-05-01 · quant
从因子研究到生产执行的量化交易全栈工程。覆盖市场微结构、数据管线、因子构造、组合优化、回测方法论、执行算法、做市策略、高频架构到生产运维。面向策略研究员与工程师。
2026-05-01 · quant
把机器学习选股从「跑一个 LightGBM 看 AUC」还原为标签构造、特征中性化、训练协议、模型解释、上线监控五个独立工程问题。重点讨论 Triple-Barrier 标签、Purged K-Fold 与 Embargo、SHAP 归因,给出可直接套用的 Python 代码骨架。
2026-05-01 · quant
把回测引擎当成一套工程系统讲清楚:事件驱动架构、撮合保真度、滑点嵌入、多频率多账户、并行加速、回放对账。给出可运行的最小事件驱动回测器与 vectorbt 向量化对照实现。
2026-05-01 · quant
回测引擎只能保证「语法对」,但真正杀死策略的是「逻辑错、数据脏、推断不严」三件事。本文系统拆解前视偏差(lookahead bias)、过拟合(overfitting)、数据窥视(data snooping)三大陷阱,介绍 Bonferroni、BH-FDR、Family-Wise Error 的多重检验修正,给出 Deflated Sharpe 与概率 Sharpe(PSR)的可运行 Python 实现,配一份 30 条上线前自检清单。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。