某私募在 CFFEX 张江 COLO 机房的交易负责人 09:45 巡视交易室,向策略组长问一个问题:"如果我们做沪深300 ETF 510300.SH 的 mean-reversion-on-mid 机器人因为 mid 算错而开始按 0.01 元发买单,它怎么在监管层来电话之前自己停下?"答案不在策略本身,而在策略外面的框架。每张订单到达 FIX 会话之前必走的六道风控门、交易室运维一键扳下的 kill switch、把 FIX 会话状态扛在身上让策略对线路协议保持无状态的订单路由器、以及精确到纳秒告诉你预算花在哪条组件上的测量探针。L3 把它建起来。
事件驱动架构模式
生产级策略是一个对外暴露四个方法供框架调用的类:OnTick(MarketDataEvent, OrderBook) 处理行情、OnFill(ExecutionReport) 处理(部分或全部)成交、OnTimer(TimerId) 处理周期定时器、OnRiskBreach(RiskBreach) 处理风控触发。框架的事件循环坐在 L2 那条去重 SPSC 队列上,每条事件分派一个方法。策略从不直接接触线路;它请求路由器发单,路由器请求风控审核,风控通过后由路由器把 FIX 报文写到线路。这就是 LMAX Disruptor 在小尺度上的形态——固定流水线、单写单读队列、编译期多态策略、热路径上零堆分配。
这个分层并不是为了"工程美学",而是要让三件事可独立替换:策略本身可以由量化研究员频繁迭代而不必动框架;风控参数可以由风控官在不重新编译策略的前提下调整限值;订单路由器可以单独升级到新的 FIX 版本或换到新的撮合场所,而策略对此完全无感。一个常见的反例就是把 FIX 序列化直接写在策略里——表面上短期省事,长期会让你每次升级协议都被迫回归测试每一个策略,且策略代码混入大量与 alpha 完全无关的会话状态机逻辑,渐渐失去可读性。L3 的目的正是把这条边界画清楚,并让它在性能上不收任何代价。
CRTP 与事件并集
用 CRTP(Curiously Recurring Template Pattern,奇异递归模板)实现编译期多态。virtual 调用在缓存命中下每次 3–7 ns 代价、还会拖累内联器;CRTP 零开销——调用通过 static_cast 内联,优化器可以跨方法边界折叠。事件并集用 std::variant<MarketDataEvent, ExecutionReport, TimerEvent, RiskBreach>(3.4.2 L4 的 vocabulary type);std::visit 按当前活跃变体在编译期分派。
#include <variant>
#include <cstdint>
struct ExecutionReport {
std::uint64_t cl_ord_id;
std::uint64_t order_id;
std::int32_t fill_price_ticks;
std::uint32_t fill_qty;
std::uint8_t side; // 0 = buy, 1 = sell
std::uint8_t ord_status; // FIX OrdStatus: 0 = new, 1 = partial, 2 = filled, 4 = cancelled, 8 = rejected
};
struct TimerEvent { std::uint32_t timer_id; };
struct RiskBreach { std::uint8_t gate_id; std::uint64_t cl_ord_id; };
using FrameworkEvent = std::variant<MarketDataEvent, ExecutionReport, TimerEvent, RiskBreach>;
// CRTP base; Derived provides OnTick / OnFill / OnTimer / OnRiskBreach.
template<class Derived>
class Strategy {
public:
// Public entry points the framework calls.
void on_tick (const MarketDataEvent& ev, const OrderBook& book) {
static_cast<Derived*>(this)->OnTick(ev, book);
}
void on_fill (const ExecutionReport& fill) {
static_cast<Derived*>(this)->OnFill(fill);
}
void on_timer(const TimerEvent& ev) {
static_cast<Derived*>(this)->OnTimer(ev);
}
void on_risk (const RiskBreach& ev) {
static_cast<Derived*>(this)->OnRiskBreach(ev);
}
};
四个入口故意用大驼峰 OnTick / OnFill / OnTimer / OnRiskBreach 写在派生类上;框架调用小写下划线的 on_* 转发函数,转发函数通过 static_cast<Derived*>(this) 跳到派生类。日后要加第五种事件类型:一个新 struct、一个新 variant 备选、一对新入口——不要 virtual override,不要运行期分派。
演示用租户:基于 mid 的均值回归
把框架端到端跑通,给一个对 510300.SH 的最小均值回归策略当试验对象。信号:最新成交价低于 mid 超过 5 个基点时,以 mid 减一个 tick 发买;卖侧对称。策略自己维护仓位计数(成交时更新),单策略仓位上限保护即止。没有 alpha,只是让路由和风控路径都跑起来。
class MeanRevMidStrategy : public Strategy<MeanRevMidStrategy> {
public:
explicit MeanRevMidStrategy(OrderRouter& router, std::int32_t threshold_bps = 5,
std::uint32_t lot_size = 100, std::int64_t max_position = 10000)
: router_(router), threshold_bps_(threshold_bps),
lot_size_(lot_size), max_position_(max_position) {}
void OnTick(const MarketDataEvent& ev, const OrderBook& book) {
if (ev.type != EventType::Executed) return; // only react to trades
const auto bid = book.best_bid_ticks();
const auto ask = book.best_ask_ticks();
if (bid <= 0 || ask <= 0) return; // no two-sided market yet
const auto mid = (bid + ask) / 2;
const auto last = ev.price_ticks;
const auto threshold_ticks = (mid * threshold_bps_) / 10000; // bps -> tick units
if (last + threshold_ticks < mid && position_ < max_position_) {
// Last trade is below mid by > threshold: mean-reversion BUY.
router_.send_limit(Side::Buy, mid - 1, lot_size_);
} else if (last - threshold_ticks > mid && position_ > -max_position_) {
// Last trade is above mid by > threshold: mean-reversion SELL.
router_.send_limit(Side::Sell, mid + 1, lot_size_);
}
}
void OnFill(const ExecutionReport& fill) {
position_ += (fill.side == 0 ? +fill.fill_qty : -fill.fill_qty);
}
void OnTimer(const TimerEvent&) noexcept {} // not used in this demo
void OnRiskBreach(const RiskBreach&) noexcept {} // logged at framework level
private:
OrderRouter& router_;
std::int32_t threshold_bps_;
std::uint32_t lot_size_;
std::int64_t max_position_;
std::int64_t position_ = 0;
};
按 510300.SH 在 mid 4.20 元、tick 0.001 元计算,5 bp 阈值是 21 个 tick(4.20 * 0.0005 / 0.001)。一次 4.198 元成交比 mid 低不到 5 bp——策略不动;一次 4.197 元成交超阈值——策略以 4.199 元(紧贴内部一个 tick)发买。lot 100 股,仓位上限 10000 股。
六道风控门
策略发来的每一张订单请求都先经过 RiskManager::check,才由路由器序列化 FIX。六道按序检查,kill-switch 排第一,让它可以在一个 cycle 内停掉一切。
| gate ID | 名称 | 检查项 | 典型限值 | 触发原因 |
|---|---|---|---|---|
| 1 | gross-notional 毛名义 | 跨所有标的 ` | position_i | * price_i` 求和 |
| 2 | net-delta 净 delta | signed_position * delta 求和 | 50000 股等价 | 策略过多或过空 |
| 3 | per-symbol position 单标的仓位 | 任一标的 ` | position | ` |
| 4 | fat-finger 价格带 | 订单价相对内部价偏离 | 500 bp(5%) | 策略 bug 或手抖,价格偏离市场过多 |
| 5 | order-rate 下单频率 | 每策略每秒下单数 | 1000/s | 策略失控发单过快(同时会触发交易所硬性限制) |
| 6 | kill-switch | std::atomic<bool> 标志;运维台切换 | 即时停 | 闪崩、突发新闻、严重异常等灾难场景 |
class RiskManager {
public:
struct OrderRequest {
std::uint64_t cl_ord_id;
Side side;
std::int32_t price_ticks;
std::uint32_t qty;
std::uint64_t symbol_id;
};
// Returns true if the order passes all six gates; sets out_breach to the failing gate ID.
bool check(const OrderRequest& req, const OrderBook& book, std::uint8_t& out_breach) noexcept {
if (killed_.load(std::memory_order_relaxed)) { out_breach = 6; return false; } // kill-switch
// 1) Gross-notional limit: |position| * price over all symbols.
const std::int64_t order_notional_usd_cents = static_cast<std::int64_t>(req.qty) * req.price_ticks;
if (gross_notional_cents_ + order_notional_usd_cents > kGrossNotionalLimitCents) {
out_breach = 1; return false;
}
// 2) Net-delta limit (delta = 1 for equity; options L3 stubs this to 1).
const std::int64_t signed_qty = (req.side == Side::Buy ? +1 : -1) * static_cast<std::int64_t>(req.qty);
if (std::abs(net_delta_ + signed_qty) > kNetDeltaLimit) { out_breach = 2; return false; }
// 3) Per-symbol position limit.
const auto symbol_pos = positions_[req.symbol_id];
if (std::abs(symbol_pos + signed_qty) > kPerSymbolLimit) { out_breach = 3; return false; }
// 4) Fat-finger price-band check: order price within X% of inside.
const auto inside = (req.side == Side::Buy ? book.best_ask_ticks() : book.best_bid_ticks());
if (inside > 0 && std::abs(req.price_ticks - inside) * 10000 > inside * kPriceBandBps) {
out_breach = 4; return false;
}
// 5) Order-rate limit: M orders per second per strategy.
const auto now = clock_ticks();
if (now - rate_window_start_ < 1'000'000'000LL) { // within 1 second
if (++rate_count_ > kOrderRatePerSec) { out_breach = 5; return false; }
} else {
rate_window_start_ = now;
rate_count_ = 1;
}
return true;
}
void on_fill(const ExecutionReport& fill) noexcept {
const std::int64_t signed_qty = (fill.side == 0 ? +1 : -1) * static_cast<std::int64_t>(fill.fill_qty);
positions_[fill.order_id /* symbol_id key in production */] += signed_qty;
net_delta_ += signed_qty;
gross_notional_cents_ += std::abs(signed_qty) * fill.fill_price_ticks;
}
void trip_kill_switch() noexcept { killed_.store(true, std::memory_order_release); }
private:
static constexpr std::int64_t kGrossNotionalLimitCents = 100'000'000'00LL; // 100M USD-cents
static constexpr std::int64_t kNetDeltaLimit = 50'000;
static constexpr std::int64_t kPerSymbolLimit = 10'000;
static constexpr std::int64_t kPriceBandBps = 500; // 5%
static constexpr std::uint32_t kOrderRatePerSec = 1000;
std::atomic<bool> killed_{false};
std::int64_t gross_notional_cents_ = 0;
std::int64_t net_delta_ = 0;
std::unordered_map<std::uint64_t, std::int64_t> positions_;
std::int64_t rate_window_start_ = 0;
std::uint32_t rate_count_ = 0;
static std::int64_t clock_ticks() noexcept; // implemented via clock_gettime(CLOCK_MONOTONIC)
};
Formula Explorer
trip_probability = 1 - (1 - p_breach)^N_orderskill switch 是承重的一环。交易室运维看到异常——闪崩、不合理成交——在控制台敲 KILL,控制台调用 risk.trip_kill_switch()。memory_order_release 写入与热路径上的 relaxed 读取配对;下一张订单请求触发第六道门并返回 false。一个微秒内停掉全公司交易,这是第六道门买给你的,并且是任何交易机构在合规层面必须具备的一道控制。
订单路由器
路由器拥有 cl_ord_id 序号、FIX 会话状态机(此处简化为 printf)、以及和风控的整合。策略调用 router.send_limit(side, price, qty),永远不必接触线路。
class OrderRouter {
public:
OrderRouter(RiskManager& risk, OrderBook& book) : risk_(risk), book_(book) {}
bool send_limit(Side side, std::int32_t price_ticks, std::uint32_t qty) {
RiskManager::OrderRequest req{
.cl_ord_id = next_cl_ord_id_++,
.side = side,
.price_ticks = price_ticks,
.qty = qty,
.symbol_id = active_symbol_,
};
std::uint8_t breach = 0;
if (!risk_.check(req, book_, breach)) {
// Risk breach: emit a RiskBreach event for the strategy and refuse to send.
std::printf("RISK_BREACH gate=%u cl_ord_id=%llu\n", breach, (unsigned long long)req.cl_ord_id);
return false;
}
// Serialise NEWORDERSINGLE (FIX 4.4 |35=D|) — for L3 we print the canonical fields.
std::printf("NEWORDERSINGLE 35=D 11=%llu 55=%llu 54=%u 40=2 38=%u 44=%d\n",
(unsigned long long)req.cl_ord_id,
(unsigned long long)req.symbol_id,
static_cast<unsigned>(side == Side::Buy ? 1 : 2), // FIX Side: 1=Buy, 2=Sell
req.qty,
static_cast<int>(req.price_ticks));
return true;
}
private:
RiskManager& risk_;
OrderBook& book_;
std::uint64_t next_cl_ord_id_ = 1;
std::uint64_t active_symbol_ = 0; // set per-strategy in production
};
生产实现里 FIX 序列化器把二进制或 SOH 分隔的缓冲写到自开盘以来一直保持的 TCP 会话上;L3 这里只是把 FIX 4.4 NEWORDERSINGLE 的关键字段(35=D 消息类型、11=cl_ord_id、55=symbol、54=side、40=2 limit ord_type、38=qty、44=price)打印出来。路由器的存在感是它定义的契约:策略永不接触 FIX、线路会话重传循环永不接触策略、父子订单管理活在路由器的地址空间。
路由器还承担两件在私募生产里关键的职责。第一是父子订单切片:交易员希望买入 50000 股 510300.SH,但策略层只看一个目标仓位,由路由器按 TWAP / VWAP / POV 等执行算法切成数百张子订单依次发出;策略要做的只是更新 target_position,路由器负责把那 50000 股按节奏喂进市场而不影响价格。第二是 FIX 会话状态:会话登录、心跳、序号协商、链路断开重连,所有这些线程级别的协议机制都在路由器内部,策略代码完全不感知。这种分工让你在切换执行经纪商或者接入新的 SSE / SZSE 路由通道时,只动路由器,不动任何策略代码。
带 rdtsc 测量的捕获事件循环
把整个流水线接起来。循环弹出一条 FrameworkEvent、用 __rdtsc() 圈住每条组件、通过 std::visit 分派、累积按组件的 cycle 数。运行结束后离线统计这些向量的中位数 / P99 / P99.9 / 最大值。
#include <x86intrin.h> // __rdtsc on GCC / Clang
// Cycle-counter to nanoseconds. Caller passes the TSC frequency (typically 2.0-3.5 GHz).
static inline std::int64_t ns_from_cycles(std::uint64_t cycles, double ghz) {
return static_cast<std::int64_t>(cycles / ghz);
}
void event_loop(SpscQueue<FrameworkEvent>& in,
MeanRevMidStrategy& strat,
OrderBook& book,
std::atomic<bool>& running,
double tsc_ghz) {
// Per-component latency accumulators (median/P99 computed offline).
std::vector<std::int64_t> ns_dispatch, ns_book_apply, ns_signal, ns_risk_emit;
ns_dispatch.reserve(1'000'000);
ns_book_apply.reserve(1'000'000);
ns_signal.reserve(1'000'000);
FrameworkEvent ev_var;
while (running.load(std::memory_order_relaxed)) {
const std::uint64_t t0 = __rdtsc();
if (!in.try_pop(ev_var)) continue;
const std::uint64_t t1 = __rdtsc();
std::visit([&](const auto& ev) {
using T = std::decay_t<decltype(ev)>;
if constexpr (std::is_same_v<T, MarketDataEvent>) {
book.apply(ev);
const std::uint64_t t2 = __rdtsc();
strat.on_tick(ev, book);
const std::uint64_t t3 = __rdtsc();
ns_book_apply.push_back(ns_from_cycles(t2 - t1, tsc_ghz));
ns_signal .push_back(ns_from_cycles(t3 - t2, tsc_ghz));
} else if constexpr (std::is_same_v<T, ExecutionReport>) {
strat.on_fill(ev);
} else if constexpr (std::is_same_v<T, TimerEvent>) {
strat.on_timer(ev);
} else if constexpr (std::is_same_v<T, RiskBreach>) {
strat.on_risk(ev);
}
}, ev_var);
ns_dispatch.push_back(ns_from_cycles(t1 - t0, tsc_ghz));
}
// Offline: sort the vectors, report median / P99 / P99.9 / max for each component.
}
__rdtsc() 在 x86 上读取 TSC 大约 2–3 ns。std::visit 内部的 if constexpr 链就是分派零开销的所在——编译器按 variant 备选在编译期决定走哪条分支。生产框架在 Skylake 之前的 Intel 上会在 timestamp 两侧加 _mm_mfence() 防重排;Skylake 之后 __rdtsc 的有序性已足够用于百分位桶。
值得停下来谈一谈为什么要把测量点切得这么细。如果你只有一个端到端的延迟数字,看到 P99.9 突然从 800 ns 升到 4 µs 时,你完全没法定位是行情解析变慢了、是 book.apply 命中冷缓存、还是策略 OnTick 里某条新加的检查触发了远端内存访问。把每条组件分桶记录,问题域就缩小到一条具体函数,私募交易桌的延迟回归调查从一晚上压缩到十几分钟。同样的 __rdtsc 模式在 3.4.3 L1 已经见过,这里只是把它系统化到事件循环的每一帧。
衔接下一课
至此你拥有了一套通过 CRTP 零开销分派的策略框架、六道风控门 + 一 cycle 即停的 kill switch 保护私募整体、负责 FIX 会话因而让策略对线路无状态的订单路由器,以及精确到纳秒告诉你预算花在哪条组件的测量探针。最后一课把这份组装好的二进制从开发桌搬到 COLO 服务器:编译标志(-O3 -march=native -flto、PGO)、运行期替换(LD_PRELOAD=libjemalloc.so、isolcpus=2-7)、让研究组在 Python 里直接调用你的 pricer 还不必复制数组的 pybind11 绑定,以及把整条 tick-to-trade 用硬件 NIC 时间戳闭环测量的方法。
练习
Exercise
(a)用 std::unordered_map<std::uint64_t, std::int64_t>(键为 symbol_id)实现 RiskManager 第 3 道单标的仓位限制门。on_fill 时更新该 symbol 的仓位;check 时查询该 symbol 当前仓位、按最坏情形(订单完全成交)计算成交后仓位、对比 kPerSymbolLimit。写单元测试:以同价发出 10000 张 1 股 BUY,验证第 10001 张被 gate ID 3 拒绝。
(b)用 1 秒滑动窗口实现 RiskManager 第 5 道下单频率门:rate_window_start_ 用 clock_gettime(CLOCK_MONOTONIC) 取得的纳秒单调时间记录当前窗口起点;每次 check 调用:若 now - rate_window_start_ >= 1'000'000'000LL,重置窗口;否则 ++rate_count_。rate_count_ > kOrderRatePerSec(1000)即返回 false 并 breach gate ID 5。写单元测试:发 999 张、sleep 1 ms、再发 999 张,全部通过;随后紧凑循环发 1001 张,验证第 1001 张被 gate ID 5 拒绝。
(c)把 kill-switch(RiskManager 第 6 道门)接成 RiskManager::check 中的第一道检查。增加一条运维控制台桩线程:从 stdin 读,用户敲 KILL 时调用 risk.trip_kill_switch()。写测试:循环发 100 张订单、每张间隔 10 ms,第 50 张之后敲 KILL,验证后续所有订单都被 gate ID 6 拒绝。
(d)在 CFFEX 张江 COLO Intel Xeon Gold 6342 上把 L3 capstone 钉在隔离核(taskset -c 6 ./l3_capstone)跑;采集 100 万条事件计时;离线对每条组件的延迟向量排序;报告 ns_dispatch、ns_book_apply、ns_signal 的中位数 / P99 / P99.9 / max。预期值:dispatch 中位 10-30 ns,book_apply 中位 20-50 ns,signal 中位 50-150 ns(均值回归很便宜;复杂策略可能花到 µs)。一句话说明为何对这项度量 P99.9 比均值在运营上更有意义。
(e)(无须实现)对下列场景各指出最可能首先触发的那道门(1-6 中之一):(i)策略 bug 把 mid 算成 0、以 0 元发 BUY(即 100% 低于市场);(ii)策略陷死循环,100 ms 内发 10000 张订单;(iii)某未充分对冲的策略对 510300.SH 持仓 200000 股,单标的限值为 10000 股;(iv)闪崩场景下策略正确识别买入时机但运维要求在市场完整性核实前停止全部交易。各用一句话给出理由。
提示
symbol_pos + signed_qty。测试时按同价发 10001 张相同 lot,断言最后一张 out_breach == 3。