土法炼钢兴趣小组的算法知识备份

【量化交易】量化交易全景:从信号到订单的工程链路

文章导航

分类入口
quant
标签入口
#quantitative-trading#alpha#backtest#execution#market-microstructure

目录

风险提示:本系列为工程参考,不构成投资建议。文中出现的策略骨架、因子、参数、阈值都是为了说明工程链路而保留的最小例子,不代表任何可直接上线的方法。

写过策略代码的人多半都遇到过这种场景:在 Jupyter 里跑出年化 35%、夏普 2.6、最大回撤 8%,曲线漂亮得不真实;接到模拟盘里第一个月就跌了 4%,再上实盘第一周直接被滑点和手续费吃掉一半收益。然后开始返工:是不是用了未来函数?是不是停牌没处理?是不是涨跌停日成交假设过乐观?是不是回测里没扣印花税?是不是复权方式搞错了?是不是把因子值用了今天才发布的财报?

这些问题不是策略写得不好,是工程链路没接好。量化交易的难点从来不是想到一个能赚钱的想法,而是把一个有效的想法在不被工程坑吃掉的前提下落到实盘。这条链路从一根 K 线进入数据库,到一笔订单从交易所成交回报回流,要走过至少八段,每一段都有自己的输入输出、失败模式与不变量。

这个系列的目标,是把这条链路上每一段的工程方法讲清楚。我们不讲”明天涨什么”,我们讲:

这是【量化交易】系列的第 1 篇,承担两件事:第一,把整条链路的骨架画清楚,让后续 28 篇每篇都能挂回这张图;第二,给出一份”研究 → 上线”的流程清单,让读者在自己写第一条策略之前就知道每一步的卡点在哪。


一、什么是量化交易:从直觉到工程

1.1 一个不太严谨但够用的定义

量化交易(Quantitative Trading)这个词在工业界被用得很宽:它既包括”用 Python 跑回测、买基金”的散户量化,也包括”FPGA 解 ITCH 包做高频做市”的机构 HFT,中间还有股票多因子、CTA 趋势跟踪、统计套利、可转债套利、加密做市、跨期跨品种套利。这些事看起来差异很大,但都满足三个共同特征:

  1. 决策由可表达为代码的规则或模型驱动,而不是由交易员的主观判断驱动;
  2. 执行由系统自动完成,至少在”何时下单、下多少、撤不撤”这些动作上不依赖人;
  3. 效果可统计度量,能用收益、夏普、回撤、信息比率这些指标评价。

这三条同时成立的系统,就是量化系统。少一条都不算:

主观投资(discretionary trading)与量化的边界并不在”用不用电脑”,而是在”决策是否可复现”。一位基金经理把所有买卖逻辑写在 Excel 里逐单录入,仍然是主观;一套基于神经网络的策略,如果同样的输入永远给出同样的输出,就是量化,不论里面藏着多少黑盒。

1.2 量化的四个层次

把量化系统拆成四个层次,对后续每一篇都有用:

层次 含义 输出物 典型角色
因子(Factor) 对某种市场异象的可计算度量 截面数列:每个标的每个时点一个数 研究员
信号(Signal) 把因子转换成”对未来收益的预测” 预期收益向量 + 不确定度 策略研究员
策略(Strategy) 信号 + 风险 + 执行约束 → 目标持仓 目标持仓与目标交易 策略工程师
系统(System) 把策略落地的全部基础设施 一条能稳定运转的流水线 量化工程师 / SRE

很多团队的事故都来自层次混淆。研究员认为”因子做得好就行了”,工程师认为”系统稳定就行了”,没人对”信号到策略到系统”那一段负责,于是因子在研究环境是有效的,进了实盘就失效。本系列在每一篇都会标注它在哪个层次:

1.3 量化能赚什么钱

工程文章不预测市场,但”系统设计为什么这么设”绕不开这个问题。市场里能被量化系统稳定收割的钱,大致来自下面几类:

不同钱对应不同的工程要求:HFT 拼时延,统计套利拼协整稳定性,基本面量化拼数据与因子库,做市拼库存控制与风控速度。后续每个具体策略类的篇章都会先回答”它在赚什么钱”,再讲怎么实现。

1.4 量化不能解决的事

工程方法不是万能。下面这些问题,再好的系统也解决不了:

工程只能保证”如果策略在历史上是有效的,工程不会把它的有效性吃掉”。能不能找到一个真正有效的策略,是另一回事。


