浅谈撮合引擎设计
- 撮合引擎简介
- 撮合引擎的发展
- 币安
- 中小型交易所
- 小型交易所
- 业务
- 交易流程
- 竞价方式
- 交易所常用指令
- 开发
- 简易架构设计
- 撮数据结构设计
- 交易委托账本
- 限价委托单
- 其它委托单
- 关键代码实现
- 1.创建一个ringbuffer
- 2. 设置事件监听
- 4.订单撮合主逻辑
- 撮合分支processMath函数逻辑
- PS
撮合引擎简介
撮合引擎是所有撮合交易系统的核心组件,不管是股票交易系统——包括现货交易、期货交易、期权交易等,还是数字货币交易系统——包括币币交易、合约交易、杠杆交易等,以及各种不同的贵金属交易系统、大宗商品交易系统等,虽然各种不同交易系统的交易标的不同,但只要都是采用撮合交易模式,都离不开撮合引擎。
撮合引擎是可以具有通用性的,一套具有通用性的撮合引擎实现理论上可以应用到任何撮合交易系统中,而无需做任何代码上的调整。即是说,同一套撮合引擎实现,既可以应用在股票交易系统,也可以应用在数字货币交易系统,可以用于现货交易,也可以用于合约交易等。
撮合引擎的发展
国内的很多数字货币交易所的撮合引擎,就是从数据库撮合迭代为内存撮合。
以前,基于数据库的撮合技术,TPS 一般只有10笔/秒。而现在基本都是采用内存撮合技术,TPS 很容易就能达到1000笔/秒,如果使用独占的高性能服务器,1万笔/秒甚至更高的 TPS 都不难达到。
LMAX 公司(LMAX是伦敦的一家外汇交易所)开源的 Disruptor,以及LMAX 架构,它的一个核心概念就是把数据放内存里,由此实现了每秒处理600万订单的超高效率。
LMAX架构巧妙在于,它并不认为多核CPU和多线程能解决问题,因为锁是一种很复杂的东西,而且并不见得效率高,它把锁去掉,降低整个系统的复杂度,并且用无锁队列 RingBuffer 来处理生产者消费者问题。
当然也并非所有的交易所都用LMAX架构。
币安
属于全内存撮合引擎LMAX架构,订单处理速度能达到 140 万单/秒。内存快照使用mmap(memory map file)的技术保证断电不会丢失数据,并且重写了很多 Java 的底层类库,去掉同步 lock,去掉异常的检查,定制化成撮合引擎专用的类库,进行了大量的 JVM 调优。
中小型交易所
内存撮合与数据库撮合结合的方式。
小型交易所
数据库撮合,只能支持 1000-2000 个用户。
业务
交易流程
- 系统开放某个交易对的交易功能。
- 用户提交该交易对的的买卖申报,即委托单。
- 系统验证委托单是否有效,包括交易标的是否处于可交易的状态、订单的价格和数量是否符合要求等。
- 确定该委托单的**挂单(Maker)费率和吃单(Taker)**费率。
- 检查用户的资产账户情况,包括账户状态是否交易受限,是否有足够资金用于下单等。
- 将详细的委托单数据持久化到数据库,并冻结用户账户中相应数量的资金。
- 将委托单进行撮合处理,即在交易**委托账本(OrderBook)**中寻找能与该委托单匹配成交的订单,匹配的结果可能是:全部成交、部分成交或无匹配。全部成交或部分成交时,可能在交易委托账本中存在一个或多个匹配的订单,即会产生一条或多条成交记录。当无匹配或部分成交时,委托单的部分数据包括剩余未成交的数量会暂时保存到交易委托账本中,等待与后续的委托单匹配撮合。
- 将撮合产生的成交记录持久化到数据库,并根据历史成交记录生成市场行情数据,如K线数据、今日涨跌幅等。
- 更新数据库中所有成交订单的委托单数据,以及更新订单用户的资产账户余额。
- 更新的订单数据、市场数据等推送给到前台。
整个交易流程中涉及到多个服务,包括用户服务、账户服务、订单服务、撮合服务、市场数据服务等。其中,只有第7步是撮合引擎处理的。从单一职责原则来说,撮合引擎就应该只做一件事,那就是负责撮合订单。撮合之前的委托单持久化、冻结资金等,以及撮合之后生成K线数据等,都不应该属于撮合引擎的职责。
竞价方式
撮合竞价方式一般有两种,一是集合竞价,二是连续竞价。股票交易系统一般会在不同交易时间段采用不同的竞价方式,比如在开盘或收盘时采用集合竞价,从而产生开盘价或收盘价,其余时间采用连续竞价。而大多数字货币交易系统则没有集合竞价,只有连续竞价,开盘价一般是在开始交易之前就设定好的。
集合竞价: 是指对一段时间内接收的买卖委托单一次性集中撮合的竞价方式
连续竞价: 是指对买卖委托单逐笔连续撮合的竞价方式。用户的挂单,只要满足成交条件,就能即时成交
交易所常用指令
Limit Order(限价单):限价委托必须以指定价格或者更好的价格成交,如果不能立即成交,则进入盘口等待成交,直到完全成交或者收到撤销指令或者收盘。
Market Order(市价单):市价委托立即以当前最佳价格成交。
Immediate-Or-Cancel (IOC): IOC委托必须立即以限价或者更好的价格成交,如果不能完全成交,未成交的部分立即取消。
Fill-Or-Kill (FOK):FOK委托必须立即以限价或者更好的价格完全成交,如果不能完全成交,则整个委托立即取消。
Cancel(取消订单):撤销指定的委托
开发
简易架构设计
亲和锁: linux在3.x以后增加了亲和锁的特性,可以将线程(或进程)绑定到给定的某个核心上,让它独享一核心,比如队列的忙等策略中、或者netty的eventLoop,将这个忙等的线程绑定到一个cpu核上,可以确保该进程的最大执行速度,实现低延迟,消除操作系统进行调度过程导致线程迁移所造成的抖动影响,还可以避免由于缓存失效而导致的性能开销.
撮数据结构设计
交易委托账本
交易委托账本(OrderBook)是整个撮合引擎里最核心也是最复杂的数据结构,每个交易对都需要维护一份交易委托账本,账本里保存着指定交易对所有待撮合的委托单。每份账本都有两个队列,一个卖单队列和一个买单队列,两个队列都需要按照价格优先、时间优先的原则进行排序。
限价委托单
其它委托单
TODO
关键代码实现
1.创建一个ringbuffer
/**
* 创建一个RingBuffer
* eventFactory: 事件工厂
* threadFactory: 我们执行者(消费者)的线程该怎么创建
* waitStrategy : 等待策略: 当我们ringBuffer 没有数据时,我们怎么等待
*/
@Bean
public RingBuffer<OrderEvent> ringBuffer(
EventFactory<OrderEvent> eventFactory,
ThreadFactory threadFactory,
WaitStrategy waitStrategy,
EventHandler<OrderEvent>[] eventHandlers
) {
/**
* 构建disruptor
*/
Disruptor<OrderEvent> disruptor = null;
ProducerType producerType = ProducerType.SINGLE;
if (disruptorProperties.isMultiProducer()) {
producerType = ProducerType.MULTI;
}
disruptor = new Disruptor<OrderEvent>(eventFactory, disruptorProperties.getRingBufferSize(), threadFactory, producerType, waitStrategy);
disruptor.setDefaultExceptionHandler(new DisruptorHandlerException());
// 设置消费者---我们的每个消费者代表我们的一个交易对,有多少个交易对,我们就有多少个eventHandlers ,事件来了后,多个eventHandlers 是并发执行的
disruptor.handleEventsWith(eventHandlers);
RingBuffer<OrderEvent> ringBuffer = disruptor.getRingBuffer();
disruptor.start();// 开始监听
final Disruptor<OrderEvent> disruptorShutdown = disruptor;
// 使用优雅的停机
Runtime.getRuntime().addShutdownHook(new Thread(
disruptorShutdown::shutdown, "DisruptorShutdownThread"
));
return ringBuffer;
}
2. 设置事件监听
/**
* 接收到了某个消息
*
* @param event
* @param sequence
* @param endOfBatch
* @throws Exception
*/
@Override
public void onEvent(OrderEvent event, long sequence, boolean endOfBatch) throws Exception {
// 从ringbuffer 里面接收了某个数据
Order order = (Order)event.getSource();
if(!order.getSymbol().equals(symbol)){ // 接收到其它事件 不处理
return;
}
MatchServiceFactory.getMatchService(MatchStrategy.LIMIT_PRICE).match(orderBooks ,order);
log.info("处理完成 订单事件===================>{}", event);
}
4.订单撮合主逻辑
/**
* 进行订单的撮合交易
*
* @param orderBooks
* @param order
*/
@Override
public void match(OrderBooks orderBooks, Order order) {
if (order.isCancelOrder()) {
orderBooks.cancelOrder(order);
//TODO 发送消息
log.info("订单取消成功 订单id={}",order.getOrderId());
return; // 取消单的操作
}
// 1 进行数据的校验
if (order.getPrice().compareTo(BigDecimal.ZERO) <= 0) {
return;
}
// 2 获取一个挂单队列
Iterator<Map.Entry<BigDecimal, MergeOrder>> markerQueueIterator = null;
if (order.getOrderDirection() == OrderDirection.BUY) {
markerQueueIterator = orderBooks.getCurrentLimitPriceIterator(OrderDirection.SELL);
} else {
markerQueueIterator = orderBooks.getCurrentLimitPriceIterator(OrderDirection.BUY);
}
// 是否退出循环
boolean exitLoop = false;
// 已经完成的订单
List<Order> completedOrders = new ArrayList<>();
// 产生的交易记录
List<ExchangeTrade> exchangeTrades = new ArrayList<>();
// 3 循环我们的队列
while (markerQueueIterator.hasNext() && !exitLoop) {
Map.Entry<BigDecimal, MergeOrder> markerOrderEntry = markerQueueIterator.next();
BigDecimal markerPrice = markerOrderEntry.getKey();
MergeOrder markerMergeOrder = markerOrderEntry.getValue();
// 我花10 块钱买东西 ,别人的东西如果大于10 块 ,我就买不了
if (order.getOrderDirection() == OrderDirection.BUY && order.getPrice().compareTo(markerPrice) < 0) {
break;
}
// 我出售一个东西 10 ,结果有个人花5块钱
if (order.getOrderDirection() == OrderDirection.SELL && order.getPrice().compareTo(markerPrice) > 0) {
break;
}
Iterator<Order> markerIterator = markerMergeOrder.iterator();
while (markerIterator.hasNext()) {
Order marker = markerIterator.next();
ExchangeTrade exchangeTrade = processMath(order, marker, orderBooks);
exchangeTrades.add(exchangeTrade);
if (order.isCompleted()) { // 经过一圈的吃单,我吃饱了
completedOrders.add(order);
exitLoop = true; // 退出最外层的循环
break; // 退出当前的MergeOrder的循环
}
if (marker.isCompleted()) {// MergeOrder 的一个小的订单完成了
completedOrders.add(marker);
markerIterator.remove();
}
}
if (markerMergeOrder.size() == 0) { // MergeOrder 已经吃完了
markerQueueIterator.remove(); // 将该MergeOrder 从树上移除掉
}
}
// 4 若我们的订单没有完成
if (order.getAmount().compareTo(order.getTradedAmount()) > 0) {
orderBooks.addOrder(order);
}
if (exchangeTrades.size() > 0) {
// 5 发送交易记录
handlerExchangeTrades(exchangeTrades);
}
if (completedOrders.size() > 0) {
// 6 发送已经成交的订单
completedOrders(completedOrders);
}
}
撮合分支processMath函数逻辑
// 1 定义交易的变量
// 成交的价格
BigDecimal dealPrice = marker.getPrice();
// 成交的数量
BigDecimal turnoverAmount = BigDecimal.ZERO;
// 本次需要的数量
BigDecimal needAmount = calcTradeAmount(taker); // 10 20
// 本次提供给你的数量
BigDecimal providerAmount = calcTradeAmount(marker); // 20 10
turnoverAmount = needAmount.compareTo(providerAmount) <= 0 ? needAmount : providerAmount;
if (turnoverAmount.compareTo(BigDecimal.ZERO) == 0) {
return null; // 无法成交
}
// 设置本次吃单的成交数据
taker.setTradedAmount(taker.getTradedAmount().add(turnoverAmount));
BigDecimal turnoverTaker = turnoverAmount.multiply(dealPrice).setScale(orderBooks.getCoinScale(), RoundingMode.HALF_UP);
taker.setTurnover(turnoverTaker);
// 设置本次挂单的成交数据
marker.setTradedAmount(marker.getTradedAmount().add(turnoverAmount));
BigDecimal markerTurnover = turnoverAmount.multiply(dealPrice).setScale(orderBooks.getBaseCoinScale(), RoundingMode.HALF_UP);
marker.setTurnover(markerTurnover);
ExchangeTrade exchangeTrade = new ExchangeTrade();
exchangeTrade.setAmount(turnoverAmount); // 设置购买的数量
exchangeTrade.setPrice(dealPrice); // 设置购买的价格
exchangeTrade.setTime(System.currentTimeMillis()); // 设置成交的时间
exchangeTrade.setSymbol(orderBooks.getSymbol()); // 设置成交的交易对
exchangeTrade.setDirection(taker.getOrderDirection()); // 设置交易的方法
exchangeTrade.setSellOrderId(marker.getOrderId()); // 设置出售方的id
exchangeTrade.setBuyOrderId(taker.getOrderId()); // 设置买方的id
exchangeTrade.setBuyTurnover(taker.getTurnover()); // 设置买方的交易额
exchangeTrade.setSellTurnover(marker.getTurnover()); // 设置卖方的交易额
/**
* 处理盘口:
* 我们的委托单肯定是: 将挂单的数据做了一部分消耗
*/
if (marker.getOrderDirection() == OrderDirection.BUY) {
// 减少挂单的数据量
orderBooks.getBuyTradePlate().remove(marker, turnoverAmount);
} else {
orderBooks.getSellTradePlate().remove(marker, turnoverAmount);
}
return exchangeTrade;
PS
时隔好几年了 ,成长了许多 但是从来没更新过博客 今天更新一把。
有几个问题大家可以思考下
1.聚合服务如何保证高可用?
2.快照服务如何生成快照?