智能订单路由(SOR)¶
SOR(Smart Order Routing)指将一张母单(Parent Order)按规则拆分到多个执行场所(Venue)——如各交易所、暗池、流动性提供商(LP)——在约束条件下尽量优化:
- 成交价格(相对基准的滑点 / 实施缺口)
- 成交数量(吃单与挂单的完成度)
- 风险与合规(额度、最小量、延迟不确定性)
母单通常拆成多条子单(Child Orders),各 venue 独立撮合或报价,再汇总为母单的执行结果。
实务中常把母单生命周期粗分为两阶段(可循环、可与其他策略组合):
| 阶段 | 目标 | 买方典型规则 | 卖方典型规则 |
|---|---|---|---|
| Aggressive(主动/吃单) | 尽快利用可见流动性成交 | 按有效买成本从低到高,依次扫 ask 档位 | 按有效卖收入从高到低,依次扫 bid 档位 |
| Passive(被动/挂单) | 未成交部分在簿上排队 | 按历史被动成交率、做市返佣等权重分配挂单量 | 同理,按权重在各 venue 挂 bid |
两阶段不是唯一范式:还有纯 TWAP/VWAP、纯 opportunistic 等;SOR 常与调度器结合(何时 aggressive、何时 passive、是否撤单再挂)。
以下约定:正费率表示对己方不利(成本);具体系统需与清算/合约条款统一符号。
Taker(吃单)
- 买方:在卖方最优价 ask 上买入,名义成本上升。 有效买价(用于排序:越小越好):
$ P^{\mathrm{buy,eff}}{\mathrm{taker}} = P \right) $}} \left( 1 + \frac{f_{\mathrm{taker}}}{10^4
- 其中 $ f_{\mathrm{taker}} $ 为 taker 费率(单位:bps)。若坚持用变量名
taker_fee_bps:
$ P^{\mathrm{buy,eff}}{\mathrm{taker}} = P \right) $}} \left( 1 + \frac{\text{taker_fee_bps}}{10000
- 卖方:在买方最优价 bid 上卖出,名义收入下降。 有效卖价(用于排序:越大越好):
$ P^{\mathrm{sell,eff}}{\mathrm{taker}} = P \right) $}} \left( 1 - \frac{f_{\mathrm{taker}}}{10^4
- 或:
$ P^{\mathrm{sell,eff}}{\mathrm{taker}} = P \right) $}} \left( 1 - \frac{\text{taker_fee_bps}}{10000
等价实现:ask * (1 + fee) / bid * (1 - fee),其中 fee = taker_fee_bps / 10000。
Maker(挂单)
- 若挂单方获得返佣(rebate),可视为负成本或单独进入权重(见下文 Passive)。
- Passive 阶段常用综合权重(比例关系):
$ w_i \propto \max(0,\,\rho_i) \cdot \left( 1 + \frac{r_i}{10000} \right) $
- 其中 $ \rho_i $ = passive_fill_rate,$ r_i $ = maker_rebate_bps。返佣越高、历史被动成交越好,越愿意在该 venue 多挂量。
暗池 / LP
可能没有公开 top of book,用指示性报价或历史成交价+滑点模型估计有效价格,再纳入同一套排序或过滤(例如仅当估计有效价优于某阈值时才路由)。
输入数据结构
Aggressive:各 venue 可见最优卖盘(买方吃单)
#include <algorithm>
#include <cstdint>
#include <string>
#include <vector>
struct VenueTopBuy {
std::string name;
double ask_px;
std::int64_t ask_qty;
double taker_fee_bps; // 按名义值的 bps,正为成本
};
inline double effective_buy_cost(double ask_px, double fee_bps) {
return ask_px * (1.0 + fee_bps / 10000.0);
}
卖方对称结构:bid_px、bid_qty,用 effective_sell_proceeds(bid_px, fee_bps)。
Passive:各 venue 挂单统计
struct VenuePassiveStat {
std::string name;
double passive_fill_rate; // [0,1],历史或模型估计
double maker_rebate_bps; // 返佣越高越倾向多挂
};
Aggressive 路由(买方扫单)
逻辑:按 effective_buy_cost 升序遍历 venue,对每家取
$ \min(\text{剩余需求量}, \text{该档 ask_qty}, \text{该 venue 额度上限}) $,直到需求为 0 或无可吃流动性。
第 $ i $ 家 venue 上吃单量(无 lot 约束时):
$ q_i = \min!\left( R_{i-1},\; Q^{\mathrm{ask}}i,\; L_i \right), \qquad R_i = R - q_i $
其中 $ R_0 $ 为剩余需求量,$ Q^{\mathrm{ask}}_i $ 为该所可见卖量,$ L_i $ 为该所额度上限(若无上限则省略 $ L_i $)。
inline std::vector<std::pair<std::string, std::int64_t>> aggressive_route_buy(
std::vector<VenueTopBuy> venues, std::int64_t remaining_qty) {
std::sort(venues.begin(), venues.end(),
[](const VenueTopBuy& a, const VenueTopBuy& b) {
return effective_buy_cost(a.ask_px, a.taker_fee_bps) <
effective_buy_cost(b.ask_px, b.taker_fee_bps);
});
std::vector<std::pair<std::string, std::int64_t>> slices;
for (auto& v : venues) {
if (remaining_qty <= 0) break;
std::int64_t take = std::min(remaining_qty, v.ask_qty);
if (take > 0) {
slices.emplace_back(v.name, take);
remaining_qty -= take;
}
}
return slices;
}
扩展:每 venue 最大可买量 venue_limit、最小下单单位 lot_size 时,应对 take 做 floor(take, lot) 并处理碎股规则。
Passive 分配(按权重拆剩余量)
权重
$ w_i = \max(0,\,\rho_i) \cdot \left( 1 + \frac{r_i}{10000} \right) $
其中 $ \rho_i $ = passive_fill_rate,$ r_i $ = maker_rebate_bps(无返佣则为 0)。
整数分配与 dust(推荐:最大余数法)
简单按 floor(remaining * w_i/sum) 会产生未分配完或分配偏差。更稳妥做法是 Largest Remainder(最大余数)。
理想份额与向下取整:
$ S = \sum_j w_j,\qquad q^_i = Q \cdot \frac{w_i}{S},\qquad q_i = \left\lfloor q^_i \right\rfloor $
(若需 lot,再对 $ q_i $ 向下取整到最小单位。)
剩余 dust:
$ D = Q - \sum_i q_i $
将 $ D $ 个单位按理想份额减去向下取整份额的差值(即小数部分)从大到小排序,依次给排名靠前的 venue 各分配 1 个单位(或 1 个最小下单单位 lot),直到 $ D $ 个单位全部分完。
$ \text{排序键: } \delta_i = q^*_i - q_i \quad (\text{降序}) $
这样 dust 按权重余数归属,而不是误用「已分配量最大」的 venue(后者在权重高但 floor 为 0 时会错)。
伪代码思路:
sum_w = sum(w)
for i: ideal[i] = remaining * w[i] / sum_w
for i: alloc[i] = floor(ideal[i]) // 再按 lot 调整
dust = remaining - sum(alloc)
按 (ideal[i] - alloc[i]) 降序,给前 dust 个 venue 各 +1
若某 venue 不允许 passive,令 $ w_i=0 $。
约束与跨所要点
| 维度 | 说明 |
|---|---|
| 延迟 | 并行下单时,先成交的 venue 会改变剩余量;需 预测到达时间、限并发 或 串行关键路径,避免超配。 |
| 资金与额度 | 每所余额、持仓/敞口上限、单日限额;Aggressive/Passive 都要 min(..., limit)。 |
| 精度 | 最小下单量、价格 tick;分配后需 向下取整到 lot,dust 按规则收敛到单一 venue 或丢弃。 |
| 合规与路由表 | 部分标的或客户仅允许特定 venue;先做 白名单过滤 再排序。 |
| 部分成交与撤单 | 子单状态机:New → PartiallyFilled → 是否撤余量 → 是否回到 Aggressive。 |
执行质量指标(回测与实盘)
VWAP Slippage(相对市场 VWAP)
$ \text{VWAP slippage (bps)} = \frac{P_{\text{your avg}} - P_{\text{mkt VWAP}}}{P_{\text{mkt VWAP}}} \times 10000 $
- 买方:平均成交价 低于 市场 VWAP → 指标为 负 → 通常视为更好。
Implementation Shortfall(IS,相对到达价 mid)
到达价 $ m $ 常用母单到达时的 mid(或 first touch)。
$ \mathrm{IS\,(bps)} = s \cdot \frac{P_{\mathrm{fill\,avg}} - m}{m} \times 10000, \qquad s = \begin{cases} +1 & \text{买入} \ -1 & \text{卖出} \end{cases} $
- 买方:成交价低于 mid → IS 为负 → 更好。
inline double is_bps(bool is_buy, double arrival_mid, double filled_avg_px,
std::int64_t filled_qty) {
if (filled_qty <= 0 || arrival_mid <= 0) return 0.0;
double sign = is_buy ? 1.0 : -1.0;
return sign * (filled_avg_px - arrival_mid) / arrival_mid * 10000.0;
}
Fill Rate
$ \mathrm{fill\,rate} = \frac{Q_{\mathrm{filled}}}{Q_{\mathrm{requested}}} $
requested == 0 时定义为 0 或 N/A,避免除零。条件写法:
$ \mathrm{fill\,rate} = \begin{cases} Q_{\mathrm{filled}} / Q_{\mathrm{requested}} & Q_{\mathrm{requested}} > 0 \ 0 \;\text{或 N/A} & Q_{\mathrm{requested}} = 0 \end{cases} $
其他(可选)
- Participation rate(相对区间成交量)
- Realized spread / 有效价差
- 各 venue 贡献度(成交量占比、价格贡献)
小结
- SOR = 在 多 venue 上,在 费率、额度、最小量、延迟 等约束下,优化 价格与成交量。
- Aggressive:按 有效 taker 价格 排序扫可见流动性(买低卖高在有效价意义下)。
- Passive:按 被动成交能力 × 返佣(等)权重 拆挂单;整数与 dust 建议用 最大余数法 或按权重最大项收尾。
- 监控:VWAP slippage、IS、fill rate;跨所需统一处理 延迟与精度。
- 费率符号与交易所规则以实际合约为准。