二、量化交易的工程链路

2.1 一张图:八段链路

把整个量化系统的数据流拆开,从左到右一定经过这八段:

数据 → 特征 → 因子 → 信号 → 组合 → 执行 → 风控 → 复盘
                                                     ↑
                                                     └── 反馈到数据/特征/信号

每一段都有清晰的输入输出,每一段都有自己的失败模式:

输入 输出 主要失败模式
数据 交易所原始行情、基本面、参考数据 干净、对齐、PIT 安全的表 时间错位、复权错、停牌缺失
特征 干净表 标的 × 时点的数值矩阵 未来函数、横截面泄漏
因子 特征矩阵 经过中性化、标准化的因子 极端值、行业暴露、风格暴露
信号 因子 + 模型 预期收益与不确定度 过拟合、时序泄漏、样本不平衡
组合 信号 + 风险模型 + 约束 目标持仓 协方差病态、约束不可行
执行 目标持仓 - 当前持仓 实际成交 滑点、冲击成本、未成交残单
风控 全链路状态 放行/限制/熔断 漏拦、误杀、规则缺口
复盘 实际成交、行情、信号 归因报表与反馈 归因方法错误、口径不一致

下面这张系统架构图把这八段画成层,并标出每段对应的本系列章节:

量化交易系统架构

图里有三种箭头:

很多团队画系统图只画前向,会漏掉两件事:风控的控制流和成交回报的回流。只要有一处回流被切断,复盘就会和实盘对不上,时间一长策略就成了黑盒。

2.2 数据:能不能正确用昨天

数据这一段决定整条链路的下限。它的工作不是”把行情存进去”,而是回答一个问题:你能不能在 t 时刻只用 t-ε 之前真实可得的数据做决策。这个能力叫 PIT(point-in-time)安全。

一份不 PIT 安全的数据有很多种死法:

数据段的不变量大致有四条:

  1. 任意一个数据点必须有”被市场看到的时间戳”asof,与”事件发生时间戳”event_time 分开存;
  2. 同一字段被多次发布的,必须按版本保存,不能覆盖;
  3. 任何一个时点 t 的查询,等价于 WHERE asof <= t,不允许出现 WHERE event_time = t
  4. 复权因子要按时点存,回测查询时给复权因子绑定 asof,不能用”当前最新的复权”。

第 5、6、7 篇会把这四条不变量在工程上怎么做钉死。

2.3 特征与因子:把数据变成可比的数

特征是从数据里算出来的”中间变量”,因子是把特征变成”对截面有信息量的数列”。两者的边界没那么清晰,但工程上有个朴素的区分:

工程上有几个反复犯的错误:

第 7、9 篇专门讲因子工厂、第 8 篇讲另类数据特征,每一类问题都会给出代码级的处理方案。

2.4 信号与组合:从预测到目标

信号是把因子翻译成”预期收益”。最简单的是单因子线性映射,复杂的是 LightGBM 或 LSTM 给出条件分布。但不论模型多复杂,输出都要落到两个量上:

组合优化的工作是把 alphaSigma 在一组约束下变成目标持仓 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

这是教科书写法。工程上需要回答更朴素的问题:

第 15、16、17 篇会给出具体的工程化方案,特别是 cost 函数的写法——它既要包含手续费、印花税,也要包含冲击成本的二次项,不然优化器会算出一份在”没有市场摩擦”假设下漂亮、在真实市场里瞬间被冲击吃掉的目标持仓。

2.5 执行:把目标持仓变成成交

执行是被低估得最厉害的一段。研究员写完信号交给”执行模块”的那一刻,往往觉得自己的工作做完了;但一份目标持仓和一份实际成交之间的差距,常常比信号本身的 Alpha 还大。

执行段要回答的问题:

这些问题对应不同的执行算法:VWAP(成交量加权平均价)、TWAP(时间加权平均价)、POV(参与率)、IS(实施差额,Implementation Shortfall)、Adaptive。第 23、24 篇会讲执行算法与智能订单路由(SOR),并给出”执行成本怎么测、怎么归因”的方法。

执行段的不变量有两条最关键:

  1. 每一笔下单必须带一个全链路唯一的 client_order_id,与回报匹配;
  2. 一个交易意图在系统里只能对应一份”在途订单状态”,禁止重复下单。

这两条在第 27 篇 OMS/EMS 架构里展开。

