简介这份资源面向区块链与数字资产交易领域的开发者提供币币交易所核心引擎 exchange-core 的完整源码与配套示例适合具备一定 Java 基础、希望深入理解撮合与风控机制的中高级工程师学习参考。压缩包共 952 个文件约 410.26MB以 252 个 Java 源文件与 247 个 class 字节码为主体辅以 339 个 html 文档、24 个 xml 配置及 ecj、ecs、eca 等工程文件另有少量 jar、js、css 与说明文档覆盖源码、编译产物与文档多个层面。内容围绕交易所核心模块展开涉及 ExchangeApi、RiskEngine、OrderBookDirectImpl、DiskSerializationProcessor 等关键组件可帮助读者梳理订单撮合、风险控制、序列化与延迟测试等实现思路并借助示例快速搭建本地验证环境。目前已有 296 人学习下载适合作为研究高性能交易系统架构与工程实践的参考素材。1. 拆开一个交易所撮合引擎exchange-core 源码和例子能跑出什么如果你正在找一个能本地跑起来、结构清晰、不是玩具级别的撮合引擎源码exchange-core 大概率已经在你的候选清单里了。它解决的核心问题很具体把「订单进来、撮合、成交、回报」这条链路用可读的 Java 代码完整实现出来而不是丢给你一堆概念图。适合谁想理解交易系统底层撮合逻辑的后端工程师、准备做量化回测或模拟盘的学生、以及需要一套可改造骨架来验证自己业务规则的人。我拿到这份源码和例子的第一反应是终于不用对着白皮书猜「价格优先、时间优先」到底怎么落到数据结构上了。它把订单簿、撮合、事件分发拆得比较开你能单独拎出某一层来读也能直接跑例子看成交结果。2. 撮合引擎的核心结构订单簿、事件与命令怎么串起来2.1 订单簿不是一张表而是两个有序结构很多人第一次看撮合代码会以为订单簿就是一个ListOrder来一单遍历一遍找对手价。exchange-core 的做法更接近真实系统买盘和卖盘各自维护按价格排序的结构同一价格下再按时间排队。这样做的直接好处是撮合时不需要全量扫描只要看最优买价和最优卖价是否交叉。常见做法是用TreeMap或跳表来维护价格档位每个价格档位下挂一个 FIFO 队列。源码里对订单的增删改都围绕这个结构走你读的时候重点看三个动作限价单进入时挂到哪一侧、市价单进入时怎么取对手方、撤单时怎么从队列中间摘掉。第三个动作最容易出问题因为队列中间删除在普通链表里是 O(n)所以真实实现通常会额外维护订单 ID 到节点的索引。// 简化示意价格档位 订单队列 TreeMapLong, ArrayDequeOrder bids new TreeMap(Comparator.reverseOrder()); TreeMapLong, ArrayDequeOrder asks new TreeMap(); void addLimitOrder(Order order) { // 买单进 bids卖单进 asks价格作为 key TreeMapLong, ArrayDequeOrder book order.side Side.BUY ? bids : asks; book.computeIfAbsent(order.price, p - new ArrayDeque()).addLast(order); }上面这段只是帮你建立直觉真实源码里还会处理订单类型、时间戳、剩余数量等字段。参数上你要留意价格是用long还是BigDecimal这决定了后续精度处理方式队列用ArrayDeque还是自定义节点决定了撤单效率。2.2 命令和事件分离是为了让撮合可回放exchange-core 里一个很值得学的设计是把「输入」和「输出」分成命令与事件两条线。命令是外部发进来的意图比如下单、撤单事件是撮合完成后系统产生的既成事实比如订单已成交、订单已取消。这样拆的好处是撮合逻辑只负责把命令变成事件事件可以被持久化、被回放、被多个下游消费。你读源码时会看到类似OrderCommand和OrderEvent的类。命令里带的是请求参数事件里带的是成交价格、成交量、剩余量、订单状态。撮合过程中一个命令可能产生多个事件比如一个大单吃掉多个价位就会生成多笔成交事件。这里的关键参数是订单 ID 和成交编号它们保证回放时能对上号。// 命令进入撮合产出事件列表 ListOrderEvent match(OrderCommand cmd) { ListOrderEvent events new ArrayList(); if (cmd.type CommandType.PLACE) { // 先尝试撮合再决定剩余部分是否挂单 events.addAll(tryMatch(cmd.order)); if (cmd.order.remaining 0 cmd.order.type OrderType.LIMIT) { addLimitOrder(cmd.order); events.add(OrderEvent.accepted(cmd.order)); } } else if (cmd.type CommandType.CANCEL) { removeOrder(cmd.orderId); events.add(OrderEvent.cancelled(cmd.orderId)); } return events; }逻辑说明tryMatch负责吃对手盘返回成交事件如果限价单还有剩余就挂到订单簿并生成一个「已接受」事件。参数说明remaining是未成交数量OrderType.LIMIT和OrderType.MARKET决定是否允许挂单。你改代码时如果想让市价单不产生挂单事件就在这里加判断。2.3 例子怎么跑从入口类到一次完整成交源码包里通常会有示例入口常见做法是提供一个Main或Demo类构造一个引擎实例然后手动灌几笔订单。你跑例子时不要只点运行要顺着调用链看一遍引擎初始化时创建了哪些组件、订单簿怎么被注入、事件处理器注册在哪。# 常见构建方式具体以源码包内说明为准 mvn clean compile mvn exec:java -Dexec.mainClassexample.MatchingDemo如果构建工具是 Gradle就把命令换成./gradlew build和./gradlew run。跑起来后重点看控制台输出的成交记录成交价格是不是等于对手价、成交量是不是取两者最小值、剩余量有没有正确回到订单簿。这三个点对了说明撮合主逻辑是通的。3. 把例子改成自己的场景参数、订单类型与事件处理3.1 先改配置再改逻辑很多人一上来就动撮合核心结果改崩了不知道哪出错。我的习惯是先找配置类或常量类把能调的参数过一遍。常见参数包括是否允许自成交、价格精度、订单最大数量、事件队列容量。这些参数往往决定了系统边界改起来风险低又能快速验证行为。比如「是否允许自成交」这个开关在测试环境里通常关掉避免同一个账户的买卖单互相吃掉。你可以在配置里找到对应布尔值改成false后重新跑例子观察同一账户下的买单和卖单是否还会成交。这个验证过程比读十遍代码都管用。3.2 加一种订单类型以「立即成交或取消」为例源码里一般已经支持限价单和市价单但真实业务常需要 IOC立即成交或取消。加这种订单类型的思路是在撮合完成后如果还有剩余量不挂单直接生成取消事件。if (cmd.order.type OrderType.IOC) { events.addAll(tryMatch(cmd.order)); if (cmd.order.remaining 0) { events.add(OrderEvent.cancelled(cmd.order.id)); } return events; }逻辑说明IOC 订单先尝试撮合剩余部分不进入订单簿直接取消。参数说明remaining大于 0 表示没吃完OrderEvent.cancelled要带上订单 ID 和取消原因。你加完后要补测试用例至少覆盖「全部成交」和「部分成交后取消」两种情况。3.3 事件处理别把回报逻辑写进撮合里撮合引擎只负责产生事件至于事件怎么推给客户端、怎么落库、怎么触发风控应该放在事件处理器里。源码例子中通常有一个简单的事件打印器你可以把它替换成自己的处理器。常见做法是实现一个接口在onEvent方法里根据事件类型分发。public class MyEventHandler implements EventHandler { Override public void onEvent(OrderEvent event) { switch (event.type) { case TRADE: // 更新持仓、推送成交回报 break; case ACCEPTED: // 推送订单已接受 break; case CANCELLED: // 推送撤单成功 break; default: break; } } }参数说明event.type是枚举TRADE事件里会带成交价和成交量。注意不要在onEvent里做耗时操作否则会拖慢整个事件队列。如果确实要落库建议先丢到内存队列由另一个线程消费。4. 避坑与排查跑例子时最容易翻车的几个点4.1 现象例子跑起来但没有成交记录原因订单价格没有交叉或者买卖方向写反了。常见情况是买单价低于卖单价自然撮合不上。解决先打印订单簿最优买卖价确认买价大于等于卖价再跑。如果方向写反检查Side.BUY和Side.SELL的赋值。4.2 现象撤单后订单还在订单簿里原因撤单只改了订单状态没有从价格档位队列中移除。解决找到removeOrder方法确认它同时操作了订单索引和队列。如果队列是ArrayDeque中间删除需要先找到节点建议加一个MapLong, Order索引。4.3 现象成交数量对不上出现负数剩余量原因撮合时没有取买卖双方的最小值或者扣减逻辑写错。解决成交数量应该是min(买方剩余, 卖方剩余)然后双方各自扣减。检查扣减后是否做了小于零的判断正常情况不应该出现负数。4.4 现象事件顺序错乱先收到成交后收到接受原因事件没有按产生顺序入队或者多线程处理导致乱序。解决确保单个订单的事件在同一个线程内按顺序生成和投递。如果用了线程池给同一个订单 ID 做哈希路由保证顺序。4.5 现象改完代码编译通过但运行报空指针原因新增字段没有初始化或者配置类没注入。解决先看堆栈定位到具体行再检查该对象的构造过程。常见做法是给订单簿和事件处理器加非空校验早失败早发现。5. 进阶用法用事件回放验证撮合一致性5.1 把事件存下来再回放一遍撮合系统最怕的是「同样的输入跑出不同结果」。验证方法很简单把一次运行产生的所有事件按顺序写到文件然后写一个回放器读事件重建订单簿状态对比最终持仓和成交总量。如果两次一致说明撮合逻辑是确定性的。// 回放从事件流重建订单状态 void replay(ListOrderEvent events) { MapLong, Order orders new HashMap(); for (OrderEvent e : events) { if (e.type EventType.ACCEPTED) { orders.put(e.orderId, e.order); } else if (e.type EventType.TRADE) { // 扣减双方数量 orders.get(e.buyOrderId).remaining - e.quantity; orders.get(e.sellOrderId).remaining - e.quantity; } else if (e.type EventType.CANCELLED) { orders.remove(e.orderId); } } // 对比 orders 与原始订单簿 }参数说明e.quantity是成交量remaining是剩余量。回放时不要重新撮合只做状态变更这样才能验证事件本身是否自洽。5.2 用边界用例压一压我一般会准备几组边界数据同价多单、跨多档大单、部分成交后撤单、自成交开关切换。每组跑完看三个指标成交总量、订单簿剩余量、事件条数。如果事件条数和预期不符多半是某个分支漏了事件。用例输入预期结果同价多单同一价格三笔买单一笔卖单按时间优先成交先挂的先成交跨多档大单一笔大卖单吃掉三个买价档位生成三笔成交事件价格依次降低部分成交后撤单限价单部分成交后撤单剩余量取消已成交部分不变自成交关闭同一账户买卖单价格交叉不成交订单挂起5.3 一个具体技巧给订单簿加快照每次事件处理完可以定期给订单簿拍快照记录最优买卖价和总深度。这样出问题时不用翻全量日志直接对比快照就能定位到哪一步开始异常。快照不用太频繁每处理一千条事件存一次就够。我自己的习惯是任何撮合相关的改动跑完例子必须走一遍事件回放和快照对比确认没有引入不确定性。从那以后我每次改撮合逻辑都强制走一遍这个流程省了很多事后排查的时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?