把交易系统搭起来只是开始。一个能跑回测、能下出第一笔单的系统,距离能托管真金白银还隔着一整套运维与合规设施:监控、告警、熔断、降级、监管报送、事故复盘、变更评审、灰度、混沌演练。这一套东西的存在感很低,写得再好也只在事故发生的几分钟里被人想起;但凡省掉一项,账户被打穿之前都不会有人察觉。
这一篇要把这层「不出事看不出价值、出了事就是全部价值」的设施写清楚。前置阅读是上一篇《交易系统架构》——本文假定读者已经接受了「研究、回测、纸交易、生产」四套环境共用核心抽象的设定,以及第十九章《风控引擎》和第二十四章《可靠性与容灾》中对于幂等、限流、隔离、灾备的一般原则。本文不重复这些通用工程结论,只补量化交易场景下的特殊性:决策窗口是毫秒级的、错误成本以现金计、监管不接受「事后修复」、复盘的目标不只是改 bug 还要改激励结构。
代码示例使用 Python
3.11、asyncio、prometheus_client
0.19。本文给出的实时风控守护进程是一个最小可运行版本,演示订单速率限制(rate
limiting)、回撤监控(drawdown
monitoring)和熔断触发(circuit
breaker)三条主路径,同步保留了把它接到真实订单管理系统(order
management system, OMS)所需要的扩展点。
风险提示:本文涉及的监管条款条文编号、阈值参数、事故年表、事故金额来自公开信息源,但具体数字可能随合规更新而变化。任何实际系统的熔断阈值、报送字段、值班流程都必须以本机构所在司法辖区当时生效的最新规则为准,并经合规、法务、交易、技术四方共同评审;不应把本文示例代码或阈值直接照搬到生产系统。复盘案例的因果分析是工程视角下的简化,不构成对相关人员或机构的法律评价,也不应据此推断未公开的事实。本文不构成任何投资建议、合规建议或法律建议。
一、量化交易的运维独特性
普通互联网服务的可用性故事是:「99.95% 可用,故障 5 分钟内恢复,用户少看几条朋友圈」。量化交易的故事是:「99.95% 可用意味着每年 4 小时 22 分钟的盲飞,期间任何持仓的 mark-to-market 偏离都不可见,任何成交回报都会丢,任何挂单都不能撤」。同一个数字在两个语境下的含义完全不同。把互联网行业的 SRE 直觉直接搬到交易系统上,最容易翻车。
一点一、故障窗口的时间尺度
行情数据流出现 200 毫秒级的卡顿,对一家 CDN 来说是测量噪声,对一支以毫秒为决策周期的做市策略来说就是「在过去 200 毫秒里你已经按 200 毫秒前的价格挂出了上千笔订单,对手方已经全部 take 走,账户被洗了一遍」。同样地,对于一个分钟级的因子策略,主行情源到备行情源切换需要的 5 秒钟可能根本没有可观察的 PnL 影响,但同一个 5 秒钟切换在做市这边足以让一支策略当天 KO。
这意味着:故障窗口必须按策略的决策周期来定义,而不是按基础设施 SLA 的 9 的个数来定义。在监控里把所有故障窗口都拉成同一根时间轴是失败的设计;正确做法是给每一类指标配它自己的时间窗口,下面第二节会展开。
一点二、错误成本的金钱化
CDN 故障的成本是「广告主退款」「用户流失」,可以平摊到月度报表里抹平。交易系统故障的成本是即时清算的:一秒钟内多发出 3 万笔错误订单,账户里的钱就消失了,钱不会因为你事后 root cause 写得好就回来。Knight Capital 在 2012 年 8 月 1 日的 45 分钟里损失 4.6 亿美元,本质就是这种错误成本即时清算的属性,和「服务降级了用户体验差」完全不是一类问题。
这条属性派生出两个工程结论:
- 任何「先观察再处置」的链路都要再问一次:在观察到的那一刻,还在持续亏损吗?如果是,观察期就要尽量短,甚至直接换成「先冻结、再观察」。互联网服务里允许的「先记录日志、人工 triage、慢慢恢复」流程,在交易系统里要换成「先熔断、后取证、再恢复」。
- 自动化的方向只能是「更保守」。任何会自动加仓、自动放开限制、自动重启策略并继续下单的逻辑都不能存在。下面第四节的熔断决策树会把这条原则展开。
一点三、可观测性必须穿透到业务语义
在普通后端系统里,监控到 HTTP 5xx、p99 延迟、CPU、内存、磁盘 IO 大体就够。交易系统不行:CPU 一切正常、HTTP 全是 200、p99 延迟没变化,但策略已经在错的方向上累积了一个意外的几千万人民币敞口——因为某只股票当天停牌却没有从 universe 里被剔除,因子值瞬间被一条脏数据污染。这一类故障的特征是「基础设施全绿,业务全红」,必须靠业务层指标才能发现。
一句话总结这一节:量化交易的运维难在错误成本即时清算、决策窗口毫秒级、故障必须能从基础设施一直追到 PnL 归因。下文所有设计都围绕这三条展开。
二、监控体系
量化系统的监控不是一个 Grafana 大屏能解决的问题。它的本质是把从「机器是否还活着」到「策略是否还在赚钱」这条链条上的每一层都做成可独立判定的健康信号,并保证任何一层异常都能向下溯源到具体的 tick 或订单。下面分六层给出实操框架。
二点一、L1:基础设施层
包含 CPU、内存、磁盘、网卡、NTP
时钟漂移、内核错误日志。这一层和任何后端系统没区别,可以直接用
node_exporter + Prometheus + Grafana
这一套。需要特殊关注的只有时钟:交易所要求时间戳同步到毫秒级(A
股某些场景要求毫秒级,US 市场对 OATS / CAT 报送要求 50
毫秒以内同步精度),任何 NTP / PTP 异常都要能即时告警。
时钟监控的具体指标是 chrony 或
ptp4l 暴露出来的相位偏差(offset),当 |offset|
持续超过 1
毫秒就告警。把它和后面的订单时间戳归因连起来,可以在出现成交回报序列错位时快速判定是「网络抖动」还是「时钟漂移」。
二点二、L2:行情数据流层
最容易被忽略的一层。指标至少包括:
- 每秒消息数(msg/s):分品种、分主备源。绝对值不重要,趋势比对重要。
- 行情序列号断裂(gap):交易所一般在协议里给每条 tick 一个递增 sequence number。任何 gap > 0 都要计数,连续 3 秒有 gap 直接告警并切换备源。
- 时间戳偏差:本地接收时间和交易所打的 exchange timestamp 之间的延迟分布。p50、p95、p99 各画一条线。
- 订阅状态:每个订阅当前是 active 还是 unsubscribed,掉线后是否自动重订阅。
这一层指标采集必须内嵌在 feed handler 进程内,而不是从下游观察。从 OMS 那一侧观察「最近 1 秒没收到任何 tick」永远会比 feed handler 自己内省晚一拍——后者能看到「socket 还在但已经没有数据」「序列号回到了 0」「时间戳跳回历史」这类微妙异常。
二点三、L3:策略与信号层
策略进程内部要打的指标包括:
- 信号生成耗时:每次
on_tick/on_bar回调到generate_signal返回的时间。这是研究和生产偏离的第一个信号——如果生产环境的耗时分布和回测时的预期偏差一个数量级以上,要么是数据规模错了,要么是某个隐藏的全局状态在生产里被反复重算。 - 因子分布:每个核心因子的实时分位数。研究阶段算出来的因子均值是 0、标准差是 1,生产里突然变成均值 30、标准差 200,多半是某个上游字段单位变了(人民币变成分、价格变成 ticks)或某个交易日因为停牌出现了脏数据。
- 目标仓位 vs 实际仓位偏离度:策略的「想要」和 OMS 的「正在执行」之间的距离。这个偏离度过大要么是订单卡在交易所队列里、要么是某个 callback 漏了、要么是策略被调度延迟。
二点四、L4:订单与成交层
这一层是最容易做指标膨胀也最容易做错的:
- 下单速率:order/s,按策略、按账户、按交易所分别统计。这是后面熔断和监管报送都要用的核心指标。
- 拒单率(reject rate):被交易所拒掉的订单占比。健康值是 0;超过 1% 必然有问题,常见原因是价格越过涨跌停、保证金不足、合约代码错误、自成交。
- 撤单率(cancel rate):撤单数 / 下单数。做市策略本来就高,需要按策略类型设阈值;趋势策略撤单率突然拉升几乎一定是 bug。
- 成交率(fill rate):在挂单时长内成交的占比。
- 滑点(slippage):成交价 vs 决策瞬间的中间价。
- 订单时延分布:从策略
submit_order()到收到交易所确认的时间。p50、p95、p99 三档。
二点五、L5:风险敞口层
实时仓位、现金、保证金、敞口分解都要在线计算并暴露成指标,不能只在收盘后跑批。具体维度至少包括:
- 单标的敞口(按市值、按 delta)。
- 行业 / 因子 / 国家 / 期限敞口。
- 杠杆:总敞口 / 净资产。
- 集中度:Top 5 / Top 10 标的占比。
- VaR / ES:日 VaR 95% 和 99%,每分钟更新。
- 保证金占用率:临界值前要预警,到限额前要熔断。
这层指标要和回测期内允许的同名约束比对:研究阶段说「单标的敞口不超过 NAV 的 5%」,生产里就要在 2% 预警、4% 软熔断、5% 硬熔断。具体熔断行为见第四节。
二点六、L6:盈亏与归因层
最顶层,最贴近业务,最容易造成幻觉。要监控的不是 PnL 本身——PnL 短期波动是策略本身的属性,盯它会带来无效告警——而是 PnL 与预期的偏离:
- 日内回撤(intraday drawdown):从当日高水位到当前的跌幅。
- 与回测预期的偏离度:把当日实盘 PnL 和回测在同一信号、同一价格、零滑点假设下应得的 PnL 做差,这个差异如果突破历史分布的 99 分位,几乎必然是滑点变大、信号失真或数据错配。
- 归因到信号 vs 滑点 vs 费用:把日内 PnL 拆成三块,分别画曲线。
每一层告警都要写明三件事:采集源、阈值依据、告警严重度。下一节展开告警治理。
三、告警与值班
监控指标摆得再多,如果告警一响就是几百条、值班的人三分钟内全部点掉,这套监控等于不存在。告警治理的核心问题是把信噪比拉到足够高,让每一条响起来的告警都值得花一分钟去看。
三点一、告警分级
实践中至少要分四级:
- P0 / 拉闸级:账户级风险、合规级风险。例如 5 秒内 100 笔以上拒单、单标的敞口超过硬限额、监管要求的报送通道断开。这一级必须自动触发熔断,不依赖人;告警的作用是通知值班「熔断已经触发,去看现场」,而不是「请你来决定要不要熔断」。
- P1 / 立即处理级:5 分钟内一定要有人响应。例如行情主备源都掉、单策略当日回撤 > 软熔断阈值、订单时延 p99 突破历史 3 倍。
- P2 / 当班处理级:本班次(2 至 4 小时)内处理完。例如个别 NTP 漂移、磁盘使用率到 80%、某个非核心策略 PnL 异常但敞口在限内。
- P3 / 工单级:下一个工作日处理。例如日志里出现新型 warning、机器房温度告警但还在范围内。
每条告警都要在创建时打上分级;分级在事件回放里要可统计,每月看一次「P0 是不是漏掉过」「P3 是不是有人长期不修」。
三点二、告警抑制与去重
最常见的反模式是「一个根因爆出 200 条告警」。常见场景是行情源掉线,下游所有策略的「数据延迟」「因子分布异常」「订单速率为零」全都同时报警。处理办法是给告警之间显式建依赖:
- 抑制规则(inhibition):当
feed_source_down触发时,自动抑制所有feed.*名字空间下的子告警。 - 去重规则(deduplication):同一指标在 5
分钟内的连续越限只发一条;恢复时再发一条
resolved。 - 聚合规则(grouping):同一 namespace 下的多个 P2 告警在 30 秒内聚合成一条摘要发送。
Prometheus / Alertmanager 都原生支持这三类规则。规则本身要进版本控制,每次修改都走代码审查,不允许在 Web UI 里直接改。
三点三、值班制度
值班(on-call)不是「24 小时电话开机」这么简单。一个能扛得住交易日的值班制度至少要写明:
- 值班窗口:交易时段 + 收盘后 1 小时 + 开盘前 1 小时是 primary,必须有人在键盘前;非交易时段是 secondary,可以接电话但允许 30 分钟响应延迟。
- 轮换周期:一周一轮是常见做法,过短会割裂上下文,过长会疲劳。
- 交接协议:上一班结束时必须把当日告警清单、未关闭的事件、变更窗口、第二天的发版计划交给下一班;交接走 ChatOps 频道留档。
- 升级路径:值班解决不了的事件 15 分钟内升级到 backup,30 分钟内升级到团队 lead,60 分钟内升级到风控负责人。
- 演习:每月至少一次 game day,故意制造一类已知故障让值班按 runbook 处置,事后看 MTTR、漏判项、runbook 是否需要更新。
三点三bis、事件管理的工具链
对于多个交易系统同时运行、多个策略团队共用一套基建的场景,需要一套事件管理的工具来避免混乱:
- 告警聚合中枢:用 Prometheus / Grafana 或类似的监控系统,把所有数据源的指标汇聚到中央,所有告警从中央发出。不要让每个策略团队维护一套私自的告警器,这会导致重复告警、遗漏告警、决策冲突。
- 事件生命周期管理:告警触发时自动创建事件单(incident ticket),关联到 jira / 服务台系统,记录 detection time、acknowledgement time、resolution time、root cause、follow-up actions。每一个生命周期阶段都有明确的责任人。
- 升级路径与 runbook:值班看到一条告警后,应该立即能找到对应的 runbook(标准操作手册),里面写明了前置检查、常见原因、快速修复步骤、何时升级到谁。runbook 本身也要进版本控制,每次出现无法按 runbook 快速定位的事件,回顾后就要更新 runbook。
- 知识库整合:把事后复盘、常见问题、历史故障都汇总到知识库里,这样新值班或新员工能快速检索,减少重复犯错。
三点四、ChatOps
「告警去 IM 群」并不是 ChatOps,那只是把邮件搬到 Slack。真正的 ChatOps 是把运维操作变成 IM 里的命令,所有命令都有:审计、确认、回滚。常见的命令骨架:
/halt strategy=foo reason="DD breach" # 软熔断指定策略
/freeze account=acc1 reason="post-trade audit" # 冻结账户新单
/cancel-all strategy=foo # 撤所有挂单
/resume strategy=foo approver=@alice # 恢复,需要双人复核
/dump position account=acc1 # 导出持仓快照
每条命令的执行都会在频道里留一条结构化记录(who、what、when、approver、result),同时落到运维库里,事后复盘和监管问询都能调出来。和邮件相比,ChatOps 的核心好处是把「群里讨论」和「实际操作」放在同一条时间线上,避免事后拼接出错。
值得强调:ChatOps 命令的执行权限要走 SSO 和 RBAC,IM
账号本身不是身份凭证。任何敏感命令都要双人复核(two-person
rule),第二节里的 /resume 就是一个例子。
四、熔断与降级
监控告警是被动的,熔断降级是主动的。监控告诉你「出事了」,熔断决定「自动停到什么程度」。一个成熟系统在 99% 的事故里不依赖人来按按钮——人来得太慢,靠人在交易时段做决策的系统在 Knight Capital 那种事故面前就是 4.6 亿美元的成本。
熔断的设计原则只有一条:自动化的方向只能是「更保守」,绝不能是「自动恢复并继续下单」。下面把具体决策树拆开。
四点一、按触发源分类
最常见的三类触发源:
- 盈亏触发:日内 drawdown 超过阈值。设计要点是 drawdown 必须按 mark-to-market 计算,不能用「已实现 PnL」否则会被「持仓亏但没平」骗过去;阈值要分级,不能只有一根线。
- 订单率触发:单位时间下单数、撤单数、拒单数、成交数。任何一项越过阈值都要触发,阈值按策略历史分布的 6σ 给。
- 风险触发:第二节 L5 层提到的敞口、杠杆、集中度、VaR 越限。
也有按外部源触发的:交易所发停牌通告、撮合引擎延迟突然拉高、系统时钟漂移、上游清算商发出 margin call 预警。这些都要纳入熔断条件。
四点二、按动作分级
熔断不是一刀切。从轻到重:
- 限速(throttle):把订单 token bucket 调小一档,例如从 100/s 到 30/s。策略继续运行,但被强制慢下来。这一档处置常用于「拒单率轻微上升、暂时还判断不了根因」。
- 软熔断(soft halt):禁止开新仓,只允许减仓。已挂的开仓单全部撤,已挂的平仓单保留。仓位继续被市场打,但不会再增加敞口。
- 硬熔断(hard halt):撤所有挂单,停止策略进程的下单权限,OMS 层把该策略的 order channel 设成只读。仓位不动,等人决策。
- 强制平仓(forced unwind):用 TWAP 或 VWAP 把仓位切片减到零。这一档只能由人触发,自动化系统不要自己跳到这一档——因为强平动作本身可能在窄市里加剧亏损,需要交易员判断市况。
每一档都要明确:触发条件、动作清单、谁能解锁、解锁需要什么证据。常见错误是「触发条件写了三页、动作写了一段、解锁条件没写」,结果熔断后没人敢解锁,整个策略停一周。
四点三、熔断的状态机
把熔断写成一个有限状态机会让代码和文档对得上:
状态:NORMAL → THROTTLED → SOFT_HALT → HARD_HALT → UNWINDING → FROZEN
转移:
NORMAL → THROTTLED : reject_rate > 1%
NORMAL → SOFT_HALT : intraday_dd > 1%
THROTTLED → SOFT_HALT : reject_rate > 5% OR persisted > 60s
SOFT_HALT → HARD_HALT : intraday_dd > 2%
HARD_HALT → UNWINDING : 人工触发 + 双人复核
UNWINDING → FROZEN : 仓位 = 0
* → HARD_HALT : 任意 P0 事件
HARD_HALT → NORMAL : 人工触发 + 双人复核 + RCA 已写
注意几个细节:
- 状态转移是单向的——一旦进入 SOFT_HALT,不会自动回到 NORMAL,必须人工介入。
- 任何 P0 事件(拒单 burst、监管断连、清算预警)都可以无视当前状态直接跳到 HARD_HALT,没有「先 throttle 再 soft」这种缓冲。
- 解锁路径写明「双人复核 + RCA 已写」是为了对抗「半夜被叫起来的值班随手解锁」的诱惑——很多重大事故都不是初次发生时打穿的,而是被重启了几次以后打穿的。
四点四、降级而不是停服
「熔断」在量化语境里不一定是停服。一些场景下更合适的是降级:
- 行情主源切换:主源出问题切到备源,策略不停,但要在指标里标记「正在跑在备源上」,因为备源数据可能有延迟、字段口径可能略有差异。
- 模型降级:复杂 ML 模型卡住或推理延迟拉高时,自动切换到一个简单的线性 fallback 模型;fallback 模型的 PnL 期望低但风险可控。
- 路由降级:智能订单路由(smart order router)失败时退回到「单交易所、市价单」的兜底路径。
降级路径都要在系统里 first-class,不是「等出事现写」。事先没有写过、没有演练过的降级路径在事故现场永远跑不通——这是从软件领域的混沌工程到金融系统压力测试都反复证明的事。
五、风控前置
熔断处理的是已经发生的异常,风控要把异常拦在它发生之前。一个成熟的量化系统里,风控不是一个进程,而是分布在三个时间点的三组检查:交易前(pre-trade)、交易中(in-trade)、交易后(post-trade)。
五点一、Pre-trade 风控
每一笔订单在离开本机之前必须过一次 pre-trade 检查。延迟预算很紧——HFT 场景下常常只有几微秒——但检查本身不能省。最常见的项:
- 价格合理性:订单价不能离当前最优价超过 X%。这是 Knight Capital 事故里的关键缺失项之一:当时的 SMARS 模块发出的订单根本没有这道检查,于是 8 秒钟内就把市场打成了一个新世界。
- 数量合理性:单笔订单数量不超过该合约日均成交量的 Y%。
- 重复检测:同一策略短时间内对同一标的反复发同侧单要计数,越过阈值直接拒。
- 自成交检测:本账户下挂单簿里如果已经有反向单,新一笔反向单要么撤旧单要么拒绝。
- 黑名单:停牌、退市、被监管限制交易的标的直接拒。
- 资金 / 保证金检查:可用保证金不足直接拒。
- 限额检查:把第二节 L5 的敞口、杠杆、集中度限额都翻译成 pre-trade 拒单条件。
这一组检查必须放在 OMS 出口、靠近交易所网关的位置,不能放在策略进程里——策略进程里的检查可以被绕过,靠近网关的检查是唯一守得住底的位置。SEC Rule 15c3-5(详见第六节)把这一类检查列为 broker-dealer 的强制义务。
五点二、In-trade 风控
订单进入交易所之后到成交之前,仍然要持续监控:
- 挂单超时撤单:挂出去 N 秒未成交就主动撤回,避免「忘了的挂单」突然成交在错误价位。
- 成交簿异常:发现自己挂的单出现在了对手簿(被反向交易匹配)等明显异常,要立即报警并撤所有挂单。
- 订单状态机一致性:OMS 内部的订单状态和交易所返回的状态要持续对账,发现 stuck 状态超过 N 秒主动 query。
五点三、Post-trade 风控
成交回报到达后,立即(毫秒级)更新:
- 持仓与现金。
- 限额复核:是否因为这一笔成交导致敞口或杠杆突破软限。如果是,立即触发减仓机制。
- 成交价合理性:成交价远离决策瞬间的 mid 时打日志,超过历史分布 99 分位时报警。这是发现「错误标的」「错误方向」的最后一道关。
每一笔成交都要落入一个事件流,下游的清算、风险、合规模块都从这个事件流读,不要让每个模块独立从交易所读——一旦事件流是单源的,对账就只用对一处。
五点四、风控的版本与上线
风控规则和策略代码一样要走变更评审。具体两条强制规定:
- 任何风控规则的修改都要双人复核:作者 + 审批人 + commit 记录,缺一不可。
- 风控规则上线要灰度:先在影子模式(shadow mode)下并行跑 N 个交易日,把它在历史订单流上的拒单决定和真实拒单决定对比,没有大差异再切换为实际生效。
这些约束乍看繁琐,但是少了它们之后,事故现场会一遍遍重演。
五点五、风控的阈值管理
一个容易被低估的风险源是「阈值在某个地方写死了」。预测一些场景:
- 周五上线新策略,设定最大敞口为 100w。但是新策略的单笔持仓尺寸比老策略大,100w 的敞口如果仓位数只有 10 个,意味着每个仓位 10w,如果某个标的突然断路器停牌(如发公告前),剩下 9 个仓位要承受更大的风险。
- 策略在 A 股跑了 6 个月,所有参数都按 A 股的流动性调。临时要在港股跑同一策略,不改参数直接部署,结果挂单速率正常但成交率大幅低于预期,资金积压,杠杆上升,触发风控。
- 系统刚启动时,PnL 从 0 开始,drawdown 阈值设的是绝对数字(例如亏 50w 就熔断),结果第一天跑到 100w 就被拦住了;后来改成相对阈值,但 hardcode 在代码里,要改一次就要发版。
解决办法是所有阈值都从配置中心读,不要 hardcode。配置中心的每一次改动都要:
- 记录 who / what / when / why / approver
- 改动前先在 shadow/staging 环境验证这个新阈值不会导致所有单都被拒
- 改动后定时审计(例如周一上午)所有阈值是否仍然合理
- 保留历史版本,能快速回滚
大多数风险事件都不是一次大意引起的,而是多次小改动积累起来的。
六、监管报送
把视角从内部运维切到外部监管。各国监管机构对程序化 / 算法 / 高频交易都有报送和合规要求,下面挑三个有代表性的辖区做工程口径的概览,重点是「要报什么字段」和「为什么这条要求会影响系统设计」。具体条文要以最新版法规为准,本节不替代合规咨询。
六点一、A 股程序化交易报告
中国证监会与上交所、深交所、北交所于 2024 年陆续发布了关于程序化交易的管理规定(《证券市场程序化交易管理规定(试行)》及配套交易所细则,2024 年 5 月公布、2024 年 10 月起施行)。要点包括:
- 报告制度:从事程序化交易的投资者要在交易前报告,内容覆盖账户信息、资金来源、交易策略类型、最高申报速率、最高单日申报笔数等。
- 高频交易差异化管理:单账户每秒申报、撤单合计达到 300 笔以上,或单日累计达到 20000 笔以上,纳入高频交易差异化监管,会被加收报送内容、加收费用,并接受重点监控。
- 异常交易行为:瞬时申报速率异常、频繁瞬时撤单、频繁拉抬打压、自成交、同期反向交易等都被列为异常交易行为,明确禁止。
工程口径下,这套规则推导出来的系统要求至少包括:
- 必须有按账户、按策略、按交易所分别统计申报与撤单速率的能力,且能以日、秒为窗口聚合,作为日常自查与应监管要求报送的底线。
- 异常交易行为里的「瞬时撤单率」「自成交」「频繁反向」都要在 pre-trade / in-trade 阶段拦截,前一节风控里已经覆盖。
- 最高申报速率要在系统里有一个全局可配置的硬限,并和报送的数字一致;改这个值要走变更评审并同步更新报送材料,不能只改代码。
六点二、美国 SEC Rule 15c3-5
SEC Rule 15c3-5(市场准入规则,Market Access Rule,2010 年通过、2011 年生效)要求向市场提供准入的 broker-dealer 必须建立合理设计的风险管理控制和监督程序,以管理与市场准入相关的财务、监管和其他风险。关键要求:
- 财务风险控制:阻止超出预设信用或资本阈值的订单,阻止明显错误的订单(erroneous orders)。
- 监管要求控制:阻止违反交易所规则或 SEC 规定的订单。
- 直接和独占的控制(direct and exclusive control):风险控制不能完全委托给客户或第三方,broker-dealer 必须能直接配置、修改、监控这些控制。
- 年度复核:CEO 每年要书面证明这些控制是合理设计且持续有效的。
工程口径下:
- pre-trade 风控不能由策略方自己跑——broker-dealer 必须有一道独立的、由它自己运维的检查,这条直接决定了「策略方部署的 risk gateway」和「broker-dealer 部署的 risk gateway」是两个进程、两套配置、两条变更通道,不能合并。
- 「明显错误的订单」是 fat-finger 检查的规范化版本,要落实到价格区间、数量上限、合约校验三类检查。
- 年度复核意味着系统要有一份可审计的配置版本历史——任何阈值的变更都要能查到 who、when、why、approver。
六点三、欧洲 MiFID II / RTS 6
欧盟 MiFID II(2018 年生效)下,从事算法交易的投资公司要遵守若干技术性标准(Regulatory Technical Standards),其中 RTS 6 专门处理算法交易系统的组织要求。要点包括:
- 测试和部署:算法部署到生产前要有完整的测试,包括用历史和模拟数据测试、用 conformance test 和交易所对接、回归测试。
- kill functionality:必须有立即终止所有未成交订单的能力(俗称「kill switch」),并能精确到策略 / 交易员 / 算法粒度。
- 变更管理:算法的任何重大变更都要走文档化的变更管理流程。
- 监控:实时监控算法行为、异常交易行为、市场扰动;负责监控的人员要有适当资质。
- 记录保存:算法交易相关的所有记录(订单、决策、配置、变更)至少保留 5 年;时钟同步精度按 RTS 25 给出,对应不同活动类型有不同同步等级。
工程口径下:
- kill switch 不是 ChatOps 命令的子集,是一个独立的、低延迟的物理/逻辑通道,最好不依赖一般化的运维系统——一般运维系统挂了的时候,kill switch 还要能用。
- 算法的任何代码改动都要带文档(设计文档、测试报告、影响评估);版本控制不能只是 git 历史,还要落到合规文档里。
- 时钟同步要按要求做到 PTP(precision time protocol),并周期性出具同步精度报告。
六点四、共通的工程结论
把三个辖区的要求合在一起看,和量化系统正常工程实践重合的部分非常多。共通的结论:
- 任何阈值都要可配置、可审计、可回滚。硬编码在代码里、改一次要发版的阈值是合规债务。
- 任何决策都要可追溯到时间戳精确的事件流。事件流要和报送字段对齐,避免事故时还要从原始日志里拼报送材料。
- kill switch 必须独立存在,不能只是「重启策略进程」。
- 变更管理走文档:每次上线要有什么改了、为什么改、谁审了、怎么回滚的完整链条。
下一节用四个真实事故把这些设施的代价讲清楚。
七、事故复盘
事故是这套设施的实战考核。下面四个事件不是要列罪,而是看每一个里面到底是哪一层设施失守,以此对前几节的内容做反向校验。每个事件的事实部分来自公开资料,因果分析是工程视角下的简化,不构成对相关人员的法律评价。
七点一、Knight Capital,2012 年 8 月 1 日
事件经过:纽约证交所当天上线 Retail Liquidity Program(RLP)。Knight Capital 部署了配套的 SMARS 路由代码,运维过程中没有把一个名为 Power Peg 的旧测试代码从一台老服务器上下线。一个被复用的二进制 flag 在新代码里启用了 RLP,但在那台没更新的老服务器上启用了 Power Peg。开盘起 45 分钟内,Power Peg 在生产里以指数级速率发出错误订单,公司损失约 4.6 亿美元,几天内被收购退出。
工程视角的失守层次:
- 变更管理:8 台服务器的部署没有自动化校验,第 8 台服务器漏更新没有任何报警。
- 代码治理:废弃代码(Power Peg)没有从代码库下线,旧 feature flag 被新代码复用,造成语义碰撞。
- pre-trade 风控:发出的大量订单没有通过任何「价格合理性」「数量合理性」检查就直接到了交易所。
- 熔断:没有「短时间内订单速率突然拉高 N 倍自动停」的硬熔断。
- kill switch:人工识别问题花了 30 分钟,期间没有一键停止所有策略 / 路由的能力。
这一个事件几乎把本文前六节里的每一项设施都打了一个反例。
七点二、Long-Term Capital Management,1998 年
LTCM 不是程序化交易事故,但它对量化运维的启示同样直接:高杠杆 + 模型假设失效 + 流动性枯竭 = 任何事后熔断都来不及。1998 年俄罗斯债务违约导致全球套利价差极端走宽,LTCM 的相对价值套利组合在几周内损失约 46 亿美元,最终由美联储召集大行救助。
工程视角的失守层次:
- 风险敞口:杠杆率长期在 25 至 30 倍,突破任何合理风控的硬限。
- 模型假设的监控:相关性矩阵在「正常市场」下估计,没有对「极端市场」有兜底;流动性溢价没有计入风险计量。
- post-trade 风控:日 VaR 在最后几周明显恶化但没有触发减仓,因为减仓本身在这种深度套利组合上就会进一步推宽价差,构成 self-fulfilling 的负循环。这一点和今天的「拥挤交易」(crowded trade)问题一脉相承。
LTCM 的教训不能用「加几条监控」修复,它必须落到第八节的制度文化层面:杠杆审批、敞口上限、退出路径必须在系统设计的最初就锁死。
七点三、Three Arrows Capital,2022 年
3AC 是加密货币基金,2022 年 6 月因 Luna / UST 崩盘和 stETH 折价被多家交易所连环 margin call,最终破产清算。和 LTCM 类似,工程视角下的核心问题是:
- 集中度:在少数几类资产上的极高集中度。
- 场外杠杆:通过 OTC 借贷加杠杆,杠杆数据不在公开撮合所看得见的地方,监管和交易对手都没有看到完整画面。
- Margin 监控:跨交易所、跨借贷渠道的保证金没有一处汇总,单边数字看上去都还行。
对自营 / 私募的工程结论:多个账户、多个交易所、多个借贷渠道下的总敞口必须有一个汇总视图,这个视图必须实时;只看单边是另一种形式的盲飞。
七点四、FTX,2022 年
FTX 在 2022 年 11 月暴雷,相关诉讼文件(包括 SBF 案的起诉书及 Chapter 11 申请文件)显示了多种公司治理与内部控制缺失。工程视角下与本文相关的几条:
- 客户资金与自营资金未隔离:交易系统在底层就允许 FTT 抵押、Alameda 透支等操作;这是制度问题先于技术问题。
- 变更和审计缺失:合规和审计职能弱化,重要参数(包括关键账户的负余额限制)可以在没有审批留痕的情况下修改。
- 对账与披露:内部账本与外部托管资产的对账不完整。
这一个事件把本文从工程话题推到了治理层。技术再到位,如果制度允许操作员单方面改风控参数、绕开 pre-trade 检查、关闭审计日志,再多监控也救不回来。第八节展开这部分。
七点五、小结:反复的故事
从 Knight Capital 到 FTX,故事在 10-20 多年间反复重演:
- 变更管理薄弱:Knight 是漏部署,FTX 是无审计修改。
- 风控参数容易被绕开:Knight 的 pre-trade 缺失,FTX 的负余额允许。
- 跨系统的数据对账不一致:3AC 的多交易所敞口没有汇总,FTX 的客户与自营混淆。
- 极端情况没有预案:LTCM 和 3AC 都是流动性突然枯竭,事前 VaR 模型没覆盖。
这些失误一次次给出的启示总是一样的:工程和治理必须绑在一起。今天的工程最佳实践(灰度、对账、审计、自动化)都可以追溯到历史教训。反过来说,如果一个系统没有这些实践的痕迹,那它就是在等待一个历史重演的机会。
八、制度与文化
前面七节是技术。这一节是技术的边界条件。任何监控、熔断、风控、报送,最终都跑在一个由人组成的系统里;制度和文化决定了人会不会去维护这些东西、会不会在事故里诚实面对、会不会在压力下保持纪律。
八点一、变更评审
任何会进入生产路径的修改都要走变更评审:策略代码、风控参数、交易所连接配置、报送字段、监控阈值。评审的最低形态是 GitHub Pull Request 的 review;更严格的形态会在评审之外加一层 Change Advisory Board(CAB)周会。CAB 的目标不是让所有人投票通过每一行代码,而是把「这次变更的风险面」「回滚方案」「上线窗口」当面对齐。
实践细节:
- 变更窗口:交易时段不发版。开市前 30 分钟到收盘后 30 分钟禁发版,紧急修复也要走 emergency change 流程。
- 回滚优先:每个变更都要在评审里给出回滚步骤,给不出来的不允许上线。
- 风险评级:把变更分成 low / medium / high,high 级别需要双人审、需要灰度、需要 game day 演练。
八点二、混沌工程
普通互联网公司里的 Chaos Monkey 在量化里有一个直接对应物:故意触发熔断、故意切换主备源、故意在影子环境里下错单,验证整套设施能否正常响应。具体做法:
- 季度演练:每季度选一类故障,在生产前最后一道环境(仿真或纸交易)上重放,记录 MTTR。
- 节日前演练:长假前一周做一次完整的「主备切换 + kill switch + ChatOps 全链路」演练。
- 覆盖项:feed 主备切换、OMS 切换、清算商断连、ChatOps 断连、监控本身的告警通道断连(也要演练,否则告警系统挂了你不知道)。
混沌工程在金融系统里推行最大的阻力不是技术,是「在生产上故意制造故障」这件事和合规、风险偏好的天然冲突。要解决这个冲突,要么在仿真环境里跑,要么在交易日之外的低风险时段跑,要么用「影子流量」的方式跑——故障注入,但订单不真发。
八点三、灰度发布
策略 / 风控 / 模型的更新都不要直接全量。常见的灰度形态:
- 金丝雀策略(canary strategy):把新版本以 1% 的资金量先跑一周,监控所有指标和老版本的偏离。
- 影子模式(shadow mode):新版本和老版本并行运行,新版本的订单不真发到交易所,只记录「如果发了会怎样」,事后对照分析。
- A/B 资金切片:把策略的资金按账户切成 A、B 两组,新版本跑 A 组,老版本跑 B 组,N 个交易日后比较。
灰度的关键是能比较什么、怎么比较。如果新老版本不在同一时段、同一行情下跑,比较结果就没意义;这又反过来要求基础设施支持「影子模式」这种 first-class 的运行方式。
八点四、双人复核
任何风险动作都要双人复核(two-person rule,亦称 4-eye principle):
- 代码合并:本人 + 至少一名 reviewer。
- 生产部署:发起者 + 审批者,IM 或工单留痕。
- 熔断解锁:第三节已经写到。
- 风控参数修改:作者 + 风控负责人。
- 大额转账 / 持仓调整:交易员 + 风控负责人 + 财务。
双人复核不是「让两个人都点确认」这么形式化的事,它的本质是在每一个能造成大额损失的动作上插入一道独立判断。这道独立判断要满足两个条件:第二个人有能力判断、第二个人没有动机和第一个人合谋。技术上要确保两人的认证是独立的,制度上要确保审批人不是发起人的下属。
八点五、复盘文化
事故复盘有两种文化,差距是数量级的:
- 找责任人文化:开会找谁的锅,最终结论是「某某操作不慎」「某某未按规范执行」。这种复盘不会改设施,因为「让人更小心」没法工程化,下一次只会换一个不同的「不慎」的人。
- 改设施文化:开会找出失守的层次(变更管理、监控、熔断、风控、报送、文化),每一层给出可落地的改进项,下一次同类事件被更早一层挡住。
前者写出来的复盘报告每篇都长得一样,后者每篇都不一样。判断一家机构属于哪一种文化,最快的办法就是翻它最近三次复盘报告:是不是每一次的 action item 都不一样、是不是有上一次 action item 的 follow-up、是不是 follow-up 闭环了。
九、SLO 与 error budget 计算示例
把上面这些设施的预算定量地拉一拉,就到了 SLO 与 error budget。SRE 的这套语言搬到量化系统里要换两个变量:
- SLI(service level indicator)不是只看 HTTP 成功率,还要看「行情数据完整率」「订单成功率」「策略可用率」「pre-trade 风控可用率」。
- error budget 不是抽象的「允许故障的时间」,而是直接对应「这段时间里允许的潜在 PnL 影响」。
举一个具体例子。假设一个 A 股日内策略,交易时段每天 4 小时,月交易日数 22。给各 SLI 设 SLO:
- 行情完整率 ≥ 99.99%(每月允许行情 gap 总时间 ≤ 4 小时 × 22 × 60 × (1 − 0.9999) 分钟 ≈ 0.53 分钟,约 32 秒)。
- 订单网关可用率 ≥ 99.95%(每月允许停摆约 158 秒)。
- pre-trade 风控可用率 ≥ 99.99%(每月允许停摆约 32 秒)。
每一项 error budget 的「允许停摆秒数」直接折算为「这段时间策略要么停、要么裸跑无风控」。把这两条进一步翻译成 PnL 影响:
- 如果策略平均每秒下 30 笔单、平均每笔 1 万元名义本金、平均滑点 5 个基点,那么裸跑 32 秒的潜在不可控 PnL 大约是 32 × 30 × 10000 × 0.0005 ≈ 4800 元。
- 这个数和策略日均盈利能力一比,就能看出「99.99% 的 SLO 够不够用」「再加一个 9 值不值」。
下面给出一个最小的 error budget
计算脚本。tracker 维护当月已用预算,越线时返回
False,调用方据此触发熔断或降级:
from dataclasses import dataclass, field
from datetime import datetime, timedelta
@dataclass
class SLO:
name: str
target: float # 例如 0.9999
window: timedelta # 例如 timedelta(days=30)
total_seconds: float # 该窗口总服务秒数(仅交易时段)
@property
def budget_seconds(self) -> float:
return self.total_seconds * (1.0 - self.target)
@dataclass
class BudgetTracker:
slo: SLO
consumed_seconds: float = 0.0
incidents: list = field(default_factory=list)
def consume(self, seconds: float, reason: str, ts: datetime) -> bool:
self.consumed_seconds += seconds
self.incidents.append((ts, seconds, reason))
return self.consumed_seconds <= self.slo.budget_seconds
def remaining(self) -> float:
return max(0.0, self.slo.budget_seconds - self.consumed_seconds)
# 示例:A 股日内策略,每月 22 个交易日,每天 4 小时交易时段
total = 22 * 4 * 3600
feed_slo = SLO("feed", 0.9999, timedelta(days=30), total)
oms_slo = SLO("oms", 0.9995, timedelta(days=30), total)
risk_slo = SLO("risk", 0.9999, timedelta(days=30), total)
print(f"feed budget: {feed_slo.budget_seconds:.1f} s")
print(f"oms budget: {oms_slo.budget_seconds:.1f} s")
print(f"risk budget: {risk_slo.budget_seconds:.1f} s")这段代码不是生产实现,是一个把 SLO
翻译成秒级预算的工具骨架。在生产里
BudgetTracker 应当从持久化层(数据库或 KV)读写
consumed_seconds,incidents
表要落库以备复盘和合规审计。
把 error budget 用起来的关键是「越线时怎么办」。一个常见做法:本月预算用尽即冻结所有非紧急变更,直到下月预算重置;这给出了「稳定性」与「迭代速度」之间的硬约束。
十、实战:实时风控守护
把前面几节的内容落到一份能跑的代码。下面这份
risk_guard.py 实现了三件事:
- 订单速率限制:基于 token bucket 的全局与单标的双层限速。
- 回撤监控:基于 mark-to-market PnL 维护当日高水位与回撤。
- 熔断状态机:实现第四节的状态转移,越线时把策略推到对应状态。
这份代码不依赖具体 broker / OMS,使用
asyncio.Queue 模拟订单流,使用
prometheus_client 暴露指标。实际接入 OMS 时把
submit() 替换成真实的 OMS 客户端调用即可。
"""risk_guard.py — 量化交易实时风控守护进程(教学版本)"""
import asyncio
import logging
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
from prometheus_client import Counter, Gauge, start_http_server
log = logging.getLogger("risk_guard")
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
class State(Enum):
NORMAL = "NORMAL"
THROTTLED = "THROTTLED"
SOFT_HALT = "SOFT_HALT"
HARD_HALT = "HARD_HALT"
UNWINDING = "UNWINDING"
FROZEN = "FROZEN"
# Prometheus 指标
ORDERS_SUBMITTED = Counter("rg_orders_submitted_total", "Orders accepted by guard", ["symbol"])
ORDERS_REJECTED = Counter("rg_orders_rejected_total", "Orders rejected", ["reason"])
PNL_GAUGE = Gauge("rg_pnl", "Current mark-to-market PnL")
DRAWDOWN_GAUGE = Gauge("rg_drawdown", "Current intraday drawdown ratio")
STATE_GAUGE = Gauge("rg_state", "Current state (numeric encoded)")
TOKEN_GAUGE = Gauge("rg_tokens", "Current tokens in global bucket")
@dataclass
class TokenBucket:
"""全局/单标的的 token bucket 限速器。"""
capacity: float
refill_per_sec: float
tokens: float = 0.0
last_refill: float = field(default_factory=time.monotonic)
def __post_init__(self):
self.tokens = self.capacity
def take(self, n: float = 1.0) -> bool:
now = time.monotonic()
elapsed = now - self.last_refill
self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_per_sec)
self.last_refill = now
if self.tokens >= n:
self.tokens -= n
return True
return False
@dataclass
class DrawdownMonitor:
"""日内回撤监控:维护当日高水位与最大回撤。"""
high_watermark: float = 0.0
current_pnl: float = 0.0
def update(self, pnl: float) -> float:
self.current_pnl = pnl
if pnl > self.high_watermark:
self.high_watermark = pnl
if self.high_watermark <= 0:
return 0.0
return (self.high_watermark - pnl) / max(1.0, abs(self.high_watermark))
@dataclass
class RiskConfig:
global_rate_per_sec: float = 50.0 # 全局每秒下单上限
global_burst: float = 100.0 # 全局突发桶容量
symbol_rate_per_sec: float = 10.0 # 单标的每秒下单上限
symbol_burst: float = 20.0
throttle_dd: float = 0.005 # 0.5% 触发限速
soft_halt_dd: float = 0.01 # 1.0% 触发软熔断
hard_halt_dd: float = 0.02 # 2.0% 触发硬熔断
reject_burst_window_sec: float = 5.0 # 拒单 burst 窗口
reject_burst_threshold: int = 100 # 拒单 burst 阈值
@dataclass
class Order:
symbol: str
side: str # "BUY" | "SELL"
qty: float
price: float
is_close: bool = False
class RiskGuard:
def __init__(self, cfg: RiskConfig):
self.cfg = cfg
self.state = State.NORMAL
self.global_bucket = TokenBucket(cfg.global_burst, cfg.global_rate_per_sec)
self.symbol_buckets: dict[str, TokenBucket] = {}
self.dd_monitor = DrawdownMonitor()
self.recent_rejects: list[float] = []
self._lock = asyncio.Lock()
def _bucket_for(self, symbol: str) -> TokenBucket:
if symbol not in self.symbol_buckets:
self.symbol_buckets[symbol] = TokenBucket(
self.cfg.symbol_burst, self.cfg.symbol_rate_per_sec
)
return self.symbol_buckets[symbol]
def _record_reject(self, reason: str) -> None:
ORDERS_REJECTED.labels(reason=reason).inc()
now = time.monotonic()
self.recent_rejects.append(now)
cutoff = now - self.cfg.reject_burst_window_sec
self.recent_rejects = [t for t in self.recent_rejects if t >= cutoff]
if len(self.recent_rejects) >= self.cfg.reject_burst_threshold:
log.error("reject burst detected: %d in %.1fs -> HARD_HALT",
len(self.recent_rejects), self.cfg.reject_burst_window_sec)
self._transition(State.HARD_HALT)
def _transition(self, new_state: State) -> None:
order = [State.NORMAL, State.THROTTLED, State.SOFT_HALT,
State.HARD_HALT, State.UNWINDING, State.FROZEN]
if order.index(new_state) >= order.index(self.state):
if new_state != self.state:
log.warning("state transition: %s -> %s", self.state.value, new_state.value)
self.state = new_state
STATE_GAUGE.set(order.index(self.state))
async def on_pnl_update(self, pnl: float) -> None:
async with self._lock:
dd = self.dd_monitor.update(pnl)
PNL_GAUGE.set(pnl)
DRAWDOWN_GAUGE.set(dd)
if dd >= self.cfg.hard_halt_dd:
self._transition(State.HARD_HALT)
elif dd >= self.cfg.soft_halt_dd:
self._transition(State.SOFT_HALT)
elif dd >= self.cfg.throttle_dd:
self._transition(State.THROTTLED)
async def check(self, order: Order) -> bool:
async with self._lock:
TOKEN_GAUGE.set(self.global_bucket.tokens)
if self.state in (State.HARD_HALT, State.UNWINDING, State.FROZEN):
self._record_reject("halted")
return False
if self.state == State.SOFT_HALT and not order.is_close:
self._record_reject("soft_halt_open")
return False
if not self.global_bucket.take(1):
self._record_reject("global_rate")
return False
if not self._bucket_for(order.symbol).take(1):
self._record_reject("symbol_rate")
return False
ORDERS_SUBMITTED.labels(symbol=order.symbol).inc()
return True
async def fake_oms(guard: RiskGuard, orders: asyncio.Queue) -> None:
"""伪 OMS:从队列取订单,过 guard,模拟成交并更新 PnL。"""
pnl = 0.0
while True:
order: Order = await orders.get()
ok = await guard.check(order)
if not ok:
continue
# 简化:买入按价 +1 滑点成交,卖出按价 -1 滑点成交,
# PnL 抖动用一个简单噪声项模拟,触发回撤。
import random
slip = 0.01 * (1 if order.side == "BUY" else -1)
fill = order.price + slip
pnl_delta = -abs(order.qty) * abs(slip) + random.gauss(0, 5)
pnl += pnl_delta
await guard.on_pnl_update(pnl)
async def order_generator(orders: asyncio.Queue) -> None:
"""生成示例订单流。"""
import random
symbols = ["600000.SH", "000001.SZ", "510300.SH"]
while True:
await asyncio.sleep(0.02) # 50 笔/秒
await orders.put(Order(
symbol=random.choice(symbols),
side=random.choice(["BUY", "SELL"]),
qty=100,
price=10.0 + random.random(),
is_close=random.random() < 0.3,
))
async def main():
start_http_server(8000)
cfg = RiskConfig()
guard = RiskGuard(cfg)
orders: asyncio.Queue = asyncio.Queue(maxsize=1000)
await asyncio.gather(
order_generator(orders),
fake_oms(guard, orders),
)
if __name__ == "__main__":
asyncio.run(main())把它跑起来:
$ python risk_guard.py
$ curl -s localhost:8000/metrics | grep -E '^rg_'
可以看到
rg_orders_submitted_total、rg_orders_rejected_total、rg_drawdown、rg_state、rg_tokens
都按预期更新。把 cfg.global_rate_per_sec
调到比订单生成器低,可以观察到
rg_orders_rejected_total{reason="global_rate"}
持续增长;把 cfg.hard_halt_dd 调小(例如
0.001),等到 PnL 抖到这一档时可以看到 rg_state
跳到 HARD_HALT
对应的数值,并且后续所有订单都被拒(reason=“halted”)。
要把它推到生产,至少补这几件事:
- 把
_record_reject里的拒单原因写到结构化日志和事件流,供下游告警与复盘。 - 把状态机的转移和恢复路径接到 ChatOps:
/halt/resume/freeze命令直接改state字段,且必须双人复核。 - 把
RiskConfig的所有字段从环境变量或配置中心读取,并维护变更审计。 - 加 pre-trade 价格 /
数量合理性检查、自成交检查、黑名单检查,挂在
check()之前。 - 把
DrawdownMonitor的状态持久化(Redis / 数据库),避免进程重启把高水位归零。 - 在 OMS 出口部署独立的 broker-side risk gateway(参见第六节 SEC Rule 15c3-5),策略侧的 guard 只是第一道。
十一、生产交易系统的应急响应流程
当系统告警触发、特别是 Level 4 / 5 告警时,响应的速度和决策质量直接影响亏损规模。一套成熟的应急流程应该包括这几个环节。
十一点一、秒级的自动止损
一旦触发硬止损(例如账户净值跌破限额),系统应该能在 100 毫秒内完成清仓,不等人工介入。这要求:
- 订单是预生成的:不要在止损时临时生成订单,而是提前计算好「如何快速清掉所有头寸」的订单序列。
- 交易所连接高可用:主连接断开后立即自动切到备用连接,不能浪费 1 秒。
- 行情数据与风控同步:风控拿到的最新价格和 OMS 看到的最新价格不能相差超过 1 秒,否则清仓价格会严重偏离市场。
十一点二、 5 分钟的人工审核与恢复
Level 5 触发后,系统应该:
- 所有新订单自动拒绝(不是取消现有单,而是在源头就拒掉新下单请求)。
- 5 分钟内,风控负责人要在 ChatOps /
电话会议里向交易总监报告:
- 触发原因(是单个策略还是全体账户?)
- 当前账户状态(已清仓的持仓、剩余冻结资金)
- 推荐恢复路径(完全重启、部分重启、换策略参数)
- 在总监批准前,系统保持 HARD_HALT,不允许任何人工「逐笔批准」突破这个硬止损。
多数风险就是源于”总监说恢复”和”实际代码判定恢复”之间的延迟与不对齐。最安全的做法是恢复也需要人工命令,风控守护进程收到 CLI 或 ChatOps 命令后再改状态,而不是让系统自动判定。
十一点三、事后 48 小时的完整复盘
触发 Level 4 / 5 后必须启动复盘流程,必须包括这几块:
- 数据复盘:重放当时的行情、订单、PnL 快照,确认各个环节的数据一致性。有没有地方因为网络抖动数据不同步了?
- 策略复盘:这轮亏损是不是特定策略的参数偏离预期?如果是,需要调参、加监控还是下线?
- 风控复盘:风控指标有没有提前预警?为什么没预警?是阈值设太宽松了,还是指标计算本身有延迟?
- 操作复盘:从告警触发到人工响应的全过程,是否有流程问题?通知链有没有断裂?
- 技术复盘:系统在应急时有没有跑出代码 bug?连接有没有异常断开?
每一个复盘点都要对应到具体的改进项,而且要定明确的完成时间。光写在报告里不改进,下次同样的问题还会再犯。
十二、组织与文化
从微观的代码、告警,到宏观的组织结构,一个交易系统的稳定性最后都取决于人。这一节讨论几个容易被忽视的组织因素。
十二点一、风控的权力结构
在多数初创或小型量化机构里,风控是被夹在中间的:上面要听投资总监的,下面要听研究/交易团队的,结果一旦有冲突就陷入权力真空。最危险的状态就是”风控经理发现风险,找总监汇报,总监说继续跑,结果亏大了”。
正确的结构是这样的:
- 风控对 CEO / 董事长汇报,不是对投资总监汇报。这样当风控说「必须停止这个策略」时,有足够的权力来强制执行。
- 风控有一票否决权,对任何超过分配 VaR / stress test 限额的策略,风控可以直接下线,不需要投资总监批准,但要在 1 小时内向总监通报。
- 风控指标是公开的,每天收盘后所有利益相关方都能看到各策略的当前风险承诺、已用限额、剩余额度,这样就不会出现「我以为这个策略能再加大」的尴尬。
十二点二、培训与 on-call rotation
新交易员、新策略研究员入职后的前 3 个月,必须跟一个资深人员 pair program,包括:
- 理解系统架构与风控规则。
- 模拟几次故障场景(例如「网络断开后,系统如何恢复」)。
- 模拟一次真实应急(历史上发生过的严重事件,从头复演)。
on-call 轮值要透明公开,这样才能防止某个人被过度依赖。一个好的 on-call 制度应该让新人也能在辅导下独立处理大部分 Level 3 / 4 告警。
十二点三、文化:失败复盘而不是甩锅
量化交易里出现 bug 或风险事件是完全正常的,关键是失败后怎么对待。
- 最差的做法:甩锅给技术、甩锅给行情、甩锅给别的部门。这样下次同样的问题还会来。
- 次差的做法:快速修复、恢复交易,但不写复盘报告。这样下次又会以新的形式出现。
- 较好的做法:深度复盘、改流程、改代码,但不处罚相关人员。这样大家才会主动暴露问题而不是隐瞒。
- 最好的做法:复盘后确认:这是个人的粗心还是系统的缺陷?如果是后者,就改系统(例如加自动检验、加告警);如果是前者,也不处罚,只是轻微整改(例如加一个代码审查点、加一个检查清单)。
一个交易系统最后能稳定到什么程度,最终是由这个团队的文化决定的。再完美的工程也抵不了一个「出了问题就甩锅」的文化。
十三、系统化的告警与升级
前面各层的监控数据最终要汇总成一个一致的动作框架。多个指标发生波动时,系统需要按优先级升级告警,避免告警风暴。
十三点一、告警升级路径
典型的升级流程是这样的:
Level 1 (Info) : 新策略上线
Level 2 (Warn) : 单个因子 IC 衰减超 20%
Level 3 (Alert) : 因子衰减 + PnL 环比下降 > 10%
Level 4 (Alarm) : Level 3 持续 > 30 分钟,或 PnL 回撤 > 2% 日均
Level 5 (Critical) : 账户余额跌破风控硬止损线
-> 触发 Level 5 : 自动降仓 50%,ChatOps 通知全体
每一级的升级都应该有明确的数据驱动触发条件,不靠人工判断。而且告警抑制(suppression)必须显式编码,避免级联。例如如果 Level 3 已经触发,就不再重复发 Level 2 的告警。
十三点二、白天 / 夜间 / 假日的分层值班
不同时段的值班强度应该不同:
- 行情高峰(9:30-11:30, 13:00-15:00):Level 3 及以上立即电话 / 短信告警,要求 5 分钟内回应。
- 行情低谷(夜盘、凌晨):Level 4 / 5 立即告警,Level 2 / 3 汇总后早晨 7 点报告。
- 周末 / 假日:只监控极端情况(Level 5),其他告警积累到下一个交易日早晨。
这套分级需要跟交易日历、交易时段、策略本身的决策周期绑定。没有一个通用的「告警优先级」标准能适配所有场景。
十三点三、无意义的告警的代价
一个经常被忽略的工程成本:频繁误报会导致值班人员「告警疲劳」。研究表明,在连续 100 条告警中,如果有 20 条以上是虚警,人对真警告的反应时间会延长 2-3 倍。换成交易系统的语言,这意味着真正的止损机会被错过了。
所以告警系统的设计哲学应该是”宁可漏报,不要误报”。每一个告警都应该是「人工看到这个指标后会采取行动」的条件,而不是「这个指标理论上可能和风险相关」。前者会让值班人员持续保持警觉,后者会让他们点完一百条告警后直接关掉手机。
十三点四、告警指标的长期演变
一个通常被低估的问题是告警规则的老化。策略上线时的告警阈值在 6 个月后可能就不适用了——因为策略的波动率、滑点、下单速率都会随着市场变化而变化。要避免「告警狼来了」,需要定期审计:
- 月度告警审计:统计本月 P0/P1 告警的真正威胁程度,哪些告警没有对应实际风险,就降级或删除。
- 半年参数复评:把本期的实盘指标分布和阈值对照,是否需要调整;调整的理由要文档化。
- 跨策略对标:如果 A 策略和 B 策略的告警响应率差异很大,要对齐标准或了解具体原因。
十四、回到本文的核心问题
回到开头:把交易系统从「能跑」升级到「能托管真金白银」需要什么?答案是这一篇里的全部十四个层次,缺一不可:
- 监控分层让任何异常都能从 PnL 一直追到基础设施。
- 告警值班把信号转成行动,并保证有人 24×7 在键盘前。
- 熔断降级让自动化系统在 99% 的事故里不依赖人就能止损。
- 风控前置让大部分异常根本到不了交易所。
- 监管报送把内部秩序对齐到外部规则,避免「合规债务」累积。
- 事故复盘把每一次失守变成下一次的防线。
- 制度文化让上面六层不会因为一次时间紧、一次老板要、一次「就这一次」而被绕开。
- 变更管理、混沌工程、灰度发布保证系统在升级中保持稳定。
- 双人复核与复盘文化把个人的失误转变为系统的改进。
- SLO 与 error budget把稳定性需求定量化、可操作化。
- 实战风控守护用代码实现设施,避免纯口头约定。
- 组织结构、权力分配、培训体系让人能保持警觉。
- 告警治理与长期演变保证通知信息的信噪比。
少了任何一层,账户被打穿之前都不会有人察觉;而打穿是即时清算的,没有第二次机会。这是量化交易工程区别于普通后端工程的根本所在。
每一层都有成本——计算成本、延迟开销、人力投入。新团队常常从「这套设施能省吗」开始,结论往往是「省不了」。一个真实的对标数据:Knight Capital 事故的 4.6 亿美元,换算成「可以买多少套完美的量化基建」,答案是数千套。任何合理的风险成本控制都会得到一个结论:投入到这套设施里的钱,永远是最划算的投资。
更细的投资回报分析可以这样看:
- 监控 + 告警 成本:一个 Prometheus 集群 + Grafana 仪表板 + Slack 集成,初期投入约 5-10 万元,后续运维成本年 2 万。如果能提前一周发现一次 2000 万 PnL 亏损,ROI 已经是 200 倍。
- 风控体系成本:2-3 个风控工程师,年成本 50-70 万。和需要及时止损的 VaR 头寸相比,这笔投入微不足道。
- 变更管理 + 灰度成本:额外的审查、演练、基础设施支持,约占交付周期的 20-30%。换来的是坏发布率从 1/100 降到 1/10000,中间任何一次坏发布救下的 PnL,都能回本。
这些成本不是消耗,都是风险的转移成本。管理风险的钱再多都不会亏。
本系列从第一篇的「全景」讲到这一篇的「运维」,覆盖的是从 alpha 研究、数据底盘、因子、策略、组合、回测、执行、到生产运维的全链路。每一篇都是完整系统的一个层次切面。
在实际部署中,这些层次之间存在着微妙的权衡。例如:
- 监控的延迟 vs 准确性:监控指标计算得越频繁越准确,但计算本身会占用 CPU,可能影响交易延迟。需要在数据采集频率(例如 1s 一次 vs 100ms 一次)和风险感知能力之间做权衡。
- 熔断的激进性 vs 机会成本:阈值设太宽松会漏掉真正的风险,设太严格会经常误熔断,损失策略机会。历史 backtesting 和实盘滑点数据是调参的唯一依据。
- 风控的全面性 vs 延迟:pre-trade 风控检查项目越多越安全,但每一项都要占延迟预算。HFT 场景下常常不得不把部分风控移到 post-trade,或者用粗粒度的 token bucket 替代细粒度的检查。
推荐的阅读方式不是从头到尾按顺序,而是按照自己的角色需求组合:
- 如果你是研究员,重点是 09-22 篇(因子、策略、组合、回测、绩效)。
- 如果你是交易/执行工程师,重点是 23-26 篇(执行、路由、做市、HFT)。
- 如果你是基建/运维工程师,重点是 05-08 篇(数据)+ 19-21 篇(回测)+ 27-28 篇(系统 + 运维)。
- 如果你是产品经理/风控,重点是 02-04 篇(市场)+ 15-18 篇(组合 + 风控)+ 28 篇。
- 如果你是初创 CEO / 投资人,按 01 → 15 → 27 → 28 这个顺序看最快。
最后一句话:量化交易工程没有捷径,只有扎实。那些在这个领域活下来的团队,都不是因为算法独一无二或者本金特别充裕,而是因为他们对工程纪律、风险管理、回测方法论、操作规范的执行力一直保持在极高的水平。新进的团队往往会高估自己的 alpha,低估运维的成本;反复交学费之后才明白,稳定性这个课题值得投入相当的精力。
一个衡量标准:如果一个量化机构用在「风险管理 + 系统运维」上的工程资源不足总工程资源的 30%,那多半还在温水里。真正成熟的机构,这个比例通常在 40-50%,有的甚至达到 60%。这不是浪费,这是活下来的代价。选择哪一个弱化,等价于赌这一个层次的风险永远不会发生——在真实市场的长尾风险面前,这个赌局的概率永远对赌客不利。
本文涵盖的十四个主题(监控、告警、熔断、风控、合规、复盘、制度、变更、灰度、双人复核、SLO、实战代码、组织、告警演变)都不是 option,都是 must-have。不是因为「最佳实践」这个词很洋气,而是因为没有它们的系统,概率上会在某个时刻被打穿。而被打穿的代价——资本金清零、监管处罚、信誉毁灭——是任何「节省工程成本」都补不回来的。
从 2010 年的 Knight Capital 事故到 2022 年的 FTX 暴雷,十多年间我们一次次看到了相同的根本原因反复出现。这些教训不是历史,是一个行业的试错过程,每一个新进的团队都可以从中学习而不必重复。你今天在系统里多花一天时间做 pre-trade 风控检查,明天可能就不会经历一场 4.6 亿美元的事故。
这一篇到此收尾。本系列最后还有一篇总结,会回到最初的问题:一个工程师在不同阶段应该如何分配精力?从研究期的「快速迭代」到交易期的「稳定至上」,再到风险管理的「永不停歇的对抗」,每个阶段的游戏规则都完全不同。希望这 28 篇能给进入这个领域的人一张完整的地图。
本文是【量化交易】系列的第 28 篇,也是工程架构部分的收尾。从这一篇开始,后续内容会转向总结与规划:如何根据人生不同阶段的目标和资源约束,制定一份量化交易的学习与职业规划。感谢你读到这里。
更新日志:本文最后一次更新于 2024 年底,涵盖的规范和最佳实践基于当时的最新合规要求和行业实践。随着市场结构和监管环境的变化,某些具体数字和阈值可能需要调整,但整体框架应该能保持稳定 3-5 年。
提示:本系列所有文章中涉及的代码示例都基于 Python / 伪代码呈现,旨在表达逻辑而非可直接部署的生产代码。任何真实部署都需要根据具体交易所、清算商、资金方的系统要求做深度适配。特别提醒:量化交易涉及真实资金风险,任何部署前必须做充分的回测、仿真演练、小额验证。本系列不构成投资建议,仅供工程参考。
附录:关键术语速查表
本系列全文用到的某些术语的快速参考:
- SLI / SLO / error budget:服务级指标 / 服务级目标 / 允许的故障预算。用于量化系统的稳定性承诺。
- 熔断 / 降级:熔断是停止或限制服务,降级是用更简单的替代方案。量化系统里两者都用。
- 风控三层:pre-trade(订单前)/ in-trade(交易中)/ post-trade(成交后),每层都有不同的检查职能。
- 影子模式:新系统和老系统并行运行,新系统的决策不真发单,只用于对比验证。
- 灰度发布:先在小部分流量 / 资金上新版本,验证稳定后再全量推出。
- 变更审查 / CAB:Change Advisory Board,集中的变更评审机制,确保每次上线都经过充分的风险评估。
- 一票否决:风控部门对超过风险限额的任何行动有直接叫停权,无需等待其他部门的批准。
- 双人复核:任何重要操作(代码合并、风控参数修改、生产熔断解锁)都需要两个独立的人确认。
参考与延伸阅读
- U.S. Securities and Exchange Commission, “Risk Management Controls for Brokers or Dealers with Market Access” (Rule 15c3-5), Release No. 34-63241, 2010.
- U.S. Securities and Exchange Commission, “In the Matter of Knight Capital Americas LLC”, Release No. 70694, 2013.
- European Commission, Commission Delegated Regulation (EU) 2017/589 (RTS 6) on organisational requirements of investment firms engaged in algorithmic trading, 2016/2017.
- 中国证券监督管理委员会、上海证券交易所、深圳证券交易所、北京证券交易所,《证券市场程序化交易管理规定(试行)》及配套实施细则,2024 年。
- President’s Working Group on Financial Markets, “Hedge Funds, Leverage, and the Lessons of Long-Term Capital Management”, 1999.
- Google SRE Book, “Service Level Objectives” / “Embracing Risk” 章节,O’Reilly, 2016.
- 《风控引擎》:通用风控架构与限额体系。
- 《可靠性与容灾》:跨机房容灾、灾备演练、RTO/RPO。
- 《交易系统架构》:本文运维设施所运行的系统底座。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【量化交易】量化交易全景:从信号到订单的工程链路
量化交易不是策略写得好就能赚钱,更难的是把数据、特征、因子、信号、组合、执行、风控、复盘这八段链路在工程上连成一条不漏数据、不串时间、不丢订单的流水线。本文是【量化交易】系列的总目录与读图,给出八段链路的输入输出、失败模式、不变量清单,并用研究流程图把从一个想法到一笔实盘订单之间所有该过的卡点串起来。
【量化交易】市场结构:交易所、做市商、暗池、ECN
系统梳理全球市场结构(Market Structure)的工程图景:从证券交易所、衍生品交易所、加密交易所,到做市商、暗池、ECN/ATS,再到 Maker-Taker 收费、PFOF、Reg NMS 与 MiFID II 的监管影响;给出量化策略选择交易场所的判断框架与基于 ccxt 的多交易所行情聚合代码。
【量化交易】市场微结构:订单簿、价差、流动性、冲击
系统讲解市场微结构的核心概念与可计算工具:限价订单簿的数据模型、报价/有效/已实现价差、Roll 模型、四维流动性度量、Kyle's lambda、订单流不平衡(OFI)、Almgren-Chriss 框架下的临时与永久冲击、PIN 与 VPIN、Hawkes 过程,并给出基于 polars 的 L2 增量处理与系数估计代码。
【量化交易】订单类型与执行语义:限价、市价、IOC、FOK、冰山
把 Limit、Market、IOC、FOK、Iceberg、Stop、MOO/MOC 这些常被混为一谈的订单类型还原为价格、数量、时效、可见性、触发五个独立维度,并对照 A 股、港股、美股、CME、Binance 五个市场的实际语义差异,给出量化系统中的订单工厂、状态机与风控前置校验的工程实现。