2.6 风控:三层一票否决

风控分三层,对应三个不同的时间尺度:

工程上最常见的风控故障:

第 16、19、25 篇会分别讲撮合层、组合层、做市层的风控做法。本系列的核心立场是:风控必须是独立通道,必须有”安全默认是拒绝”的能力,必须能在毫秒级熔断

2.7 复盘:闭环的最后一段

复盘的工作不是事后写报告,是把实盘结果反哺回研究流程。一份合格的复盘要回答:

第 22 篇会给出一套”实盘 - 回测差异分解”的口径模板。这一段的工程要点是口径一致:复盘用的费率、滑点假设、撮合假设必须和回测引擎完全一致,否则你比的是两套系统而不是策略本身的有效性。


三、关键约束:时间正确性

3.1 量化系统里时间有几种

普通系统里的时间通常只有一种:墙上时钟。量化系统里至少有六种,必须分清:

时间名 含义 典型来源
事件时间(event_time) 事件在现实中发生的时间 交易所撮合时间、财报披露时间
采集时间(capture_time) 数据被你的系统首次采到的时间 行情接收器入队时间
可见时间(asof) 市场上一般参与者能看到该数据的时间 撮合时间 + 网络延迟
决策时间(decision_time) 策略基于已知数据做出决策的时间 信号生成时刻
下单时间(submit_time) 订单离开本系统的时间 网卡发出时间
成交时间(exec_time) 交易所撮合该订单的时间 成交回报中的时间字段

回测里只能用 asof <= decision_time,不能用 event_time <= decision_time。事件时间小于决策时间不代表那个时点你能看到,这是新手最常踩的坑。

3.2 未来函数:从公式到代码

未来函数(look-ahead bias)是指一个本应只能用 t 时刻及以前数据计算的量,事实上偷用了 t 之后的数据。常见形态:

工程上防止未来函数有三层:

  1. 数据层:所有事实表都带 asof,查询封装强制 asof <= t
  2. 特征层:滚动窗口默认右对齐,禁止 center=True,禁止整体标准化(用 expanding 或滚动窗口替代);
  3. 回测层:撮合假设必须延迟一根 bar,即 t 时刻信号只能用 t+1 的开盘价或 VWAP 成交。

3.3 复权与停牌:两种最常见的隐形未来

复权和停牌看起来是数据问题,本质是时间问题。

复权有三种:不复权(原始价)、前复权、后复权。前复权是最常用的,但它的复权因子依赖未来的所有除权除息事件。一个回测如果用”今天最新的前复权数据”在历史时点上做信号,整段历史的价格已经被未来事件污染了。

正确做法:保留原始价 + 一张 (asof, factor) 表,回测查询时按 asof 取当时已知的复权因子。等价的实现是后复权(以历史某点为基准向后累乘),它有一个好性质——历史值固定不变,后续除权事件只追加新数据,不修改老数据。

停牌带来的问题更隐蔽:

第 6 篇会给出停牌处理的几条标准动作。

3.4 三套环境的时间语义

研究、回测、实盘三套环境对时间的处理是不同的,工程上必须对齐:

环境 时间推进方式 数据可见性 撮合假设
研究 事后批处理 全样本可见 不撮合
回测 按 bar 推进 asof <= t 模拟撮合(滑点/延迟模型)
实盘 真实墙上时钟 真实可见 真实撮合

研究阶段允许用全样本,但只能用于”探索”,不能用于评价策略表现;回测阶段必须严格按时间推进;实盘则没有”重来一次”的机会。

第 19 篇专门讲回测引擎的时间引擎怎么写,第 20 篇讲常见的时间相关坑(前视、生存者偏差、再平衡时机偏差)。


四、买方与卖方视角

4.1 角色差异

量化系统的设计思路在买方与卖方之间差异很大:

简单粗暴的对照:

维度 买方 卖方
目标函数 风险调整后收益 流动性提供 + 价差 + 佣金
时延要求 多数策略秒级到日级,少数毫秒 几乎全部微秒级
持仓方向 主动选 Alpha 被动跟随客户/市场
风控重点 组合层风险 库存与单笔风险
系统重点 研究 → 回测 → 组合优化 → 执行 行情 → 撮合/做市 → 风控 → 清结算

4.2 系统设计差异

买方系统重在研究链路:数据平台、因子库、回测引擎、组合优化器、归因分析。执行通常用券商提供的算法或外部 EMS,不必自己造。

