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

【量化交易】智能订单路由(SOR)与暗池策略

文章导航

分类入口
quant
标签入口
#sor#dark-pool#routing#latency#fragmentation

目录

订单从交易员桌面发出去到落到某一档队列里、变成成交回报,中间走过的最后一公里几乎从不在策略代码里出现。但这一公里的工程质量决定了一笔大单实际付出多少滑点、暴露多少信息、付出多少手续费、被多少高频对手方反向选择。智能订单路由(Smart Order Routing,SOR)就是把这一公里管起来的系统:它在多个 Lit market 与暗池之间为每一片子单选择一个落点,并在回报到来后动态调整剩余分配。SOR 与上一篇讲的执行算法(VWAP、TWAP、IS、AC)是上下层关系——执行算法决定什么时候发多少,SOR 决定这一片往哪里发、用什么方式发、用什么参数发。两者在工程上常常被同一个 OMS(订单管理系统)承载,但建模逻辑迥然不同。

本文不止步于「把单子分到几个交易所」这种字面理解,而是把 SOR 的内核一层层打开:监管层面为什么会出现碎片化,目标函数里的几项成本各自如何建模,Lit market 上「队列长度 + 价位 + reserve 单」三件事如何决定 maker/taker 选择,暗池的最低成交量门槛与对手方筛选如何抑制反向选择,pinging 这种探测手段与防御手段在对抗中的均衡形态,跨市场套利与 latency arbitrage 在 SOR 视角下为什么是同一个问题的两面,加密资产里 CEX 间路由与 DEX 聚合器(1inch、Paraswap、CoW)的工程范式有什么异同,最后给出连接管理、撮合状态聚合、Failover 这一层的可落地实现骨架。

风险提示与适用范围:本文不构成任何投资建议。所有代码、参数取值、SVG 中的数值均用于说明算法与工程结构,未经过完整生产审计。SOR 涉及交易所与 ATS(替代交易系统)的具体规则、费率档、报告义务,监管口径会随地区与时间变化,本文以美股 Reg NMS、欧盟 MiFID II / MiFIR 的公开规则为参考。直接套用本文代码做实盘路由由使用者自负责任;商用部署请基于经纪商或交易所认证的 OMS / EMS(执行管理系统)组件,并对每条路由路径单独做合规与最佳执行(best execution)评估。


一、市场碎片化与 SOR 的诞生

要把 SOR 讲清楚,必须先把它出现的原因讲清楚。SOR 不是一个单纯的算法选择,是一个由监管推动的、被结构性强加给所有大型交易者的工程负担。如果同一只股票只在一个地方撮合,谁也不需要 SOR;恰恰因为「一只股票同时在 N 个场所被撮合」成了制度安排,SOR 才从可有可无的优化变成了买方机构的标配。

一·一 美国:Reg NMS 与 Order Protection Rule

美国证监会(SEC)2005 年通过、2007 年全面生效的《Regulation NMS》(National Market System,全国市场系统规则)是现代股票市场碎片化的核心法律基础。其中最关键的是 Rule 611,也就是 Order Protection Rule(订单保护规则)。这条规则要求每一个交易所、ATS 在执行一笔订单时,必须保护其他交易所上显示的、可即时成交的、最佳报价(top-of-book protected quotation)不被穿透;任何一笔成交都不能在比某个其他场所更差的价格上执行(除非该场所暂时不可达,或者是 ISO,intermarket sweep order,跨市场扫单)。

这条规则带来的直接后果有两条:第一,「最佳价格」从一个交易所内部的概念被改造成了跨场所的全国合并最佳报价(NBBO,National Best Bid and Offer);第二,因为交易所不能再凭借「我这儿成交量最大」躺着收单,新的交易所、ECN、ATS 大量出现,每个场所都以差异化的费率结构、撮合规则、客户群体争夺订单流。结果是同一只大盘股的成交量在 NYSE、Nasdaq、BATS(后被 Cboe 收购)、IEX、各家暗池之间被切成十几份,没有任何一个场所能单独提供「足够深的流动性」来吃下一笔机构订单。

碎片化反过来强制了 SOR 的存在:买方要发出一笔 5 万股的订单,必须同时看着十几个场所的盘口,把订单切成片,按价格、费率、成交概率分配到不同场所;并且因为 Rule 611 的存在,SOR 还必须主动避免自己在某个场所的成交穿透了另一个场所的更好报价(一旦发生,会触发 trade-through 投诉,经纪商承担合规责任)。

Reg NMS 的另外一条重要规则是 Rule 610(access rule),它把交易所对外的接入费上限定在 0.003 美元/股(30 mil),这同时从下方把 maker rebate 也限制在了同一量级(因为交易所要靠 taker fee 给 maker rebate 提供资金)。这条规则间接催生了「maker-taker」与「inverted」两种相反的费率模型——前者付钱给挂单(提供流动性),后者付钱给吃单(在价差不利时仍激励 taker 来),SOR 在这两类场所之间选择时必须把费率纳入成本函数。

一·二 欧洲:MiFID I 与 MiFID II 的两次切割

欧洲的市场碎片化路径与美国不同,但终点相似。2007 年生效的 MiFID I(Markets in Financial Instruments Directive I)废除了多数欧洲国家的「集中交易义务」(concentration rule),允许 MTF(multilateral trading facility,多边交易设施)与 SI(systematic internaliser,系统内部撮合商)合法地与传统交易所(regulated market)竞争同一只股票的订单流。Chi-X、Turquoise、BATS Europe 在 2008–2010 年间崛起,把欧洲蓝筹的成交份额从单一国家交易所手里抢走了 30%–40%。

MiFID II / MiFIR 在 2018 年生效,对碎片化做了第二次切割,但方向是收紧暗池。它引入了著名的 Double Volume Cap Mechanism(DVCM,双重交易量上限机制):单个场所的暗池交易量超过该证券全欧成交量 4%、或全欧暗池总成交量超过 8%,相关参考价格豁免(reference price waiver)和议价豁免(negotiated trade waiver)就要被暂停 6 个月。这条规则的本意是把交易拉回 lit market,但实际效果是订单流流向了两个新通道:LIS waiver(large in scale,大宗豁免)通道继续允许大额订单不显示——这把暗池业务集中到了真正的 block trading 平台;periodic auction(周期性集合竞价)作为新的「半透明」执行方式快速增长。MiFID II 还显著增强了 SI 制度,要求做市商在向客户报价时承担类似交易所的报价义务,这进一步多出来一类需要 SOR 评估的场所。

MiFID II 同时强化了 best execution(最佳执行)义务(RTS 27 / 28),要求投资公司每年披露其每一类金融工具的前五大执行场所、每一类客户的前五大经纪商,并解释它们在价格、成本、速度、成交可能性、订单大小等多维度上的最佳执行评估。这意味着 SOR 不再只是「省钱」的工具,更是合规证据的来源——任何一个买方机构在被监管或客户问到「你为什么把这笔单送到这儿」的时候,都必须能给出基于实测数据的答复。SOR 的日志、决策因子、回测复盘因此变成法律证据链的一部分。

一·二·补 Periodic auction 与 frequent batch auction

MiFID II 之后兴起的另一类执行场景是 periodic auction book(周期性集合竞价)。Cboe Periodic Auctions、Turquoise Plato Lit Auctions、Aquis 都属于这一类。它们的撮合规则不是连续撮合,而是短周期集合竞价:每次集合时间在 50–100 ms 之间,触发条件是池内出现可成交的双边匹配,所有当前在册的订单按价格 / 时间优先级一次性撮合。

从 SOR 视角看,这类场所的特殊性有三:

经济学家 Eric Budish 等人在 2015 年的论文里直接把 frequent batch auction 当作 HFT 武器竞赛的「市场设计应答」,建议用全市场的离散时间撮合替代连续撮合。MiFID II 的 periodic auction 不是这个学术建议的完整实现,但是它在工业里给出了一个可工作的雏形,并且在欧洲股票里占据了不可忽略的成交份额(蓝筹股约 4%–8%)。SOR 在欧洲股票上必须把 periodic auction 单列为一类候选场所,与 lit、dark 三分天下。

一·三 亚洲:单一交易所语境下的「跨账户」碎片化

中国大陆 A 股受沪深两所主导,单只股票只在一个交易所撮合,表面上没有碎片化问题。但跨账户、跨经纪商、跨衍生工具(股票 + 港股通 + ADR + 股指期货)的执行场景,依然需要 SOR 思想:同一支跨境双重上市股票(如阿里、京东在美股 + 港股、宁德时代在 A 股 + GDR)在不同市场的报价存在跨市场套利空间,机构在执行多市场篮子时仍需路由决策。香港、日本、新加坡的市场结构则与欧洲更像:HKEX 之外有 Chi-X Asia、CLSA 等 ATS 形态,机构投资者跨场所路由是常规工作。本文以美股 + 欧洲 + 加密三个语境为主,亚洲单交易所内部「订单类型选择」更接近上一篇执行算法 + 本篇第三节的内容。

一·四 SOR 的工程职责边界

