我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

C++多类型订单簿的设计:从状态机到撮合引擎的实践指南

C++多类型订单簿的设计:从状态机到撮合引擎的实践指南 做一个交易类项目或行情回放工具时很多人会先写一个“能跑”的订单簿订单进来按价格聚合然后按价格从最优到次优排队。我当时也这样做的。处理限价单足够可一旦系统里出现市价单、止损单、冰山单原来的按价格聚合逻辑就会变得非常别扭。问题不是C写订单簿难而是订单本身是多变的而订单簿的骨架却往往是按一种最理想化的限价单模型设计的。于是我重新用 C 构建了一个多类型订单簿并在重构过程中意识到真正的难点不是“支持多少种订单类型”而是如何让订单生命周期、价格队列和撮合规则都围绕同一个稳定的抽象来工作。这篇文章我不打算贴一个完整的高性能撮合引擎源码那是几千行甚至上万行的事我想先把构建这类系统时最容易困惑的部分拆开讲清数据结构选型、多类型订单的建模、撮合顺序、并发演进和排查路径。如果你正在写自己的回测系统、模拟撮合引擎或者只是想把订单簿原理搞清楚这篇内容应该对你有用。1. 多类型订单簿到底在解决什么问题先回到基础问题。订单簿的职责是什么通常说法是管理订单、维护双向挂单深度、撮合买卖订单。但真正的订单簿要处理的不是静态快照而是连续到达的订单流。每个订单从进入系统到最终被完全成交、撤销或失效中间会经历多个状态。只用一个 struct 保存“价格、数量、方向、类型”很难支撑不同订单类型的差异。1.1 一个订单真正会经历哪些状态变化一个普通限价买单的路径相对简单进入订单簿检查是否有卖单可以成交能成交则成交剩余数量挂入买单队列。不能成交则直接挂单等待后续卖单到达。但订单类型一旦多起来状态一下子就复杂了。我先列几个常见动作市价单没有价格它进入系统后必须以对手盘价格尽快成交止损单通常不主动成交而是在市场价格触及某个触发价后转换成市价单或限价单冰山单只显示一部分数量当显示量被吃完后需要从隐藏量中释放下一批显示量IOC立即成交否则撤销单要求不能成交的部分立刻撤销FOK全部成交否则撤销单要求要么全部数量一次成交要么整单撤销。因此多类型订单簿必须在订单对象里记录当前状态而不仅是订单类型。一个可用的状态设计至少要包含状态含义主要动作新订单刚进入系统校验并准备进入撮合流程待撮合已进入撮合逻辑按价格/顺序参与撮合已挂单全部或部分数量未成交进入订单队列等待对手方订单已部分成交一部分数量已成交剩余仍排队或等触发继续参与交易或保留已触发止损/止盈单到达触发条件转换为市价或限价单已完成全部数量成交从活动订单集合中移除已撤销被用户取消或超时失效从订单队列中移除在设计订单簿时不要把这些状态散落在各种条件分支里而要尽量让订单状态和行为组合起来。比如“止损单”不应只是一个 bool 字段它意味着订单进入系统后先不进入普通排队而是等待市场价格达到某个触发价然后转换为另一种行为。1.2 多类型订单簿的“多类型”体现在哪里很多人以为多类型订单簿就是支持 LIMIT、MARKET、STOP 这样的枚举值然后 switch 一下。这是一种极其容易把代码写崩的做法。多类型真正的差异点有三个订单进入方式不同限价单可以立刻进入价格队列止损单需要挂到一个独立的“触发集合”里市价单则直接发起吃单。订单成交规则不同限价单只与指定价位或更优价格成交市价单按对手盘价格逐档成交冰山单前段按普通限价单处理但显示量消耗后还有隐藏量补充FOK 单的成交量必须全有或全无。订单生命周期不同普通限价单直到被成交或撤销止损单从“等待触发”变为“已触发”后才是真正的可成交订单GTC、GTD、IOC、FOK 这些时间策略决定了订单在未完全成交时的处置方式。所以“多类型”不是简单多几个枚举而是“订单在生命周期各阶段呈现出不同的数据访问方式和交易行为”。1.3 主判断核心不是类型枚举而是变形与生命周期这是我想强调的一个主线判断用 C 构建多类型订单簿时最容易出现的问题不是性能而是把“订单类型”做成一组散落的 if/else最后导致每个函数都要考虑“如果它是止损单怎么办”“如果它是冰山单怎么办”。方向应该是把“类型”和“行为”解耦。订单进入系统后它先是一个带状态的交易意图系统根据类型和当前状态决定它下一步的行为。这样新增一个订单类型时不需要改动撮合主流程只需要添加新的状态转换规则和交易行为策略。换句话说订单簿的核心框架应该像一台状态机 事件驱动的交易引擎而不是一个纯数据表。后面我会深入这一步。2. 先搭骨架订单、价格层级和队列关系在讨论复杂的多类型之前先把最基础的骨架搭好。无论最终支持多少订单类型都需要底层的数据组织方式订单对象、价格序列、委托队列。2.1 订单对象要存哪些字段哪些字段会被撮合流程修改订单对象不要只存交易所需字段还要考虑生命周期管理和日志追踪。一个面向多类型订单扩展的订单对象至少要包含enum class OrderType : uint8_t { Limit, Market, Stop, StopLimit, Iceberg, IOC, FOK }; enum class OrderSide : uint8_t { Buy, Sell }; enum class OrderStatus : uint8_t { New, PendingTrigger, Working, PartiallyFilled, Filled, Cancelled, Rejected }; struct Order { int64_t order_id; int64_t user_id; OrderSide side; OrderType type; OrderStatus status; int64_t price; // 限价价格市价单这里可能为0或空 int64_t quantity; // 原始数量 int64_t filled_quantity 0; int64_t remaining_quantity() const { return quantity - filled_quantity; } // 用于止损/限价止损 int64_t trigger_price 0; // 冰山单用 int64_t display_quantity 0; int64_t hidden_quantity 0; // 时间策略 int64_t create_time_ns; int64_t expire_time_ns 0; // 可供 GTx 使用 bool is_immediate_or_cancel false; bool is_fill_or_kill false; // 运行时公共字段 int64_t seq 0; int64_t last_update_time_ns 0; };这不是唯一写法但它有助于说明多类型订单并不是靠子类化每个订单类型来处理的而是用一组通用字段描述其属性。止损单额外有trigger_price冰山单需要hidden_quantity。C 里可以用继承或组合但我更推荐在早期先用一个扁平结构 行为策略因为订单对象的生命周期管理可以从同一个存储池中分配代码也更直观。修改最频繁的字段是status、filled_quantity、hidden_quantity。撮合时更新这些字段必须保证只有一个线程在写入或者有明确的多线程访问约束。2.2 价格序列容器选型从 std::map 到更精细的层级管理订单簿最核心的组织是按价格维护两个方向的订单队列。买方向需要快速找到最高买价买一卖方向需要快速找到最低卖价卖一。常见选型std::mapint64_t, OrderQueue, std::greater维护买方std::mapint64_t, OrderQueue, std::less维护卖方同时还可以缓存卖一和买一迭代器减少每次begin()的调用开销。std::map是平衡树查找、插入、删除都是 O(log n)。对于大部分模拟盘、研究和中小型交易系统这个复杂度完全够用。真正需要自己写跳表或红黑树的地方通常是每秒要处理几万到几十万笔订单并且对延迟要求极高时。价格队列使用 FIFO 结构因为同价位订单需要按时间优先成交。队列里的元素可以是订单指针也可以是订单 ID。存储指针便于快速修改订单状态和引用消息系统。需要小心的点是队列中可能包含已被部分成交但尚未完全移除的订单移除订单时要准确在对应队列中找到并 erase。2.3 最小可运行框架添加限价单与按价格聚合先从一个只支持普通限价单的版本入手。这里我给出一个方向性示例不是完整生产撮合代码class OrderBook { public: void add_order(Order* order); private: struct OrderQueue { std::dequeOrder* orders; int64_t total_quantity 0; }; std::mapint64_t, OrderQueue, std::greaterint64_t bids_; // 买价从高到低 std::mapint64_t, OrderQueue, std::lessint64_t asks_; // 卖价从低到高 };买入限价单的挂单过程可以简化为void OrderBook::add_limit_buy(Order* order) { int64_t price order-price; if (asks_.empty() || price asks_.begin()-first) { // 没有可成交对手价直接挂入买队列 auto queue bids_[price]; queue.orders.push_back(order); queue.total_quantity order-remaining_quantity(); order-status OrderStatus::Working; return; } // 否则进入撮合逻辑后面补充 }这个步骤看起来简单但从这里开始你会意识到单子类型增加后不能继续用add_limit_buy、add_market_buy、add_stop_order这样的独立函数各自处理因为后面的市价单、止损单、冰山单都要复用同一个“检查对手盘、轮询队列、成交剩余数量”的撮合循环。2.4 先跑通再谈多线程和性能这里最容易犯的错是一上来就设计 lock-free、内存池、CPU 缓存优化。如果订单簿的业务规则还没有完全跑通优化只是给错误代码穿上性能外衣。我建议的开发顺序是用单线程实现所有订单类型的撮合逻辑加入完整的状态流转和日志用大量的历史行情回放验证撮合结果与预期一致分析热点再决定是否引入并发和更复杂的数据结构。这一步的核心目的不是得到最快代码而是获得一个“逻辑正确”的参考实现。后续做性能优化和并发改造时可以用它作为基准进行回归测试。3. 多类型订单的扩展方式策略、工厂与状态机订单簿一旦接多类型订单代码结构很容易从“多个 if”滑向“无法维护”。关键是你用什么样的代码组织方式应对“多类型”这个复杂度。3.1 为什么简单的继承可能成为负担一种自然想法是像这样定义一个基类 Order然后派生 LimitOrder、MarketOrder、StopOrder、IcebergOrder。但这样往往带来几个问题OrderBook 容器里只能放基类指针实际访问价格、触发价、隐藏量时要向下转型每种派生订单在撮合流程中如果行为差异大很容易出现大量 dynamic_cast 或类型判断订单从“未触发”到“已触发”对象类型难道要变化吗止损单触发后变成市价单你是在原对象上改类型还是创建一个新对象替换掉原来的用继承表达“订单类型”看起来面向对象实际上是把交易系统的多变行为硬塞进了类继承体系里。对于一个交易引擎我更愿意把订单当成一组数据把行为拆到策略组件中。3.2 用订单类型枚举 行为策略拆分而不是把每种类型写死在一个类里一个很实用的做法是定义一个订单创建接口根据类型和参数填充Order然后在核心撮合阶段使用一组职责清晰的策略函数预处理策略决定订单是否进入“立即撮合”通道订单队列放置策略决定这只订单在“触发前”放在哪个集合价格获取策略市价单取对手盘最优价限价单取指定价格部分成交后的剩余单处理策略IOC 撤单、FOK 撤全部、GTC 继续排队、冰山单释放下一层显示量触发策略止损单只有在行情价格穿过触发价时才生效。这些策略可以用OrderActionProcessor形式组织class OrderProcessor { public: void process(OrderBookContext ctx, Order* order); };内部根据order-type和order-status选择对应的执行步骤。不让每个订单类型写一个process_foo_order而是把整个撮合过程抽象成几个阶段每个阶段内部的差异用小型函数处理。这样做的好处是测试时可以针对某个状态和策略单独验证新增一种订单类型时你只需要扩展一两个阶段而不是把add_order、match_order、cancel_order全改一遍。3.3 市价单、止损单、冰山单的插入/触发/撤单差异下面我具体拆拆分三种典型差异便于你理解状态机之后落在代码里是什么样子。市价单市价单进入系统后没有价格约束直接扫描对手盘深度从对侧asks_或bids_的最优价格开始逐层吃掉可用数量直到全部成交或对手盘耗尽可能还有剩余。如果剩余数量没有特殊时间策略按 IOC 处理会更好否则一个没有对手盘的市价单通常会被拒绝或当作“部分成交剩余取消”因为市价单不应该挂单等待。止损单当价格触发时止损单会变成市价单或限价单。因此在订单簿中止损单通常不放在普通价格队列里而放在待触发集合里。你需要在行情 tick 更新时检查当前市价是否达到了订单的触发条件。常见规则是卖出止损单在价格下跌到小于等于触发价时触发买入止损单在价格上涨到大于等于触发价时触发。实际操作中交易所对“达到触发价”的定义存在差异需要先用边界值测试。触发后订单的行为模式从“被动等待”切换到“立即触发”这一步可以在订单状态中记录为PendingTrigger - Triggered然后再进入市价或限价逻辑。冰山单冰山单的复杂性在显示量和隐藏量的管理。订单进入买队列时只向对手盘暴露一部分显示数量。当显示数量被全部吃掉时系统需要从隐藏量里补充下一个显示量而隐藏量不参与主动市场深度显示但总数量要确保不会超过原始数量。用一个简化的伪代码描述冰山单剩余处理void releaseNextDisplayIfNeeded(Order* iceberg) { if (!iceberg-hidden_quantity) return; int64_t next_display std::min(iceberg-hidden_quantity, iceberg-display_quantity); iceberg-hidden_quantity - next_display; // 把新的一段显示量加入原价格队列尾部或者继续在当前队列中等待 }这里要注意队尾更新和剩余数量的计算。如果冰山单当前显示量部分成交后需要继续留在价格队列中那么它不能从原队列中被移除后并重新插入到队头否则会破坏时间优先。3.4 让订单簿真正可扩展的关键接口抽象综合前面想法一个可供扩展的订单簿核心接口不应只暴露add_order。可以拆成几个层次class OrderBookEngine { public: // 输入命令 void InsertOrder(Order* order); void CancelOrder(int64_t order_id); void CancelReplace(int64_t old_order_id, Order* new_order); void OnMarketTick(int64_t price, int64_t bid, int64_t ask); // 查询 int64_t BestBid() const; int64_t BestAsk() const; size_t BidDepth() const; size_t AskDepth() const; };OnMarketTick是触发止损单和特殊条件单的入口它告诉订单簿“最新行情价格或成交价已改变”。你不希望在每次刷单时都遍历所有订单因此需要维护一个按触发价排序的止损单集合。常见的止损集合选择也可以是一个按价格和触发顺序排序的多重映射std::multimapint64_t, Order* stop_buy_orders_; // key 为触发价 std::multimapint64_t, Order* stop_sell_orders_;价格 tick 到来时可以按价格区间取出应该触发的订单逐个处理。把这个阶段集中在接口层面后后续添加 AT附加条件、追踪止损等都更容易接入。4. 撮合流程多类型叠加时如何维持公平性多类型订单簿的真正复杂度出现在撮合顺序上。不同类型的订单在同一时刻都希望对同一个对手盘深度生效如果规则不明确结果很可能与真实市场不一致。4.1 价格优先、时间优先的顺序如何定义常规限价单队列的顺序是价格优先同一价格下按进入队列先后。真正需要讨论的是市价单和止损单触发后的订单到底插入到哪个时间点一个直接入场且能成交的买单通常按对手方队列的价格顺序逐档成交在某一档价格如果存在多个可成交的买单如新进的限价单、止损后转成的市价单、冰山单显示量的旧委托谁先谁后通常交易所采用价格优先、时间优先但“时间”默认是同类型队列中的 order id 或 entry time。市价单通常不进入订单队列而是直接扫对手盘所以它相对于同价格限价单的先后取决于系统在撮合瞬间如何选择可用量。多数情况下可以用一个全局递增序号给所有进入撮合的订单打时间戳用来解决同一队列内部顺序以及触发单和被动单之间的公平性。更稳妥的做法是先用一个单调递增的seq值记录每个订单进入系统的时间无论订单是初始进入还是止损触发再次进入都分配新的 seq这里需要注意如果止损单触发后变成市价单再按触发顺序进入吃单流程通常需要重新记录触发时间而不是原始下单时间。另一种做法是保留原始 seq 但不一定公平因为不同订单的触发时间点不同。这个问题没有唯一答案关键是你在文档和测试中要明确自己采用哪种规则并持续保证一致性。常见模拟引擎的简化做法是新进入的限价单和转成的市价单都使用一个全局order_seq_同一价格档订单队列按 seq 升序排列一个订单只保留最早进入该队列的 seq如果它被移动或重新插入需要重新分配 seq或者再保留一个queue_insert_seq_。4.2 从限价单买入到吃单怎么触发止损、怎么消耗冰山一个多类型场景的完整撮合步骤可以用下面这个顺序理清新买单到达首先判断是否为当前限价买单读取它的请求价格。如果请求价格低于当前卖一且没有交叉则该单进入买队列交易结束。如果有交叉则进入撮合循环。在循环中获取当前卖一队列的第一笔委托如果是普通卖单则按可成交数量匹配如果是冰山卖单能成交的数量必须是当前显示量不能直接使用隐藏量如果匹配的订单是 FOK 的对手方需要判断剩余数量能否满足 FOK 的全部数量若不能满足则整笔无法成交。撮合成功后更新双方订单的filled_quantity和status。若对手方剩余数量为 0则从队列中移除若远大于当前成交对手方原地更新或移除。如果买单剩余数量为 0结束如果剩余数量 0 且订单是 IOC则整单撤销剩余如果是 GTC则挂入买队列。如果买单是止损单触发后来到的先检查它的触发状态再走同一套撮合逻辑。整个过程听上去不复杂但一旦有多类型每一步都可能被插入条件分支。因此需要把“获取可成交对手方数量”这类动作封装成接口而不是在每个 elif 里重复实现。4.3 撮合中常见的隐蔽坑数量回退、价格交叉、触发顺序我列出几个容易忽略的坑这些是我实际看过和调过的问题。价格交叉问题编写撮合循环时必须严格确保买单价格 卖一价格才可以成交卖单价格 买一价格才可以成交。否则可能出现价格倒挂后一个限价单被成交在比自己限价更差的价格上。限价单的成交价必须处于订单价格与对手方价格之间而且永远不会超出自身限价。数量回退没有做某个 FOK 卖单想要一次性卖 100 手而当前买盘深度足够但其中有几笔是状态已经部分成交但尚未移除的残留单或者有订单刚好要被取消。如果撮合逻辑没有先计算可用的对手单数量总和而是一边匹配一边更新可能最终总量不足导致 FOK 被错误满足。正确的做法是对于 FOK 订单先快速计算对面可成交数量是否足够如果不满足直接拒绝且不能让其他订单产生部分成交。冰山单显示量被“一次性穿透”如果一个市价买单的剩余量很大而对面第一档有一个冰山卖单显示量只有 10 手但隐藏量为 1000 手。在真实市场规则中市价单只能吃掉冰山单的显示量 10 手如果 10 手还不够会继续吃同一价位的普通卖单或下一档卖单而不是直接吃到冰山单的隐藏量。隐藏量会在显示量被吃掉后像新的限价单一样重新暴露到该价格队列尾部。因此撮合循环要区分“对手方当前可用于立即成交的露出数量”和“该订单的总剩余数量”。止损单触发顺序多个止损单在同一价格 tick 满足条件时应该按什么顺序触发如果价格是从上往下穿系统通常需要先处理最接近刚刚触发线的那批单这个顺序会影响它们后续吃单的先后。建议用按触发价格和时间共同排序的待触发集合并添加 tick 级回放测试。4.4 日志与消息输出让撮合过程可审计撮合引擎中的日志不是可选项。我见过很多项目在刚开始没有日志一旦撮合结果不对只能到处打印临时变量效率极低。可以为每一个事件定义结构化日志至少包括收到订单类型和时间是否进入撮合循环逐档撮合的价格数量与对手订单 ID订单状态变化撤单或拒绝原因。一个简单的输出结构struct TradeReport { int64_t trade_id; int64_t taker_order_id; int64_t maker_order_id; int64_t price; int64_t quantity; int64_t trade_time_ns; };把所有成交和状态变化都记录成消息后续做行情回测和问题定位会轻松很多。甚至在验证订单簿正确性时可以用一个“参考订单簿”对比每笔撮合结果这比人眼盯日志更可靠。5. 单线程到并发无锁优化前的思考路径当订单逻辑正确后自然会想到性能。但多类型订单簿引入并发比普通限价订单簿更麻烦。因为撮合过程不是一个纯增删操作而是带有状态依赖和多个集合的一致性修改。5.1 不要一上来就无锁无锁队列、无锁哈希表在交易系统里被广泛讨论但如果你的多类型订单簿仍然大量使用std::map和 FIFO deque想改成无锁结构会非常痛苦。更常见的成熟架构是多个行情源或订单源通过命令队列发送请求只有一个撮合线程负责处理所有订单和行情 tick其他线程负责读取行情快照和成交结果快照读线程如果需要访问订单簿采用读写锁或双缓冲快照。这个设计下核心订单簿只在撮合线程中被修改读线程看到的是定期发布的不可变快照。这样你完全不需要对订单簿集合做无锁并发修改也大大降低了错误概率。5.2 分线程模型、跨线程命令、异步日志多类型订单簿的并发瓶颈通常不在订单簿本身而在 IO、日志、行情数据接入。合理的线程划分可以这样行情线程负责从 TCP/共享内存/API 读取行情数据把价格 tick 和交易所事件投递到订单簿线程的命令队列订单簿线程只负责处理订单和 tick更新订单簿状态风险/管理系统线程负责读取快照做下单决策日志线程异步写入统计和交易记录。命令队列可以使用一个有界 MPMC 队列或单生产者单消费者 SPSC 队列。多数模拟系统用互斥锁保护的std::deque就够了真正的核心不是队列性能而是每个处理步骤不要持有锁时发起网络调用。价格和数量的判断要放在撮合线程内不能假设外部行情已对齐到同一个时间点。5.3 内存池和对象复用减少分配抖动如果每秒处理几十万笔订单频繁 new/delete 会变成一个大开销尤其是订单对象和队列节点。常见的做法使用std::vectorOrder作为订单存储池订单 id 就是索引使用 free list 管理已撤销订单的空闲槽位对不同价格的订单队列节点可以自行实现一个轻量内存池避免每次插入队列都分配内存。但请注意内存池应该在逻辑和性能测试跑通后再引入。过早优化会掩盖很多语义问题。当你已经明确要压榨性能时可以用pmr或自研内存池替换默认分配器但要保留相同的接口和可测试性。这里展示一个简单的订单池对象复用思路class OrderPool { public: Order* allocate(int64_t order_id) { if (free_ids_.empty()) { orders_.emplace_back(); orders_.back().order_id order_id; return orders_.back(); } int64_t idx free_ids_.back(); free_ids_.pop_back(); Order* o orders_[idx]; o-reset(); o-order_id order_id; return o; } void deallocate(Order* order) { order-status OrderStatus::Cancelled; free_ids_.push_back(order-order_id); } private: std::vectorOrder orders_; std::vectorint64_t free_ids_; };简单写法的局限是订单 ID 和向量下标不一定一致生产环境通常需要更复杂的索引映射。但方向是对的减少动态内存分配让订单生命周期尽量可控。5.4 性能验证从哪里开始量化不要靠感觉优化。先建立压测脚本和目标单线程每秒钟能处理多少条订单插入在 10000 档深度、每档 50 笔订单的情况下单笔吃单延迟是多少多类型订单触发和撤单在总订单中的占比是多少内存占用和碎片率又有多少从我的经验看很多系统的延迟瓶颈其实出现在锁竞争和缓存未命中而不是算法复杂度。你可以先用 profiler 找到热点函数再用性能基线做对比。如果没有基线任何“优化后更快”的说法都没有意义。6. 落地建议与排查链路前面讲了设计思路和技术点最后给出一套能落地的开发路径、排查方式和边界判断。6.1 一个可以开始的最小开发路径如果你的目标是理解或验证订单簿不要直接从一个完整交易所的历史数据开始也不要从一个全是延迟指标的高性能引擎开始。我更建议这样推进写一个只支持限价单并且能正常撮合的订单簿用一条条手工测试确认买卖队列和成交顺序给订单对象加状态字段加入撤单和部分成交逻辑加入市价单加入阈值触发逻辑支持止损单加入数量管理和隐藏量支持冰山单加入 IOC/FOK 等时间策略用随机订单流回放测试对比某种基准规则下的事件数量级完成日志和状态机后再评估是否需要优化。每一步都保持系统可运行并且输出清晰的事件日志。路径图可以简化为限价单订单簿 → 状态与撤单 → 市价单 → 止损/条件单 → 冰山单 → 时间策略IOC/FOK → 回放与验证6.2 排查问题时的正确顺序当订单簿表现不对别急着改代码。我习惯按这个顺序排查先看现象是价格错误、数量错误、顺序错误还是程序崩溃再看输入订单时间戳、类型、价格、数量、触发价是否正确行情 tick 到达顺序是否与测试用例一致再看状态订单当前状态是否符合预期是否因为一次错误触发导致状态机发散再看撮合顺序搜索成交日志看逐档撮合时是否使用了正确的最佳买卖价和对手订单再看数据结构在std::map中增删价格层级时迭代器是否失效价格层级数量是否被正确更新。最后看并发问题是否有非撮合线程修改了订单对象或队列数据。如果使用的是异步命令队列还要检查该命令事件是否被重复投递或乱序。回放日志法是最有效的复现手段把输入事件和输出事件全部记录下来一个小样本能复现再加日志定位。6.3 什么情况不该自己造订单簿需要说明适用边界。如果只是做策略研究或教学自己造订单簿是很好的学习路径。但如果目标是实盘或需要极低延迟而你又没有足够的时间夯实调研、撮合规则和异常恢复那么使用成熟的交易所撮合引擎、已有的开源仿真系统或券商提供的 API往往是更稳妥的选择。自己造轮子适合的场景你需要深入理解订单簿机制你的策略需要对不同订单类型的成交规则做定制你在做学术论文或回测框架不想依赖外部订单簿接口你想训练 C 系统设计能力。不适合自己造的场景直接实盘且没有内部专门团队维护风险控制目标仅仅是把策略发到回测里没有特别复杂的交易所微观结构缺少有效撮合结果验证数据你需要面对极端参数和异常行情但没有足够的容灾设计。很多问题不是出在订单簿本身而是你能拿到的逐笔数据、成交规则、撤单规则不够完整。在撮合规则不确定时自己写的再快结果也可能带偏。6.4 长期演进从撮合引擎到回测与仿真多类型订单簿构建完成之后长期价值在于它可以作为回测系统的“真实市场模拟器”。这也是我最终选择自己实现它的原因之一外部平台的撮合规则类似一个黑盒而你的策略如果想在“部分成交后继续等待”“冰山单吃单深度”“止损触发瞬间的流动性冲击”等维度上有更加真实的表现就需要一个能由自己控制的撮合内核。回测系统需要输出每个订单进入系统后的状态变化和成交回报而我们上述设计的订单对象和管理日志已经覆盖了这一点。后续增加大量随机订单、不同订单类型的混合比例、模拟市场噪声都只需要接入事件流。多类型订单簿不是一次性项目它更像一个持续演进的系统组件。你的重点是让状态、规则、数据和策略保持清晰边界这样每次增加一种新类型或调整某条撮合规则时不会把整个系统推倒重来。从最初的“按价格聚合成字典”到后来的状态机、生命周期、策略化扩展这个重构过程给我最大的启发是在 C 中实现这类系统真正值钱的不是内存池和无锁队列而是对交易模型本身的抽象能力。把订单类型看作状态与行为的组合把所有规则收敛到有限的几个流程节点上订单簿的扩展和稳定性都会好很多。要做的代码不一定多但每一步都得想清楚“这个字段在哪里被修改、这个状态由谁触发、这个剩余量应该交给哪个队列”。先把这些问题回答清楚再谈高性能也不迟。
返回列表