卖方系统重在低延迟链路:行情解码、撮合、做市报价、风控网关、订单路由、监管报送。研究在卖方更多体现为定价模型与做市参数调优,不是 Alpha 挖掘。

两者中间还有一类——量化做市与高频套利,介于买方与卖方之间,靠极低延迟在订单簿上做被动或半主动报价。第 25、26 篇会专门讲做市与 HFT 架构。

本系列以买方视角为主线(第 1–22 篇),同时覆盖做市/HFT 与系统工程(第 23–28 篇)。读者可以按自己角色选择重点。


五、市场覆盖:A 股、美股、期货、加密

5.1 市场结构差异速查表

下面这张表是多市场量化的”地理常识”,必须记住:

维度 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

每一栏背后都对应回测里的一行代码:

5.2 A 股的几个特殊约束

A 股是国内量化的主战场,有几个其他市场少见的约束,单独列出:

第 2 篇会把这些约束铺开讲,并给出回测引擎里对应的开关。

5.3 加密的特殊性

加密市场的工程难点和传统市场不一样:

第 14 篇专门讲加密策略的工程化,包括跨所搬砖、资金费率套利、做市等。


六、研究流程:从想法到上线

6.1 一张图

研究流程图把”一个想法到一笔实盘订单”中间的所有关键卡点串起来:

量化研究流程

整个流程有 12 个步骤,6 道关键卡点(G1–G6)。每一道卡点都是一次”go / no-go” 决定,不通过就回到上一步或者放弃。

6.2 每一步的关键动作与卡点

1. 假设

2. 因子构造

卡点 G1:因子 PIT 校验通过。把因子计算往前挪一天,结果应该完全一致;如果不一致说明哪里偷看了未来。

3. 单因子检验

卡点 G2:Rank IC 显著(t > 2)、IC_IR > 0.5、衰减半衰期 ≥ 调仓周期。不通过则回到第 2 步调整因子定义。

4. 多因子合成

5. 组合构建

6. 回测

卡点 G3:扣除真实费率与冲击成本后,样本外 Sharpe 仍然显著为正、最大回撤可控、收益分布无极端尾部依赖。

7. Walk-forward 验证

8. 容量估计

9. 模拟盘

卡点 G4:模拟盘与回测的日收益相关性 > 0.8,日均偏差 < 一定阈值,滑点偏差能解释清楚来源。

10. 灰度上线

卡点 G5:灰度期内监控指标无系统性漂移,无非预期事件。

11. 全量上线

12. 复盘归因

卡点 G6:归因模型对实盘 - 回测差异的解释力 > 阈值,否则视为偶然或存在未识别风险。

6.3 每一步的常见卡点

步骤 常见卡点 处理
1 假设含混无法证伪 写一句话假设
2 因子值含未来数据 PIT 校验脚本
3 IC 显著但分组不单调 检查极值与行业暴露
4 多因子合成后 IC 反而下降 因子相关性筛选
5 优化器无解 / 极端解 约束放宽 + 协方差收缩
6 回测漂亮、滑点崩 加更激进的冲击模型
7 参数曲面陡 视为过拟合,丢弃
8 容量太小 直接放弃或按容量缩规模
9 模拟与回测偏差大 检查回测假设、对齐口径
10 灰度期 IC 衰减 暂停,研究是否结构性失效
11 实盘异常 熔断
12 归因解释力不足 视为偶然,暂停加仓

七、本系列的边界

7.1 我们讲什么

【量化交易】系列覆盖以下内容

7.2 我们不讲什么

下面这些不在本系列覆盖范围:

7.3 与其他系列的引用关系

【量化交易】与【金融科技工程】是互补的。简单划线:

两套系统在交易所/经纪商侧汇合:撮合是金融科技的工作,发单与回报是量化交易的工作。

与【分布式系统】系列也有交叉:消息总线、时序数据库、共识算法,量化系统都用得上,但本系列不重复讲分布式基础,需要时引用 【分布式系统】系列


八、阅读路径

整个系列共 28 篇,不同读者可以按下面四条路径选读。

8.1 入门路径

目标:建立量化的世界观,能跑通第一个回测。

  1. 第 1 篇(本篇)
  2. 第 2 篇 市场结构
  3. 第 5 篇 数据管道
  4. 第 6 篇 生存者偏差
  5. 第 9 篇 因子库入门
  6. 第 19 篇 回测引擎
  7. 第 22 篇 绩效指标