把上面的监管演化合在一起,SOR 在一个买方机构的执行栈里承担的职责可以列成下面这张清单:

  1. 维护一份实时的 NBBO / EBBO(Europe Best Bid and Offer),跨场所聚合 L1 / L2 行情。
  2. 维护一份每个场所的撮合状态:自身订单在该场所的队列位置(如果场所提供)、已成交量、未成交残量、最近 N 秒的成交速率。
  3. 接收上游执行算法发来的子单切片(slice),结合实时状态算出一个分配方案,把切片再拆成若干 venue child orders。
  4. 在每一个场所选择订单类型:是 limit 挂在 BBO(best bid/offer)上、是 IOC 直接吃、是 reserve / iceberg、是 mid-peg、是 block RFQ。
  5. 收到 fill 回报后动态重路由:未成交残量根据新状态重新算一遍分配。
  6. 在「连接断开、撮合系统宕机、行情延迟跳变」时安全降级:取消该场所未成交单、把残量重路由到其他场所、记录事故日志。
  7. 输出供 best execution 报告与 TCA(transaction cost analysis)使用的完整决策日志,每一笔决策都附上当时观察到的市场状态与目标函数取值。

这几件事都是工程问题,不是策略问题。一个 SOR 系统真正区分好坏的,是它能把上述七件事做到什么颗粒度、出错时多快能恢复、决策能不能被审计回放。下面几节就一项项展开。


二、SOR 的目标函数

任何路由决策在数学上都可以归约到一句话:给每一个候选场所 v 估算一个「单位成交成本」cost_v(x_v),在 Σ x_v = q 的约束下选择分配 {x_v} 使总成本最小。SOR 与执行算法在这一句话的层面是一致的,区别只在「时间维度」上:执行算法决定 q 在 t 上怎么分布,SOR 决定 q_t 在 v 上怎么分布。

二·一 成本的五项分解

cost_v(x) 完整展开,任何成熟的工业 SOR 都至少包含下面五项:

\[ \text{cost}_v(x) = \underbrace{\tfrac{1}{2}\,s_v}_{\text{spread}} + \underbrace{\eta_v\,(x/Q_v)^{\alpha_v}}_{\text{impact}} + \underbrace{f_v - r_v}_{\text{net fee}} + \underbrace{\lambda\,L_v\cdot|\dot p|}_{\text{latency}} + \underbrace{\gamma\,\theta_v(x)}_{\text{info leak}} \]

每一项的含义、估计方法、数据需求都要拆开看。

Spread(价差)项 ½·s_v:对吃单方向,等于穿过半个买卖价差所需付出的成本,单位是价格。如果订单大到打穿一档,就要再加上跨档成本,写成 ½·s_v + Σ_k (p_k − p_1)·q_k / x 的加权平均,其中 p_k 是 k 档卖单价格、q_k 是该档存量。挂单方向的 spread 成本理论上是负的(赚价差),但要乘以成交概率 π_v——只有在被对手方吃到的那部分挂单才真正赚价差。

Impact(市场冲击)项:用一个幂律函数估计「此次发单本身对成交价的不利推动」。η_v 是该场所的瞬时冲击系数、Q_v 是该场所近 N 分钟的平均成交量代理,α_v 通常取 0.5(与 Almgren-Chriss、Kyle’s lambda 的实证一致)。冲击在 lit market 上随订单尺寸单调上升,在暗池上可以近似为零(不显示则不冲击),但暗池的「实际冲击」会通过信息泄漏这一项重新出现。

Net fee(净费率)项 f_v − r_v:包含 taker fee、maker rebate、清算费、监管费(SEC fee、TAF)、PFOF(payment for order flow)回流(仅美股零售)。这一项是确定性的,但符号取决于订单类型:吃单付 taker fee(正数),挂单收 maker rebate(负数),inverted 场所反过来。实测中,一个高频做市商单笔订单的费率项可以从 −25 mil 到 +30 mil,跨度比 spread 还大,所以费率不是「小项」。

Latency(延迟)项:用 L_v · |dp/dt| 估计「从决策到落单这段 RTT 内市场发生不利价格变化的期望」。L_v 是该场所的端到端 RTT,|dp/dt| 是当前波动状态下的预期价格漂移速率。这一项在普通市场状态下很小(µs 级 RTT × 平稳波动),在事件期(数据发布、风险事件)会指数级放大,因为 |dp/dt| 突然变大。λ 是风险厌恶参数。

Info leak(信息泄漏)项:暗池路由特有的非显示成本。当一个 buy 子单被某暗池的 HFT 对手方 ping 出来后,对方会在 lit market 上抢先建仓,等买盘把价格推上去再卖回。θ_v(x) 是「此次发到 v 的尺寸 x 在未来 ΔT 内对市价的不利影响」,可由历史路由日志估计:把每一次发往 v 的暗池单作为事件时间点,看接下来 30 秒、5 分钟、30 分钟的市场涨跌幅相对发单方向的偏差,分场所做条件期望。γ 是另一个风险厌恶参数。

二·二 目标函数的两种形态:单切片成本最小 vs 全程实施成本最小

把上面五项加起来后,SOR 的目标函数还有两种写法。

单切片版本(每个 slice 独立优化):

\[ \min_{x_v \ge 0} \sum_v \text{cost}_v(x_v),\quad \text{s.t. } \sum_v x_v = q_t,\quad x_v \le \text{liq}_v \]

liq_v 是该场所的可见 + 估计隐藏流动性。这种写法假定每个 slice 之间相互独立,是大多数生产 SOR 的简化版本。

全程版本(多 slice 联合优化,与执行算法耦合):

\[ \min_{\{x_{v,t}\}} \sum_t \sum_v \text{cost}_v(x_{v,t}) + \text{IS\_penalty}(\sum_{v,s\le t} x_{v,s}) \]

把 SOR 与执行算法(implementation shortfall 惩罚项)联合求解,理论上更优,但维度爆炸、状态空间巨大,工业实现里通常用层级分解:上层执行算法每 1–5 秒按 IS 框架决定 q_t,下层 SOR 在 q_t 给定后做单切片优化,并把执行回报反馈给上层做下一个 q_{t+1}。

二·三 非线性与非凸:为什么不直接调 scipy.optimize

cost_v(x) 在 lit market 上是分段线性 + 凸的(spread 跨档分段、impact 二次以上),在暗池上是非凸的——成交概率 π_v(x) 经常长成 sigmoid 形(小尺寸基本成交不了、过 MES 后骤然上升、再加大尺寸又因为对手方匹配不上而下降)。在加密 DEX 上 price impact 是封闭曲线(AMM 的 x·y=k 推出的对数函数),凸但带约束。直接拿 scipy.optimize.minimize 放进去经常出现:

工业实现的常见做法是:

  1. 把成本函数线性化 + 二次化,得到 QP(quadratic programming);
  2. 用 KKT 条件给出闭式解(见后面 Python 实现);
  3. 暗池非凸部分用离散选择(要么发 0,要么发 ≥ MES)枚举两个分支,分别在 lit 上凑剩余切片,比较总成本。

二·四 成交概率 π_v 的实证估计

π_v(x, t) 在工程里通常用半参数方法估计。最简单的做法是把每一笔历史挂单的「挂入时盘口快照 → 是否在 ΔT 内被吃掉」作为二分类样本,特征用 (该价位队列长度、价位距 BBO 距离、过去 N 秒成交速率、过去 N 秒队头取消速率、波动率水位),标签用 0/1。用 logistic regression 或 GBDT 训练 P(fill within ΔT | features)。线上部署时把当前盘口快照喂进去得到 π_v。