读完这 7 篇能写出一个”不犯严重错误的回测”。

8.2 策略路径

目标:理解多种策略类型与适用边界。

  1. 第 9 篇 因子库
  2. 第 10 篇 统计套利
  3. 第 11 篇 事件驱动
  4. 第 12 篇 ML Alpha
  5. 第 13 篇 DL 时序
  6. 第 14 篇 加密
  7. 第 15 篇 组合构建
  8. 第 16 篇 风险模型

策略路径读完,对”市面上的量化策略大概在做什么”有结构性认知。

8.3 工程路径

目标:把策略变成稳定运行的系统。

  1. 第 5 篇 数据管道
  2. 第 7 篇 特征存储
  3. 第 19、20、21 篇 回测三连
  4. 第 23、24 篇 执行算法 + SOR
  5. 第 27 篇 系统架构
  6. 第 28 篇 运维合规

工程路径是给量化工程师准备的”系统书”。

8.4 HFT 路径

目标:进入低延迟领域。

  1. 第 3 篇 微观结构
  2. 第 4 篇 订单类型
  3. 第 16 篇 风险模型(实时风控相关)
  4. 第 25 篇 做市
  5. 第 26 篇 HFT 架构
  6. 第 27 篇 系统架构

HFT 路径要求对网络、内核、硬件加速有基础。


九、三段最小代码

文章末尾给三段最小可运行代码,分别覆盖数据加载、因子计算、回测骨架。它们的目的是让读者立刻有”代码触感”,后续每一篇都会在更具体的场景上扩展。

9.1 数据加载:PIT 安全的行情读取

下面这段用 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()

要点:

  1. 先按 asof <= decision_date 过滤,再按 (symbol, event_date) 取 last。这样保证拿到的是”在 decision_date 之前最新可见”的版本,不是事件时间为 decision_date 的最新值;
  2. adj_factor 跟着 asof 走,回测查询时直接用,不要拿”今天最新的复权因子”;
  3. universe 过滤放在最后,避免在 scan 阶段触发字段全表扫。

PIT 校验脚本可以这样写:把 decision_date 往前挪一天再跑一遍,结果应该是上一版的子集。如果出现”挪前一天反而多出一些行”,说明 asof 字段被弄脏了。

9.2 因子计算:行业中性化的横截面动量

下面是一个最简单的”过去 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

要点:

  1. shift(1) 是防止信号偷看同一根 bar 的收盘——典型的未来函数;
  2. MAD 截尾对厚尾数据更稳健;如果改成 clip(quantile(0.01), quantile(0.99)) 也行;
  3. 行业中性化用”组内去均值”做最简单实现,更严格的做法是对行业哑变量做横截面 OLS 取残差;
  4. 当横截面有效样本太少时(< 50)跳过这一天,避免噪音放大。

9.3 回测骨架:向量化的最小回测

下面是一段向量化回测骨架。它做出三个关键工程决定:信号 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),
    }

要点:

  1. weight.shift(1) 是不可省略的一步——信号 t 形成、t+1 开始持有,回测里漏掉这一步等于用未来数据;
  2. 换手成本用 |w_t - w_{t-1}| 直接近似,单边费率与滑点合并到 bps;
  3. 这是”向量化回测”,速度快但表达力弱:它假设无限流动性、无冲击成本、按收盘成交。第 19 篇会给出”事件驱动 + 滑点模型 + 容量约束”的完整版本。

向量化回测的最大用途是因子早期筛选——从几千个候选因子里快速挑出几十个可能有效的,再交给事件驱动回测做精测。第 19、20 篇会讲两套引擎怎么配合。

9.4 一段 IC 计算:单因子检验的最小样例

光算因子还不够,必须配套一段 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)

判定准则(仅做参考,具体阈值与策略类型有关):

9.5 关于 numba 与 vectorbt

回测对性能敏感时,用 numba 加速核心循环、用 vectorbt 做大规模参数扫描是常见选择。两者的边界:

第 19 篇会把三种方案在同一个策略上做基准对比,给出选择建议。



十、回测引擎选型:四类方案的工程取舍

回测引擎是所有量化系统的核心基础设施之一。市面上主流方案大致分四类,工程取舍不同。

10.1 向量化回测(Vectorized)

代表:自己写的 pandas/numpy/polars 流水线、vectorbt。

10.2 事件驱动回测(Event-Driven)

代表:zipline、backtrader、自研事件循环。

10.3 模拟撮合(Tick-level Simulation)

代表:自研 tick 级订单簿重建 + 撮合模拟器。

10.4 影子账户(Paper Trading)

代表:把信号接到真实行情、模拟下单、影子持仓核算。

四类方案在一个完整研究流水线里通常会组合使用:向量化做粗筛 → 事件驱动做精测 → 影子账户做最终验证。第 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 变了),会显式声明。


十二、常见误区与反模式

工程链路上反复看到的几个反模式,列出来作为反例:

12.1 “回测漂亮就上线”

漂亮的回测有 N 种来源:未来函数、生存者偏差、过拟合、容量假设、滑点假设过乐观、费率忘记扣、训练样本与检验样本未隔离、参数在几百次试错后挑出来的。任何一种都能让回测看起来比实盘强 5–10 倍。

工程上的对策:每一份回测必须配套交付以下检查:

12.2 “把研究脚本直连实盘”

最危险的反模式。研究环境通常没有风控、没有限速、没有幂等、没有状态恢复。直连实盘的后果是策略 bug 直接变成资金事故。

工程上的对策:研究环境永远不能拿到真实账户的下单凭证。下单链路必须经过 OMS/EMS,OMS/EMS 必须开启所有风控开关。第 27 篇会给出环境隔离的具体方案。

12.3 “风控写在策略代码里”

风控如果和策略写在一起,策略 bug 会一并把风控绕过。Knight Capital 2012 年的 4.4 亿美元事故就是典型——老代码模块被误激活,没有独立风控通道兜底。

工程上的对策:风控必须是独立通道,物理上独立的进程、独立的部署、独立的代码仓库(或至少独立的代码路径)。策略再怎么 bug 也不能污染风控状态。

12.4 “本地估算持仓”

很多新写的系统会把”持仓”算成本地状态:上次目标 + 上次成交 = 当前持仓。问题是网络抖动、回报丢失、撤单回报延迟都会让本地估算和交易所真实持仓漂移。漂移一旦发生,风控就会基于错误数据放行错误订单。

工程上的对策:本地状态只作为辅助,真实持仓以交易所回报为准,每分钟(或每次下单前)做一次对账,发现漂移就停止下单并告警。

12.5 “复盘换口径”

实盘表现不好,复盘时把回测的滑点假设调高一点、费率算少一点,让数据”对得上”。这是自欺。

工程上的对策:回测口径和复盘口径字段级一致,由同一份配置文件生成。任何口径修改必须同步两端,并对历史所有跑过的回测重跑一遍。第 22 篇会给出口径管理的工程做法。

12.6 “用平均数掩盖尾部”

夏普比率假设收益正态分布,对厚尾不敏感。一个策略 250 个交易日里 248 天小赚、2 天大亏 50%,年终统计看 Sharpe 仍然不错,但实盘上你扛不住那 2 天。

工程上的对策:评价策略至少看四个指标:Sharpe、最大回撤、Calmar(年化收益/最大回撤)、收益分布的偏度峰度。任何一个不达标都不上线。第 22 篇会展开指标体系。


十三、一份不变量清单

把整篇文章的工程要点压缩到一份清单,写在墙上每天看一遍,能避免 80% 的事故:

每一条都对应后续某一篇的工程方法。本系列的所有内容,本质上都是在帮你把这份清单从一行字落到一行代码。


十四、结语

量化交易的工程难度,不在任何一段单独的代码,而在八段链路必须同时正确。一根 K 线数据进来时不被时间错位污染、一个因子算出来时不偷看未来、一份目标持仓在执行时不被滑点吃掉、一笔成交回报回来时能精确匹配上原订单、一次崩盘时风控能在毫秒内把链路熔断——这些事任何一件单看都不难,难的是 365 天每天都对。

这个系列的目标,是让读者读完之后,对每一段链路都有”我知道哪里会出事、我知道怎么守住”的信心。后续 27 篇会把上面的每一条不变量从一句话展开到几千字 + 代码 + 实测。第 2 篇先从市场结构开始:A 股、美股、期货、加密各自的撮合机制、订单类型、交易制度,这是所有后续策略代码的底层假设。


参考文献

书籍

论文

规范与文档

工具与库


上一篇系列首页  下一篇02 市场结构

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-05-01 · quant

量化交易

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

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 条上线前自检清单。


By .