更进一步可以用生存分析(survival analysis):把「挂单到成交」当作一个 censored 事件(部分挂单还没成交就被自己撤掉算 censored),用 Cox 比例风险模型估计 hazard。这个方法的好处是能输出 P(fill | survival to t') 而不仅仅是 P(fill within ΔT),让 SOR 在「等多久重路由」这件事上有连续依据,而不是写死 ΔT = 5 秒。

工程上要注意的几个陷阱:

二·五 冲击系数 η 的标定

冲击系数 η_v 不是观察来的,是标定来的。常用做法是按 Almgren 等人 2005 年的方法:用过去 N 个月内自家发出的所有大单(≥1% ADV)作为样本,每一笔计算 implementation shortfall(开始执行时刻中点价 → 加权平均成交价)与参与率(执行量 / 同期市场总量)。把 IS 对参与率画散点图,用 IS = η · participation^α 拟合两个参数。α 在不同市场实证里在 0.4–0.6 之间,与 Almgren-Chriss 给出的 0.5 接近。

不同场所的 η 通常不一样,因为同一笔订单在两家场所之间「实际效用」不一样:流动性差的小所,每多打一手对价格的推动比大所大得多。生产 SOR 应当分场所标定 η,而不是用一个全市场的统一系数。

二·六 λ 与 γ 的取值

成本函数里两个风险厌恶参数 λ(latency)与 γ(leak)的合理取值经常被工程师忽略,结果路由器输出与人类直觉差很远。常用方法是通过 TCA 的实证回归反推

  1. 把过去 N 个月所有路由决策按 venue 分桶;
  2. 对每一桶,回归「实际 IS」对「该 venue 的 latency」「该 venue 的 mark-out」;
  3. 回归系数就是 λ、γ 的估计。

直觉上,λ ≈ 1(latency 风险按 1:1 计入)、γ ≈ 0.5–2(信息泄漏按 0.5–2 倍 mark-out 计入),具体值随对手方画像变化。每季度重标定一次是合理频率。

二·七 多目标与 Lagrangian

实务里 SOR 不是只有一个目标,至少有四个目标同时存在:最小化总成本、最大化成交概率、最小化信息暴露、满足合规约束。把它们写成单一目标会丢失信息。一个更工程化的写法是 Lagrangian:

\[ \mathcal{L} = \sum_v \text{cost}_v(x_v) - \mu_1 \cdot \mathbb{P}[\text{fill} \ge q_t \cdot \kappa] + \mu_2 \cdot \text{leak}(x) + \sum_k \mu_k g_k(x) \]

其中 κ 是「至少成交比例」(比如 0.7),g_k 是合规与风险约束(单场所最大份额、单股最大持仓、跨场所交易报告义务)。Lagrangian 形式让每个目标的权重可调,并能在调度器层面对不同投资组合采用不同的 (μ_1, μ_2):长线 PM 的母单可以容忍低成交率换低成本,短线策略的母单要高成交率不惜付滑点。


三、Lit market 路由策略

「往 lit market 发」表面看起来简单——选一个价、点 send。但同一个价位至少有四种不同的发法,对应四种不同的成本结构。

三·一 四种基本发单模式

模式 A:post-only at BBO(在 BBO 挂 maker 单)。把一个 buy 子单挂在 best bid 上(不主动跨价差)。优点是没有 spread 成本、能拿 maker rebate,缺点是成交不确定(要等到对手方主动卖到这个价上来)。成交概率取决于「此时此刻 best bid 的队列长度 + 该价位的成交速率」。

模式 B:post-only at BBO+1(更激进的 maker,往内挪一档)。把 buy 单挂在 best bid + 1 个 tick 上,这样它立刻成为新的 BBO。优点是队头位置(time priority 最高),代价是新 BBO 一旦出现,整个市场的 reference price 就跟着移动,对抗策略可以围绕你这个新 BBO 做反向选择。

模式 C:IOC sweep(fill-or-kill 跨价差扫单)。直接跨过 spread,吃掉对侧 best ask 上的所有挂单(如果还有残量就继续吃下一档),未能立刻成交的部分立即取消。优点是确定性成交、零成交不确定性;缺点是付完整 spread + 跨档冲击 + taker fee。

模式 D:Reserve / iceberg(隐藏数量挂单)。把一个 50,000 股的子单挂在 BBO,只显示 1,000 股,其余 49,000 在交易所内部隐藏。对外看起来是 1,000 股的薄挂单,每被吃掉 1,000 就自动补上下一个 1,000。优点是大额单不暴露真实尺寸、避免显示尺寸触发对手方反应;缺点是隐藏部分牺牲了一部分时间优先权——多数交易所规则规定,隐藏部分在同一个价格档上排在所有显示量之后(Nasdaq、IEX、Cboe 都是如此)。

三·二 队列模型:成交概率怎么算

模式 A 与 D 的关键问题是估计成交概率 π。最简单的队列模型把交易所看成 M/M/1 排队系统:到达率 λ(taker 单到达 best bid 把队头吃掉的速率),队列长度 N。如果你新挂的 maker 单排在队尾,你成交需要前面 N 单都被吃掉。在简单泊松假设下,N 单都在时间 T 内被吃掉的概率是 P(K_T ≥ N),K_T 是参数 λT 的泊松随机变量。

工业建模会更细致:

把这些建模合在一起,估计出 π_v(x_v, t),再乘以「成交后的成本节省」(spread/2 + maker rebate)减去「未成交的机会成本」(被迫去其他场所 IOC 吃单付出的成本),就是 maker 模式的期望收益。

三·三 Reserve 单的取舍

Reserve 单看起来像 free lunch(既隐藏又能成交),但有两个隐性成本。第一,时间优先权牺牲:上面已经提到,隐藏部分排在显示量之后;第二,显示部分被反复重启:每次显示部分被吃完、补出新的 1,000 股,这个新 1,000 在多数交易所规则下被视为「新订单」,时间优先权重置到队尾。这意味着一个 50K 的 reserve 在繁忙的盘口里,可能要排队 50 次、每次都从队尾开始。

Reserve 的合理使用场景是「中等尺寸 + 对手方慢节奏」:盘口流速适中,显示部分能在合理时间被自然吃到,且单笔显示量不大到引起算法注意。对超大单(几十万股以上、>5% ADV)reserve 的效率不如直接走暗池或 block RFQ。

三·四 实战:分场所选择 maker / taker

把上面三小节合起来,单切片 SOR 在 lit market 上的具体决策可以写成下面的伪流程:

  1. 拉取所有候选 lit 场所的 L2,得到 NBBO;
  2. 对每个场所 v 估算:
    • π_v_post:在 best bid 挂 maker 的成交概率(队列模型);
    • π_v_post_plus:在 best bid + 1 挂的成交概率;
    • cost_v_ioc:直接 IOC 吃单的成本(spread + 跨档 + taker fee);
  3. 计算 cost_v_post = (1-π_v_post)·fallback_cost + π_v_post·(0 − maker_rebate),其中 fallback_cost 是 maker 不成交后被迫 IOC 的成本估计;
  4. 在所有场所、所有模式中选出 (v*, mode*) = argmin cost
  5. 如果 mode* 是 IOC,发出后立即收到 fill;
  6. 如果 mode* 是 post,挂单后等 ΔT,超时未成交就重路由:把残量按当前状态重新算一遍。

四、暗池路由

暗池(dark pool)是不公开显示订单簿的交易场所。它在 SOR 决策矩阵里占据一个特殊位置:它能消除显示型市场冲击,但代价是引入一个新的、隐性的对手方质量风险。把暗池用好的核心,是把这个新风险量化、纳入成本函数。

四·一 暗池的三种类型

按对手方与撮合规则可分为三类:

类型一:Bank-owned ATS / Broker-dealer dark pool。Goldman Sigma X、UBS ATS、Morgan Stanley MS Pool 等等。撮合规则通常是 mid-point matching(在 NBBO 中点撮合),意味着 buy/sell 双方各让出半个 spread。这类池子的对手方混合了买方机构、做市商、内部 HFT desk,质量良莠不齐。

类型二:Block trading platform。Liquidnet、Posit、Cboe LIS。专门面向超大额单(block),常配合最低成交量门槛(MES,minimum execution size)—— buy 与 sell 必须双方都同意一个最小量才能撮合,比如 5,000 股或 $1M。对手方多为 buy-side(养老金、共同基金),HFT 参与受限。这类池子在 MiFID II 的 LIS waiver 下尤其重要。

类型三:Exchange-operated dark order。在 lit 交易所内部的隐藏订单类型(hidden order、mid-peg、d-quote 等),技术上不算独立场所,但在 SOR 视角下与暗池一致——对外不显示,按特定规则撮合。

四·二 最低成交量门槛(MES)的作用

MES 是暗池抑制 pinging(信息探测)的核心机制。一个 HFT 对手方想试探暗池里有没有大单,最便宜的办法是发一个 100 股的 buy 单进去;如果有人挂了 sell,它会立刻吃掉这 100 股得到一个回报,从而推断出「这个池子里有 sell 流」,然后跑到 lit market 上反向做空、推低价格、等买盘自己被推上去。

设置 MES = 5,000 股就让这个 100 股的探测立即失效——MES 以下的 buy 单连参与撮合的资格都没有。代价是真正的小买方(比如 buy-side 的零碎单)也被挡在外面。MES 是「过滤探测者」与「过滤合法小单」之间的权衡。买方机构在向暗池路由时,应当把自己 slice 切到 ≥ 该池 MES 的尺寸,否则白白走一遍连接也不会成交。

四·三 对手方筛选

主流暗池都允许参与者设置对手方类型偏好(counterparty type preferences):是否愿意与 retail-flow internalizer、buy-side、HFT、proprietary trading shop 撮合。一个理性的买方会在向暗池发单时勾选只与 buy-side 对手方撮合,从而避开 HFT 主导的反向选择。代价是成交概率显著下降(有 HFT 时池子流动性高、没有 HFT 时池子可能很久没人)。

工业 SOR 维护一份暗池毒性评分(toxicity score),按对手方类型组合分别打分,并随时间更新。每个池子会定期发 IOI(indication of interest,意向指示),SOR 据此估计该池子在每只股票上的实时活跃度。再结合「最近 N 笔与该池成交后未来 K 秒的价格漂移」(短期反向选择度量,由 Easley、López de Prado、O’Hara 提出的 VPIN 思想类比扩展),给该池打分。评分高(毒性低)的池子分配更多。

四·四 信息泄漏成本的度量

第二节里 θ_v(x) 这一项的具体度量方法值得单独说。最常用的指标叫 mark-out

\[ \text{mark-out}_{v}(\Delta t) = \mathbb{E}\big[(p_{t+\Delta t} - p_{\text{fill}}) \cdot \text{side}\big] \]

side = +1(buy)/ −1(sell),p_fill 是当时的成交价。如果一个买单成交后市场继续上涨,mark-out 为负(你买便宜了,幸运成交);如果市场反向下跌,mark-out 为正(你被反向选择了)。把每一笔成交按 venue 分桶,计算 30 秒 / 5 分钟 / 30 分钟的 mean mark-out,得到一个分场所的「短期 toxicity」曲线。这个曲线就是 θ_v 的实测值,可以直接进 SOR 成本函数。

实际工程里,mark-out 数据要跟「市场整体漂移」做基准化(用 NBBO 中点的同期变化做对比),否则会把市场整体的方向性涨跌错算成场所毒性。

四·五 暗池路由的整体决策

合在一起,暗池路由的工程逻辑是:

  1. 候选池子集合 V_dark,每个池子有 (MES_v, π_v(x), θ_v, fee_v);
  2. 当前切片大小 q,根据 MES 过滤掉 q < MES_v 的池子;
  3. 估计每个剩余池子的成交概率 π_v 与毒性 θ_v;
  4. 期望成本 cost_v = π_v · (-spread/2 + θ_v + fee_v) + (1-π_v) · fallback_cost_lit,其中 -spread/2 是中点撮合相对 lit IOC 的价差节省(取负号是收益);
  5. 在 lit + dark 整个集合上联合优化(第二节目标函数),得到分配 {x_v}。

实战中常见的策略是「先 dark 后 lit」:把暗池当成 X 秒的「机会窗口」,向多个池子并行发非显示单(IOC 或 GTC),等 X 秒;命中的部分立即扣减残量,未命中的部分再走 lit。X 通常取几百毫秒到几秒,与 slice 持续时间相比要短一个数量级。


四·六 暗池毒性的实证分布

把 mark-out 数据按场所按时间段做分桶,工业里观察到几个稳定的实证规律:

这些规律本身不能直接套用,每个买方机构必须用自己的成交数据做估计——同一个池子对你的 mark-out 与对别人的不一样,因为它取决于你母单的方向性、规模、时机分布。「公共暗池毒性表」是噪音,「我自己在该池上的 mark-out 时序」才是信号。

四·七 暗池的反 Pinging 设计与买方对抗

接下来一节会专门讨论 ping 与反 ping,这里只先点出:暗池侧的设计动机与买方机构的使用动机是部分对齐的——双方都想压制 HFT 探测,但暗池又依赖一定的 HFT 流量保流动性。这种结构性张力让暗池的反 ping 设计永远不彻底。


五、Pinging 与反 Pinging

暗池里最常被讨论的对抗形态是 pinging:试探者发出小额订单试图发现隐藏流动性。MES 是主要防御之一,但还有更多。

五·一 Pinging 的目的与形态

Pinging 不仅仅是「发小单看回报」,主要有四种形态:

形态 P1:尺寸 ping。发若干个尺寸递增的小单(100、200、500、1,000),观察哪一个尺寸开始成交,反推对手方在哪个尺寸级别上有挂单。

形态 P2:价格 ping。发若干个价格递进的 IOC 单(best bid、best bid − tick、best bid − 2 ticks),观察成交分布,反推订单簿的真实形状(含隐藏量)。

形态 P3:Cross-venue ping。同时向多个暗池发非常相似的小单,观察哪个池子先成交、成交后剩余池子的状态如何变化(lit market 价格是否漂移),由此推断哪些池子之间共享对手方流量

形态 P4:时间 ping。在一段固定的时间窗口里反复发标准化小单,分析池子在不同时段的活跃度模式,构建对手方时段画像。

每一种 ping 都对应一种特定的对手方类型与目的,但共同点是:通过低成本探测获取关于隐藏流动性的信息,再到其他场所变现

五·二 暗池侧的反 Pinging 机制

暗池为防御 ping 在工程上做了多层措施:

机制 D1:MES。已述。

机制 D2:随机化匹配延迟。对到达的订单引入一个随机化的撮合延迟(10–500 ms 之间均匀分布),让 ping 到的回报时间不可预测,破坏对手方的延迟测量。

机制 D3:对手方筛选。买方机构可以勾选「不与 HFT 撮合」,由暗池在撮合阶段过滤。

机制 D4:行为启发式封禁。监控每个参与者的下单 / 撤单 / 成交比,若超阈值(比如 fill ratio < 5% 的频繁小单)暂停其访问。

机制 D5:IOI 不广播。早期一些暗池会把池内 IOI 推送给特定 broker,被认为是隐性 pinging 通道;MiFID II 之后大幅收紧。

这些机制的成本是「让真正的小买方也变慢了」,所以买方机构经常在多个池子里挑「反 ping 力度合适的」池子——太松的容易被 ping 走,太严的成交慢。

五·三 买方策略:避免被 ping、避免主动暴露

作为买方机构,避免被 ping 的策略要点:

反过来,主动 ping 在监管上属于灰色地带:MiFID II 与 FINRA 都有相关规则禁止「明知不打算成交的下单」(layering、spoofing),但小额试探单本身合法。买方机构在自己使用 ping 来构建市场感知时,应当走专门的合规审批与日志通道,与正常路由流量分离。


五·四 Spoofing、layering 与监管红线

值得专门点出,spoofing(虚假报价)与 layering(分层报价)在所有主流司法辖区都是非法的。Spoofing 的定义是「下单时已无意成交,目的是引导其他参与者反应,自己在反应发生后撤单获利」。2010 年 Dodd-Frank 法案在美国把 spoofing 明确列为刑事犯罪;MiFID II 与 MAR(Market Abuse Regulation)在欧洲做了类似规定。CFTC 与 SEC 已在多起案件中对 spoofing 行为给出超过 100 万美元罚款与刑事起诉,包括 2014 年 Sarao 案与 2020 年 JP Morgan 9.2 亿美元和解案。

Pinging 在监管定义上不属于 spoofing——一个发出去的 100 股小单,只要有合理预期可能成交就不构成 spoofing,即使发单方真实意图是探测。但接近边界的行为(发单后 µs 级取消、跨场所协调取消)可能触发 layering 调查。所有 SOR 工程团队应当:

这件事不是技术问题,是合规问题,但反向影响了 SOR 的设计自由度。


六、跨市场套利与 latency arbitrage 的 SOR 视角

到这一步,本文一直把 SOR 描述成「服务于一笔买方单」的工具。但 SOR 的同一套机制,在 latency arbitrage(延迟套利)与跨市场套利交易者眼里是主动盈利模型。把这两个角度并起来看,能让买方更清楚自己面对的对手方在做什么。

六·一 Latency arbitrage 的本质

经典 latency arb 的故事是:A 交易所与 B 交易所撮合同一只股票。A 发布了一笔大单成交、价格从 100 跳到 100.05;B 由于 SIP(securities information processor,证券信息处理器)的合并行情延迟,几百微秒后才能看到 A 的最新价。一个有 co-lo 直连的 HFT 在 A 看到跳动后立刻向 B 发单:吃掉 B 上还停留在 100 的所有 ask(或挂在 99.95 的 bid 上等买方来吃),等 B 的价格几百微秒后跟上 A,HFT 已经把仓位 lock 在了好价格上。

从 SOR 视角看,这是一种「逆向 SOR」:HFT 跨场所路由的不是买方的合并订单,而是它自己的套利信号。它的成本函数里把 latency(自身相对其他参与者的延迟优势)写成负值——延迟越低、套利窗口越大、利润越大。

六·二 对买方的影响

这件事直接吃买方机构的肉:

防御措施有几个:

措施一:用 SIP 对齐而不是直连价差。买方 SOR 不要在不同场所之间做超低延迟同步触发,给对手方的 latency arb 制造机会;但反过来又损失自己的执行质量。

措施二:使用 IEX 等带 speed bump 的场所。IEX 在所有进入订单上引入了 350 µs 的强制延迟(physical coiled fiber bump),让 HFT 的速度优势失效。买方机构在 IEX 上 IOC 的反向选择 mark-out 显著低于其他场所。

措施三:Cross-venue ISO。Reg NMS 规定的 intermarket sweep order 允许买方在主场所成交时同时向其他所有场所发出锁价的 IOC,把跨场所抢跑窗口压到最短。SOR 必须在执行 ISO 时同步发出,且每一个被发出的 ISO 都要附上 trade-through 标记。

措施四:Mid-peg only。完全不显示在 lit market 上、只在中点撮合,让 HFT 的 latency 信号没有锚点可抢。

六·三 跨市场套利与 best-ex 的张力

跨市场套利在监管上是合法的(提供跨场所价格收敛),但它消耗的就是买方的执行质量。这是 best-ex 框架与市场效率框架之间的根本张力。买方机构不能寄希望于监管单方面消灭这种成本,只能通过 SOR 的工程选择把暴露面最小化:场所选择(避开 HFT 集中度高的)、订单类型选择(mid-peg、IOC ISO)、时序选择(避开高波动事件窗口)。


六·四 Speed bump 的多种形态

IEX 的 350 µs coiled fiber 是最知名的 speed bump,但工业里还有几种变体:

监管争议焦点是:speed bump 是否违反「价格保护」原则——SEC 批准 IEX 时讨论了将近两年,最后裁定 350 µs 因为「足够小」不构成对 NBBO 的实质性穿透,但任何超过 1 ms 的 speed bump 都会触发 protected quote 资格的重新评估。SOR 在选择 speed bump 场所时要把「该场所被对手方故意忽略」的概率纳入——如果 IEX 的报价被算法跳过,IEX 上挂单的成交概率会显著降低,节省反向选择的同时损失成交率。

六·五 跨市场套利的对手方画像

跨市场套利者大体分两类:纯 latency arb(吃信息延迟)与 statistical arb(吃跨场所定价偏差,hold 几秒到几分钟)。两者对 SOR 的影响形态不同:

把对手方画像写进 SOR 的方法是在每只股票上维护一份「对手方分布」估计:通过历史 mark-out 在不同时间尺度(30s、5m、30m)的衰减形态,反推主导对手方是 latency arb 还是 stat arb。前者 mark-out 在 30 秒内迅速回落,后者在 30 分钟内才衰减。SOR 据此调整切片节奏与场所偏好。


七、加密资产 SOR

加密资产 SOR 与传统股票 SOR 在算法层面同源,但在工程层面差异巨大,以至于很多传统经验直接套过来会出错。

七·一 CEX 间路由

加密 CEX(centralized exchange,中心化交易所)有几十家:Binance、OKX、Coinbase、Kraken、Bybit、Bitfinex 等等,同一个币对(如 BTC/USDT)在每家盘口都有独立流动性,没有任何全球 NBBO 机制把它们绑在一起。这意味着同一时刻 BTC 在 Binance 是 67,000、在 OKX 可能是 67,012、在某个二线交易所可能是 67,050——12–50 美元的瞬时差异是常态,几百毫秒内会被套利者抹平。

加密 SOR 的特殊性:

工程上,加密 CEX SOR 的最小可用形态:维护一份多所聚合订单簿(CCXT、CryptoCompare、自家 WS 接入),实时计算 cross-exchange best bid/best ask,在每一片 slice 决策时跑一次第二节的优化器,加上余额、限流约束。

七·二 DEX 聚合器

DEX(decentralized exchange,去中心化交易所)的 SOR 形态完全不同。链上交易没有「订单簿」概念(除少数 orderbook DEX 如 dYdX),主流是 AMM(automated market maker,自动做市商):Uniswap V2/V3、Curve、Balancer、SushiSwap、PancakeSwap 等等,每个流动性池是一个 (TokenA, TokenB) 的恒定函数曲线(V2: x·y=k,V3: 集中流动性)。

DEX 聚合器(aggregator)如 1inch、Paraswap、0x、CoW Swap、KyberSwap 的工作就是 SOR:给定输入 token A、目标 token B、数量 x_A,找一条穿越多个 DEX 与多跳(multi-hop)的最优路径使输出 x_B 最大。它要做:

  1. 路径搜索:在 token 图(节点 = token,边 = 池子)上做受限的最短路径搜索,常用 Dijkstra 变体;
  2. 池间分配:把同一对 token 的输入分到多个池子里降低单池冲击(split routing);
  3. gas 成本估计:每多一跳就多消耗 gas,gas 也算进 cost;
  4. MEV 防护:通过 Flashbots private mempool、commit-reveal、CoW 的 batch auction 等机制避免被三明治攻击;
  5. 滑点保护:每条路径附带最小可接受输出 (minOut),链上执行时低于该值则 revert。

DEX 聚合器的成本函数大体是:

\[ \text{cost} = \underbrace{\text{slippage}(\text{path})}_{\text{曲线决定}} + \underbrace{\sum_{\text{hops}} \text{gas}}_{\text{执行成本}} + \underbrace{\text{MEV expected loss}}_{\text{被夹收益}} + \underbrace{\text{aggregator fee}}_{\text{协议费率}} \]

跟 CEX SOR 比,DEX 聚合器面对的是一个完全确定性的池子曲线(链上状态在 next block 之前完全公开),所以理论最优解是确定的;但MEV 攻击者也看得到同一份状态,会在你的交易前后插单赚你的滑点。所以 MEV 这一项的实测值很大,且严重依赖私有 mempool 的可用性。

七·三 CEX-DEX 混合路由

最复杂的形态是 CEX-DEX 混合 SOR:一个用户想用 1,000 USDT 换一个长尾 token,最优路径可能是「Binance 用 USDT 换 ETH → 提到链上 → Uniswap V3 ETH 换长尾 token」。这条路径涉及:CEX 撮合延迟、链上确认时间(数秒到数分钟)、跨链桥(如果跨链)、gas 费、MEV 窗口。算总成本时不能只看价格,要把时间风险(执行期间币价漂移)也纳入。CoW Protocol、UniswapX、Hashflow 等正在做这一类「intent-based trading」,让用户只表达「我要 X 换 Y」,由 solver 网络竞标提供最优路径。这是加密 SOR 的下一代形态,但现阶段在工程稳定性上仍待验证。


七·四 加密 SOR 的几个特殊难题

把上面三种形态合在一起,加密 SOR 有四个传统市场没有或者不严重的难题:

难题一:余额预分配的资金成本。要在 N 家所同时路由,必须在 N 家所都备好足够保证金。这部分资金不能动,年化资金成本(按机会成本估)通常 5%–15%,远比传统 prime broker 的资金成本高。SOR 的「最优分配」如果总是把一家所留太多余额、另一家一直空仓,TCA 看起来漂亮但综合成本反而上升。生产 SOR 应当把余额再平衡的成本写入目标函数,而不是只看单笔执行。

难题二:API key 与冷热钱包分层。出于安全考虑,单个 API key 通常只能访问一部分资金(热钱包),剩余在冷钱包。SOR 可路由的资金量受限于热钱包余额,热钱包补给依赖人工或半自动流程。母单大到一定程度时,热钱包不够支撑就要走链上提币,SOR 必须能识别这种「容量天花板」并提前与上层沟通。

难题三:时序与时钟漂移。每家 CEX 的 WebSocket 时间戳定义不同(订单簿更新时间 vs 撮合时间 vs 推送时间),本地接收时间与服务器时间通常有 50–200 ms 漂移。多所聚合订单簿的时序合并是噪音重灾区,最佳实践是用本地接收时间戳重对齐 + 用每家所的 sequence number 做完整性检查,丢包时主动 resnapshot 而不是用旧数据。

难题四:交易所故障与撤单失败。加密交易所的可用性远低于传统交易所——单家所每年若干小时全停是常态、API 间歇性返回 500 是日常。SOR 必须假定「下单成功 + 撤单失败」的场景会发生,并在 reconciliation 流程里能识别出「我以为撤掉了但实际还在挂着」的孤儿单,避免被市场反向选择走。这一点和传统 ATS 失联场景类似,只是频率高一个量级。


八、工程实现

把上面七节合在一起,最后这一节给一个能跑起来的最小工程骨架。代码不是生产级,而是说明「关键组件接口长什么样」。

八·一 多场所订单簿模拟

# sor_sim.py — 多场所订单簿与简单 SOR 决策器
import numpy as np
from dataclasses import dataclass, field
from typing import List, Dict, Tuple

@dataclass
class VenueBook:
    """单场所订单簿快照(仅 top 5 档,足够说明 SOR)。"""
    name: str
    bids: List[Tuple[float, int]]    # [(price, qty), ...] 价格降序
    asks: List[Tuple[float, int]]    # [(price, qty), ...] 价格升序
    fee_taker: float                 # 例如 +0.0003 (30 bps)
    fee_maker: float                 # 例如 -0.0002 (-20 bps,rebate)
    latency_us: float                # 单向 RTT 一半,微秒
    is_dark: bool = False
    mes: int = 0                     # 暗池最低成交量
    toxicity: float = 0.0            # 历史 mark-out 估计(bps)

    def best_bid(self) -> float:
        return self.bids[0][0] if self.bids else float('-inf')

    def best_ask(self) -> float:
        return self.asks[0][0] if self.asks else float('inf')

    def mid(self) -> float:
        return 0.5 * (self.best_bid() + self.best_ask())

    def sweep_cost(self, side: str, qty: int) -> Tuple[float, int]:
        """估算 IOC 吃单的平均成交价与实际可成交量(不超过盘口存量)。"""
        levels = self.asks if side == 'buy' else self.bids
        remaining = qty
        notional = 0.0
        filled = 0
        for px, sz in levels:
            take = min(sz, remaining)
            notional += take * px
            filled += take
            remaining -= take
            if remaining <= 0:
                break
        avg_px = notional / filled if filled else float('nan')
        return avg_px, filled


@dataclass
class MarketSnapshot:
    venues: Dict[str, VenueBook]

    def nbbo(self) -> Tuple[float, float]:
        """NBBO(不区分暗池)。"""
        bb = max((v.best_bid() for v in self.venues.values() if not v.is_dark),
                 default=float('-inf'))
        ba = min((v.best_ask() for v in self.venues.values() if not v.is_dark),
                 default=float('inf'))
        return bb, ba

VenueBook 把单场所建模到「足够 SOR 推理」的颗粒度:top-5 档、费率、延迟、毒性。MarketSnapshot 聚合所有场所并提供 NBBO。

八·二 成本模型与决策器

@dataclass
class SORDecision:
    venue: str
    qty: int
    mode: str          # 'ioc' / 'post' / 'dark'
    expected_cost_bps: float

class SORRouter:
    """简单 SOR 决策器:min Σ cost_v(x_v)。"""

    def __init__(self,
                 lambda_latency: float = 0.05,    # 延迟惩罚权重
                 gamma_leak: float = 1.0,         # 信息泄漏权重
                 vol_per_us: float = 1e-7):       # 每 µs 价格漂移期望(bps)
        self.lam = lambda_latency
        self.gamma = gamma_leak
        self.vol_per_us = vol_per_us

    # ---- 五项成本估计 ----

    def _spread_cost_bps(self, v: VenueBook, side: str, qty: int, ref_mid: float) -> float:
        """穿越价差 + 跨档成本(lit IOC)或中点节省(dark)。"""
        if v.is_dark:
            # 中点撮合:相对 NBBO 中点的价差节省 = 半个 NBBO 价差
            return 0.0  # 中点 = 参考 mid,相对 ref_mid 不付 spread;节省由 fallback 体现
        avg_px, filled = v.sweep_cost(side, qty)
        if filled == 0:
            return 1e6  # 不可成交,惩罚极大
        sign = 1 if side == 'buy' else -1
        return sign * (avg_px - ref_mid) / ref_mid * 1e4

    def _impact_bps(self, v: VenueBook, qty: int) -> float:
        """简单二次冲击:η · sqrt(qty / Q_v),Q_v 取盘口前 5 档总量为代理。"""
        Q = sum(sz for _, sz in v.asks[:5]) + sum(sz for _, sz in v.bids[:5])
        if Q <= 0:
            return 1e6
        return 5.0 * np.sqrt(qty / Q)  # 5 bps · sqrt(participation)

    def _fee_bps(self, v: VenueBook, mode: str) -> float:
        if mode == 'ioc':
            return v.fee_taker * 1e4
        elif mode == 'post':
            return v.fee_maker * 1e4   # 通常为负
        elif mode == 'dark':
            return v.fee_taker * 1e4 * 0.5  # 暗池常按 taker 半价收
        return 0.0

    def _latency_bps(self, v: VenueBook) -> float:
        return self.lam * v.latency_us * self.vol_per_us * 1e4

    def _leak_bps(self, v: VenueBook) -> float:
        return self.gamma * v.toxicity if v.is_dark else 0.0

    def _venue_cost(self, v: VenueBook, side: str, qty: int,
                    mode: str, ref_mid: float) -> float:
        return (self._spread_cost_bps(v, side, qty, ref_mid)
                + self._impact_bps(v, qty)
                + self._fee_bps(v, mode)
                + self._latency_bps(v)
                + self._leak_bps(v))

    # ---- 主决策 ----

    def route(self, snap: MarketSnapshot, side: str, qty: int) -> List[SORDecision]:
        """单切片路由:贪心 + 暗池优先尝试。"""
        bb, ba = snap.nbbo()
        ref_mid = 0.5 * (bb + ba)
        decisions: List[SORDecision] = []
        remaining = qty

        # 1) 暗池阶段:每个达到 MES 的池子分配 min(qty, MES)
        dark_venues = [v for v in snap.venues.values() if v.is_dark]
        for v in sorted(dark_venues, key=lambda x: x.toxicity):
            if remaining <= 0:
                break
            send = max(v.mes, min(remaining, v.mes * 2))
            if send > remaining:
                continue
            cost = self._venue_cost(v, side, send, 'dark', ref_mid)
            if cost < 0:  # 节省价差超过费率 + 毒性
                decisions.append(SORDecision(v.name, send, 'dark', cost))
                remaining -= send

        # 2) Lit 阶段:逐场所按 cost 升序贪心
        lit_venues = [v for v in snap.venues.values() if not v.is_dark]
        lit_costs = []
        for v in lit_venues:
            for mode in ('post', 'ioc'):
                c = self._venue_cost(v, side, remaining, mode, ref_mid)
                lit_costs.append((c, v, mode))
        lit_costs.sort(key=lambda x: x[0])

        for c, v, mode in lit_costs:
            if remaining <= 0:
                break
            # 取该场所盘口可成交量为容量(ioc)或 BBO 队列剩余空间(post)
            if mode == 'ioc':
                _, cap = v.sweep_cost(side, remaining)
            else:
                cap = sum(sz for _, sz in (v.asks[:1] if side == 'buy' else v.bids[:1]))
                cap = max(cap, remaining)   # post 单总能挂上去
            send = min(remaining, cap)
            if send <= 0:
                continue
            decisions.append(SORDecision(v.name, send, mode, c))
            remaining -= send

        return decisions

这份决策器是贪心 + 阶段化的,不是全局最优,但工程上简单稳定、可在 100 µs 量级跑完。生产级 SOR 会把这个贪心替换成真正的 QP 求解器,并把毒性、π_v 用 EWMA 在线更新。

八·三 最小可跑的端到端示例

def demo():
    snap = MarketSnapshot(venues={
        'NYSE': VenueBook('NYSE',
            bids=[(99.95, 5000), (99.94, 8000), (99.93, 12000), (99.92, 20000), (99.91, 30000)],
            asks=[(100.05, 5000), (100.06, 8000), (100.07, 12000), (100.08, 20000), (100.09, 30000)],
            fee_taker=0.0003, fee_maker=-0.00025, latency_us=30),
        'Nasdaq': VenueBook('Nasdaq',
            bids=[(99.95, 4000), (99.94, 7000), (99.93, 10000), (99.92, 15000), (99.91, 25000)],
            asks=[(100.05, 4000), (100.06, 7000), (100.07, 10000), (100.08, 15000), (100.09, 25000)],
            fee_taker=0.00025, fee_maker=-0.0002, latency_us=50),
        'EDGA': VenueBook('EDGA',  # inverted
            bids=[(99.95, 2000), (99.94, 3000), (99.93, 5000), (99.92, 8000), (99.91, 15000)],
            asks=[(100.05, 2000), (100.06, 3000), (100.07, 5000), (100.08, 8000), (100.09, 15000)],
            fee_taker=-0.0001, fee_maker=0.00015, latency_us=80),
        'SigmaX': VenueBook('SigmaX',
            bids=[], asks=[],
            fee_taker=0.0001, fee_maker=0.0,
            latency_us=1000, is_dark=True, mes=2000, toxicity=3.0),
        'Liquidnet': VenueBook('Liquidnet',
            bids=[], asks=[],
            fee_taker=0.00015, fee_maker=0.0,
            latency_us=2000, is_dark=True, mes=10000, toxicity=0.5),
    })
    router = SORRouter()
    plan = router.route(snap, side='buy', qty=30000)
    for d in plan:
        print(f"{d.venue:>10}  qty={d.qty:>6}  mode={d.mode:<5}  cost={d.expected_cost_bps:+.2f} bps")

if __name__ == '__main__':
    demo()

跑一下大约能看到:Liquidnet(低毒性 + 大 MES)拿走一部分,剩余在 NYSE / Nasdaq 上按 cost 排序分掉。EDGA 因为 inverted(taker rebate)在小尺寸下可能跑赢主交易所。

八·四 CCXT-based 加密 SOR 骨架

# crypto_sor.py — 基于 CCXT 的多 CEX 加密 SOR 骨架
import ccxt
import asyncio
from dataclasses import dataclass
from typing import Dict, List

@dataclass
class CryptoVenueState:
    exchange: ccxt.Exchange
    symbol: str
    bid: float = 0.0
    ask: float = 0.0
    bid_size: float = 0.0
    ask_size: float = 0.0
    fee_taker: float = 0.001     # 默认 10 bps
    balance_quote: float = 0.0
    rate_limit_remain: int = 100

async def refresh(state: CryptoVenueState):
    """异步拉一次 ticker + balance。"""
    ob = await state.exchange.fetch_order_book(state.symbol, limit=5)
    if ob['bids']: state.bid, state.bid_size = ob['bids'][0]
    if ob['asks']: state.ask, state.ask_size = ob['asks'][0]
    bal = await state.exchange.fetch_balance()
    quote = state.symbol.split('/')[1]
    state.balance_quote = bal.get(quote, {}).get('free', 0.0)

class CryptoSOR:
    def __init__(self, venues: Dict[str, CryptoVenueState]):
        self.venues = venues

    def _est_cost_bps(self, v: CryptoVenueState, side: str, qty: float) -> float:
        if side == 'buy':
            px = v.ask
            available = v.ask_size
        else:
            px = v.bid
            available = v.bid_size
        if available < qty * 0.1:        # 流动性显著不足
            return 1e6
        # 简单滑点:超过盘口顶档时线性放大
        slip_bps = 0.0 if qty <= available else 5 * (qty / available - 1)
        fee_bps = v.fee_taker * 1e4
        return slip_bps + fee_bps

    def plan(self, side: str, qty: float) -> List[Dict]:
        nbbo = (
            max((v.bid for v in self.venues.values()), default=0.0),
            min((v.ask for v in self.venues.values()), default=float('inf')),
        )
        ranked = sorted(
            self.venues.items(),
            key=lambda kv: self._est_cost_bps(kv[1], side, qty / max(1, len(self.venues))),
        )
        remaining = qty
        plan = []
        for name, v in ranked:
            if remaining <= 0:
                break
            cap = (v.ask_size if side == 'buy' else v.bid_size)
            # 余额 / 限流约束
            if side == 'buy':
                max_by_balance = v.balance_quote / max(v.ask, 1e-9)
                cap = min(cap, max_by_balance)
            cap = min(cap, v.rate_limit_remain * 1.0)   # 简化:每单 1 quota
            send = min(remaining, cap)
            if send <= 0:
                continue
            plan.append({
                'venue': name,
                'qty': send,
                'side': side,
                'price_hint': v.ask if side == 'buy' else v.bid,
                'cost_bps': self._est_cost_bps(v, side, send),
            })
            remaining -= send
        if remaining > 0:
            plan.append({'venue': '__shortfall__', 'qty': remaining, 'side': side})
        return plan

async def run_demo():
    venues = {
        'binance': CryptoVenueState(
            exchange=ccxt.binance({'enableRateLimit': True}),
            symbol='BTC/USDT',
            fee_taker=0.0010, balance_quote=200_000),
        'okx': CryptoVenueState(
            exchange=ccxt.okx({'enableRateLimit': True}),
            symbol='BTC/USDT',
            fee_taker=0.0008, balance_quote=150_000),
        'kraken': CryptoVenueState(
            exchange=ccxt.kraken({'enableRateLimit': True}),
            symbol='BTC/USDT',
            fee_taker=0.0016, balance_quote=80_000),
    }
    # 实测时 await asyncio.gather(*[refresh(v) for v in venues.values()])
    sor = CryptoSOR(venues)
    print(sor.plan(side='buy', qty=2.5))

骨架的几个关键点:

八·五 连接管理与 Failover

工程上比算法更难做对的是「场所连接挂了怎么办」。最小要求:

  1. 每场所独立 heartbeat:每 200–500 ms 一次 ping,连续丢 N 次切到 degraded 状态;
  2. Degraded 状态语义:不再向该场所发新单,但已挂未成交单仍跟踪;超过 T 秒未恢复则发取消(如果取消通道也挂,记入待重对);
  3. Failover 路由route() 输入「可用 venue 子集」,被剔除的场所占用的残量自动重分配;
  4. 状态聚合的强一致:本地 OMS 维护一份「该订单在每场所的 state machine」(NEW / PARTIAL / FILLED / CANCELLED / UNKNOWN),UNKNOWN 状态需要走人工或自动 reconciliation;
  5. 回放日志:每一次 SOR 决策的输入快照、输出分配、回报到达时间都要落盘,作为 best-ex 报告与事故复盘的证据。

八·六 图示

下面两张图分别给出 SOR 决策的端到端流程与跨场所撮合优先级矩阵:

SOR 决策流程

上图展示父单从切片到最终分配到多个场所的完整链路:候选场所集合经实时市场状态填入成本模型,五项成本(spread / impact / fee-rebate / latency / info-leak)合并得到 cost(v),再由分配优化器在容量与最小成交量约束下求解,最后把 child orders 派发到 lit / dark / reserve 多个目的地,回报反馈循环到下一片切片。

跨场所撮合优先级矩阵

上图把六类典型场所(lit-primary / lit-MTF / inverted / dark-mid / dark-block / internalizer)在 14 个维度上的相对优势直观呈现。读法是:没有一个场所在所有维度上都最优,SOR 的工作就是按当前 slice 的尺寸、紧迫度、价差、对手方画像,实时挑选适配的维度组合。例如「大额、不急、可等」更适合 dark-block 的 LIS 通道;「小额、急、价差稳定」更适合 lit-primary 直 IOC;「中额、想拿 rebate、不怕信息暴露」走 lit maker post。

八·七 测试与回放

SOR 比执行算法更依赖回放测试,因为它的决策过程嵌入大量场所状态。最小可用回放系统应当能:

回放最大的工程难点是重建当时的队列位置——除非交易所提供 add/modify/cancel 顺序号,否则只能近似。CME 的 MBO(market by order)流提供完整顺序号,BATS PITCH 提供 add 编号,许多场所只有 BBO 更新流,不支持精确回放,只能用蒙特卡洛近似。

八·八 最佳执行报告与 TCA 衔接

SOR 的所有决策日志最终汇入 TCA(transaction cost analysis)系统。TCA 在每一笔母单平仓后输出:

这套报告反过来喂回 SOR 的参数调优:哪个池子近 30 天毒性升高?哪个交易所的 maker 成交率下降?把 TCA 的实测分布定期写回 θ_vπ_vη_vL_v 的估计,是 SOR 能持续保持对手方质量感知的唯一机制。MiFID II RTS 28 报告所要求的「前五大执行场所」披露,也直接来自这层数据。

八·九 常见反模式

最后列几个工程实战里反复踩到的坑:

  1. 静态成本表。把 fee_vL_vη_v 写在配置文件里几个月不更新,结果实际费率早改了、对手方画像早变了;
  2. 决策不带快照。SOR 决策时只记录输出(路由分配),不记录输入(当时的 NBBO、各场所盘口、毒性估计)。事故复盘时无法回放;
  3. 暗池 IOI 当成实时盘口用。IOI 是「意向」不是「确定可成交」,把 IOI 当成 firm quote 路由会大量失败成交;
  4. 把 reserve 的隐藏量当成显示量算冲击。隐藏部分不显示、不冲击,但成交概率低,模型要分开估;
  5. 跨场所时序不对齐。多场所行情用各自时间戳合并出来的 NBBO 是错的,必须用本地接收时间戳重对齐;
  6. failover 不演练。生产 SOR 必须每月做一次「断 X 场所」的混沌测试,验证残量自动重路由的正确性;
  7. TCA 与 SOR 用不同 reference price。TCA 用 arrival mid,SOR 用 trade mid,两份报告的成本核算会对不上,导致 PM 与 trader 之间扯皮;
  8. 加密 SOR 忽略提币时间。把链上跨所迁移当成「即时」处理,结果实际几十分钟才到账,期间币价已经动了。

八·十 组件接口与依赖关系

把上面所有子节合起来,一个能跑的 SOR 系统的组件依赖关系大致是:

┌─────────────┐    parent order     ┌──────────────────┐
│ Strategy /  │ ───────────────────▶│ Execution Algo   │
│ PM EMS      │                     │ (VWAP/IS/AC...)  │
└─────────────┘                     └──────┬───────────┘
                                          │ slice q_t
                                          ▼
┌─────────────┐    L2 stream        ┌──────────────────┐
│ Market Data │ ───────────────────▶│ SOR Router       │
│ Aggregator  │                     │ (cost model +    │
└─────────────┘    NBBO / queue     │  optimizer)      │
                                    └──────┬───────────┘
                                          │ child orders
                                          ▼
┌─────────────┐    venue conn       ┌──────────────────┐
│ Venue Conn  │◀────────────────────│ OMS / Order Mgr  │
│ Layer (FIX) │                     │ (state machine)  │
└──────┬──────┘                     └──────┬───────────┘
       │ fills                            ▲
       └──────────────────────────────────┘
                                    ┌──────────────────┐
                                    │ TCA / best-ex    │
                                    │ Reporting        │
                                    └──────────────────┘

每一层应当在独立进程或独立线程,通过低延迟 IPC(共享内存环或 ZMQ inproc)通信,而不是把全部逻辑塞进单进程的同步调用——出问题时单组件挂掉不会拖死全栈。状态持久化用 append-only log(kdb+、ClickHouse、自家 binary log 都可),每条决策附带 wall-clock 时间戳与单调递增的 SOR 决策 ID。这份日志同时是 best-ex 报告的来源,也是事故复盘的依据。

八·十一 实测调优清单

最后给一份 SOR 上线后头三个月的调优 checklist,按优先级降序:

  1. slice 尺寸分布:母单切到 1k / 10k / 100k 股的成本曲线长什么样?是否存在「微小切片导致 fee 占比过高」的反模式?
  2. lit 与 dark 配比:dark 实际成交占比是不是和初始假设一致?是否过度依赖某一两个池子?
  3. maker 命中率分场所:每个 lit 场所上 post 单的实际命中率与队列模型预测的偏差有多大?偏差是否随时段变化?
  4. mark-out 校准:每个池子的实测 mark-out 与 SOR 模型里的 θ_v 偏差有多大?是否需要用 EWMA 替换静态值?
  5. failover 演练记录:上线 90 天里至少发生过多少次场所断连?SOR 的自动重路由有没有偏离预期?
  6. TCA 报告闭环:TCA 给出的「该用 venue X 而不是 Y」的反例有多少?这些反例的特征是否被 SOR 模型捕捉?
  7. 合规日志覆盖率:所有路由决策是否都附完整快照?被审计时能否在 5 分钟内复盘任意一笔?

把这份清单作为月度运营会议的固定议程,是让 SOR 从「能跑」走向「跑好」的唯一办法。


八·十二 与上一篇执行算法的接口

最后再点一下与上一篇的接口边界。执行算法(VWAP、TWAP、IS、AC)在每一个时间步 t 决定 q_t(这一片要发多少);SOR 在 q_t 给定后决定如何分配。两者的状态都包含「累计已成交量、剩余残量、当前市价」,但更新节奏不一样:执行算法在秒级或分钟级运行,SOR 在毫秒级运行。

接口契约可以写得很简单:

class ExecutionAlgo:
    def step(self, t: float, market: MarketSnapshot,
             filled_so_far: int) -> int:
        """返回这个时间窗口要发多少股 (slice qty)。"""
        ...

class SORRouter:
    def route(self, snap: MarketSnapshot,
              side: str, qty: int) -> List[SORDecision]:
        """把 slice 分配到多个场所。"""
        ...

# 主循环
algo = AlmgrenChrissAlgo(parent_qty=100_000, T=3600, ...)
sor = SORRouter()
filled = 0
while filled < parent_qty:
    snap = market_data.snapshot()
    slice_qty = algo.step(now(), snap, filled)
    if slice_qty <= 0:
        time.sleep(0.5); continue
    plan = sor.route(snap, side='buy', qty=slice_qty)
    for d in plan:
        oms.send(d.venue, d.qty, d.mode)
    filled += oms.collect_fills(timeout=1.0)

这套契约把执行与路由解耦,使两者可以独立替换、独立测试、独立回放。这也是大多数生产系统的实际形态。


九、收束

SOR 把「市场碎片化」「监管最佳执行」「对手方博弈」「连接工程」四件原本属于不同系的事情压在了同一个组件里。本文从 Reg NMS 与 MiFID II 出发,把碎片化解释成 SOR 的存在前提;用一个五项分解的成本函数把目标函数写实;把 lit market 的 maker / taker / reserve 选择拆到队列模型一层;把暗池的 MES、对手方筛选、毒性度量当成抑制反向选择的具体工具;把 pinging 与反 pinging 的对抗写成一种均衡;把 latency arbitrage 从对手方视角倒过来看一次;把加密 SOR(CEX 间路由 + DEX 聚合器 + CEX-DEX 混合)的工程差异点列清楚;最后给出一个可跑的多场所订单簿模拟、贪心 SOR 决策器、CCXT 加密 SOR 骨架、连接管理与 failover 的最小要求。

下一篇会接着把视角再翻一面:做市商(market maker)视角的报价、库存管理、reservation price、对抗反向选择的 Avellaneda-Stoikov 框架——做市商既是 SOR 决策的对手方,也用同一套微观结构语言定义自己的目标函数。

最后留三条工程性的实操建议作为本篇收尾:

  1. 不要在第一天就追求最优 SOR。从「等权分配 + 简单 IOC fallback」起步,把日志、监控、回放、TCA 闭环跑顺,再逐步把成本函数复杂化。过早引入复杂优化只会让 bug 隐藏更深。
  2. 每一项 cost 的权重都来自实测。不要照搬学术论文的 η、α、λ、γ 取值。用自家成交数据回归出来的参数才反映你机构的对手方画像与流量规模。
  3. best-ex 报告是 SOR 最重要的输出之一,不是附属品。监管与客户审计会反复问「为什么这单去了那儿」,能不能在 5 分钟内复盘出完整决策链路,决定一个 SOR 项目能不能在合规层站住脚。

风险再次提示:SOR 的所有决策都直接落到资金成本上。本文给出的 Python 代码与公式是教学用最小骨架,未涵盖高频场景下的内存模型、锁竞争、重连风暴等工业问题;MES、毒性、费率等参数的具体数值随场所、品种、时间持续变化,取用前必须以自家实测数据校准。任何把本文骨架直接放进实盘路径的尝试,都应当先在 paper trading 与 staged rollout 下完整跑过至少一个完整 trading cycle,并通过独立合规审阅。


参考文献

  1. U.S. Securities and Exchange Commission (2005). Regulation NMS, Final Rule. Release No. 34-51808. https://www.sec.gov/rules/final/34-51808.pdf

  2. European Parliament and Council (2014). Directive 2014/65/EU on Markets in Financial Instruments (MiFID II). Official Journal of the European Union, L 173/349.

  3. European Parliament and Council (2014). Regulation (EU) No 600/2014 (MiFIR). Official Journal of the European Union, L 173/84.

  4. ESMA (2017). RTS 27 and RTS 28 on Best Execution Reporting.

  5. Foucault, T., Pagano, M., & Röell, A. (2013). Market Liquidity: Theory, Evidence, and Policy. Oxford University Press.(市场结构与碎片化的标准教科书)

  6. Harris, L. (2003). Trading and Exchanges: Market Microstructure for Practitioners. Oxford University Press.

  7. O’Hara, M. (2015). “High Frequency Market Microstructure.” Journal of Financial Economics, 116(2), 257-270.

  8. Easley, D., López de Prado, M., & O’Hara, M. (2012). “Flow Toxicity and Liquidity in a High-Frequency World.” Review of Financial Studies, 25(5), 1457-1493.(VPIN 与毒性度量)

  9. Almgren, R., & Chriss, N. (2000). “Optimal Execution of Portfolio Transactions.” Journal of Risk, 3(2), 5-39.

  10. Kissell, R. (2013). The Science of Algorithmic Trading and Portfolio Management. Academic Press.(执行算法与 SOR 的工程视角)

  11. Mittal, H. (2008). “Are You Playing in a Toxic Dark Pool? A Guide to Preventing Information Leakage.” Journal of Trading, 3(3), 20-33.

  12. Buti, S., Rindi, B., & Werner, I. M. (2017). “Dark Pool Trading Strategies, Market Quality and Welfare.” Journal of Financial Economics, 124(2), 244-265.

  13. Zhu, H. (2014). “Do Dark Pools Harm Price Discovery?” Review of Financial Studies, 27(3), 747-789.

  14. Comerton-Forde, C., & Putniņš, T. J. (2015). “Dark Trading and Price Discovery.” Journal of Financial Economics, 118(1), 70-92.

  15. Lewis, M. (2014). Flash Boys: A Wall Street Revolt. W. W. Norton.(latency arbitrage 与 IEX 的通俗背景)

  16. Aquilina, M., Budish, E., & O’Neill, P. (2022). “Quantifying the High-Frequency Trading ‘Arms Race’.” Quarterly Journal of Economics, 137(1), 493-564.

  17. Budish, E., Cramton, P., & Shim, J. (2015). “The High-Frequency Trading Arms Race: Frequent Batch Auctions as a Market Design Response.” Quarterly Journal of Economics, 130(4), 1547-1621.

  18. Foucault, T., Kozhan, R., & Tham, W. W. (2017). “Toxic Arbitrage.” Review of Financial Studies, 30(4), 1053-1094.

  19. Adams, H., Zinsmeister, N., & Robinson, D. (2020). Uniswap V2 Core. Uniswap white paper.

  20. Adams, H., et al. (2021). Uniswap V3 Core.

  21. Daian, P., et al. (2020). “Flash Boys 2.0: Frontrunning in Decentralized Exchanges, Miner Extractable Value, and Consensus Instability.” IEEE Symposium on Security and Privacy.

  22. Qin, K., Zhou, L., & Gervais, A. (2022). “Quantifying Blockchain Extractable Value: How dark is the forest?” IEEE Symposium on Security and Privacy.

  23. CCXT Authors (2024). CCXT — CryptoCurrency eXchange Trading Library. https://github.com/ccxt/ccxt

  24. 1inch Network (2023). Pathfinder Algorithm: Multi-Path Routing. https://docs.1inch.io/

  25. CoW Protocol (2023). CoW Swap and Batch Auctions Documentation. https://docs.cow.fi/

  26. Flashbots (2023). MEV-Boost and Private Mempool Documentation. https://docs.flashbots.net/

  27. IEX Group (2016). IEX Exchange Rule Book and the Speed Bump.

  28. Cboe Global Markets (2023). U.S. Equities Fee Schedule and Order Type Specification.

  29. Nasdaq (2023). Nasdaq Stock Market Rules: Order Types and Modifiers.

  30. FINRA (2024). Regulatory Notice 21-12: Best Execution and Payment for Order Flow.

  31. CME Group (2023). MBO (Market By Order) Data Specification. 提供完整 add/modify/cancel 顺序号,是回放级 SOR 测试的基础数据形态。

  32. Nasdaq (2023). TotalView-ITCH 5.0 Specification. Nasdaq 的盘口完整事件流,对队列位置精确建模必须。

  33. Cboe Global Markets (2023). PITCH Multicast Feed Specification. BATS / Cboe 的等价产品,支持 add 编号回溯。

  34. Hasbrouck, J. (2007). Empirical Market Microstructure. Oxford University Press.(学术回放与队列建模的方法论)

  35. Cont, R., Stoikov, S., & Talreja, R. (2010). “A Stochastic Model for Order Book Dynamics.” Operations Research, 58(3), 549-563.(队列模型的连续时间形式化)

  36. Maglaras, C., Moallemi, C. C., & Zheng, H. (2015). “Optimal Order Routing in a Fragmented Market.” Mathematical Finance, 25(2), 282-322.


系列导航

同主题继续阅读

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

2026-05-01 · quant

【量化交易】市场结构:交易所、做市商、暗池、ECN

系统梳理全球市场结构(Market Structure)的工程图景:从证券交易所、衍生品交易所、加密交易所,到做市商、暗池、ECN/ATS,再到 Maker-Taker 收费、PFOF、Reg NMS 与 MiFID II 的监管影响;给出量化策略选择交易场所的判断框架与基于 ccxt 的多交易所行情聚合代码。

2026-05-01 · quant

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

量化交易不是策略写得好就能赚钱,更难的是把数据、特征、因子、信号、组合、执行、风控、复盘这八段链路在工程上连成一条不漏数据、不串时间、不丢订单的流水线。本文是【量化交易】系列的总目录与读图,给出八段链路的输入输出、失败模式、不变量清单,并用研究流程图把从一个想法到一笔实盘订单之间所有该过的卡点串起来。

2026-05-01 · quant

【量化交易】市场微结构:订单簿、价差、流动性、冲击

系统讲解市场微结构的核心概念与可计算工具:限价订单簿的数据模型、报价/有效/已实现价差、Roll 模型、四维流动性度量、Kyle's lambda、订单流不平衡(OFI)、Almgren-Chriss 框架下的临时与永久冲击、PIN 与 VPIN、Hawkes 过程,并给出基于 polars 的 L2 增量处理与系数估计代码。

2026-05-01 · quant

【量化交易】订单类型与执行语义:限价、市价、IOC、FOK、冰山

把 Limit、Market、IOC、FOK、Iceberg、Stop、MOO/MOC 这些常被混为一谈的订单类型还原为价格、数量、时效、可见性、触发五个独立维度,并对照 A 股、港股、美股、CME、Binance 五个市场的实际语义差异,给出量化系统中的订单工厂、状态机与风控前置校验的工程实现。


By .