浅谈撮合引擎

news2024/11/25 18:47:41

浅谈撮合引擎设计

  • 撮合引擎简介
    • 撮合引擎的发展
      • 币安
      • 中小型交易所
      • 小型交易所
    • 业务
      • 交易流程
      • 竞价方式
      • 交易所常用指令
    • 开发
      • 简易架构设计
      • 撮数据结构设计
        • 交易委托账本
        • 限价委托单
        • 其它委托单
      • 关键代码实现
        • 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 个用户。

业务

交易流程

  1. 系统开放某个交易对的交易功能
  2. 用户提交该交易对的的买卖申报,即委托单
  3. 系统验证委托单是否有效,包括交易标的是否处于可交易的状态、订单的价格和数量是否符合要求等。
  4. 确定该委托单的**挂单(Maker)费率和吃单(Taker)**费率。
  5. 检查用户的资产账户情况,包括账户状态是否交易受限,是否有足够资金用于下单等。
  6. 将详细的委托单数据持久化到数据库,并冻结用户账户中相应数量的资金
  7. 将委托单进行撮合处理,即在交易**委托账本(OrderBook)**中寻找能与该委托单匹配成交的订单,匹配的结果可能是:全部成交、部分成交或无匹配。全部成交或部分成交时,可能在交易委托账本中存在一个或多个匹配的订单,即会产生一条或多条成交记录。当无匹配或部分成交时,委托单的部分数据包括剩余未成交的数量会暂时保存到交易委托账本中,等待与后续的委托单匹配撮合。
  8. 将撮合产生的成交记录持久化到数据库,并根据历史成交记录生成市场行情数据,如K线数据、今日涨跌幅等。
  9. 更新数据库中所有成交订单的委托单数据,以及更新订单用户的资产账户余额
  10. 更新的订单数据、市场数据推送给到前台。

整个交易流程中涉及到多个服务,包括用户服务、账户服务、订单服务、撮合服务、市场数据服务等。其中,只有第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.快照服务如何生成快照?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/143807.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

uniapp实现iOS支付苹果内购支付踩过的坑以及具体操作步骤

由于我们app会员属于虚拟产品&#xff0c;所以苹果商店要求我们必须选择苹果内购&#xff0c;否则就勒令下架。 无奈&#xff0c;于是就又开始了踩坑之旅~ uniapp可以直接使用uni-pay的插件去进行苹果内购。 但是&#xff0c;在对接自己的项目之前&#xff0c;建议先跑通示例项…

JavaEE-Spring(Spring中的五大类注解,@Bean注解,对象装配(@Autowired,@Resource),Bean对象在Spring中的作用域)

文章目录1. 配置扫描路径2. Spring五大类注解3. Spring Bean注解对象装配4. Bean对象在Spring中的作用域5. Bean生命周期1. 配置扫描路径 只有设置了扫描路径&#xff0c;其他的路径下注解不会被Spring扫描 这里设置路径为com.beans下 <?xml version"1.0" enc…

(七)devops持续集成开发——jenkins流水线发布一个node环境下的前端vue项目

前言 在前面的章节中已经介绍了jenkins集成前端流水化部署环境的内容&#xff0c;本节内容是关于前端项目的流水化部署发布&#xff0c;通过实操发布一个前端项目&#xff0c;从而完成前端项目的流水化发布。前端项目主要是静态资源的发布&#xff0c;这里我们以一个vue项目为…

智慧物流信息化供应链管理体系转型发展现状

现如今&#xff0c;伴随着时代的迅速发展和高新科技水准的持续提升&#xff0c;人们慢慢进入了信息时代。在其中&#xff0c;物流制造行业也从以往20年前的粗放型管理机制慢慢变化为信息化、智慧化的管理机制。 5G、云计算技术、AI、物联网等新技术的出现加快了各个领域经营方法…

k线图中的三条线是什么?

新手投资朋友可能会在行情软件中发现&#xff0c;图表中除了K线以外&#xff0c;其下方还有三条颜色不一样的曲线&#xff0c;到底这三条线有什么功能呢&#xff1f;它们的使用方法又是怎样的呢&#xff1f; 其实&#xff0c;这三条线分别是短、中、长周期移动平均线&#xff0…

界面控件DevExpress WinForm——属于WinForm组件的MVVM框架

DevExpress WinForm拥有180组件和UI库&#xff0c;能为Windows Forms平台创建具有影响力的业务解决方案。DevExpress WinForm能完美构建流畅、美观且易于使用的应用程序&#xff0c;无论是Office风格的界面&#xff0c;还是分析处理大批量的业务数据&#xff0c;它都能轻松胜任…

谷粒学院——Day17【数据同步工具、SpringCloud【GateWay网关】、权限管理功能(接口)】

❤ 作者主页&#xff1a;Java技术一点通的博客 ❀ 个人介绍&#xff1a;大家好&#xff0c;我是Java技术一点通&#xff01;(&#xffe3;▽&#xffe3;)~* &#x1f34a; 记得关注、点赞、收藏、评论⭐️⭐️⭐️ &#x1f4e3; 认真学习&#xff0c;共同进步&#xff01;&am…

sql调优

一、MySQL架构总览&#xff1a; 三、SQL解析顺序 SELECT DISTINCT< select_list > FROM< left_table > < join_type > JOIN < right_table > ON < join_condition > WHERE< where_condition > GROUP BY< group_by_list > HAVING<…

振弦采集模块的频率值与温度值的修正

振弦采集模块的频率值与温度值的修正 此功能在 SF3.51 版本时增加。 固件版本 V3.51 修改固件版本号为 V3.51_2200827。 增加了频率和温度的多项式修正参数和对应指令。 $STFP、 $GTFP、 $STTP、 $GTTP 增加了 FFT 频幅数据输出功能。设置 ATSD_SEL.[5]为 1。 修正了 VM608 采集…

java实现的非关系型数据库:nosqldb

nosqldb一、nosqldb介绍二、nosqldb功能介绍三、数据存储结构介绍1. 数据文件存储结构(data.nosqldb)2.索引文件存储结构(index.mbdb)三、优化点1. 不支持连表查询2. 不支持分片存储3. 碎片整理一、nosqldb介绍 github地址 https://github.com/MaBo2420935619/nosqldb nosqld…

2022年终总结2023年计划

目录 前言&#xff1a; 2022年总结&#xff1a; 工作上&#xff1a; 生活上&#xff1a; 2023年规划&#xff1a; 工作上&#xff1a; 生活上&#xff1a; 前言&#xff1a; 嗨&#xff0c;不知不觉一年又过去了&#xff0c;2022已经结束了&#xff0c;我们迎来了2023。…

c++ -- STL容器--list容器

7. list容器7.1 简介① 功能&#xff1a;将数据进行链式存储。② 链表(list)是一种物理存储单元上非连续的存储结构&#xff0c;数据元素的逻辑顺序是通过链表中的指针链接实现的。③ 链表的组成&#xff1a;链表由一系列结点组成。④ 结点的组成&#xff1a;一个是存储数据元素…

serverless论文总结

1.Benchmarking, Analysis, and Optimization of Serverless Function Snapshots https://zhuanlan.zhihu.com/p/572288442 这项工作引入了vHive&#xff0c;一个针对无服务器实验的开源框架&#xff0c;它使系统研究人员能够在整个无服务器堆栈中进行创新。vHive集成了来自领…

ITSM | 权威指南发布,高速IT服务管理团队是什么样子的?

当Netflix正在打造流媒体平台时&#xff0c;还有人在营业厅里为一张网卡而烦恼。当Craig Newmark创建免费分类广告网站时&#xff0c;报社的网络管理员最关心的还是重启电子邮件服务器。当数字化转型成为一道必答题&#xff0c;谁能率先给出解题之法&#xff1f; 阅读本篇文章&…

Input子系统

文章目录前言Input子系统简介Input子系统代码实现框架Linux Input子系统支持的数据类型input核心层设备驱动层input_allocate_device 与 函数input_set_capabilityinput_register_device 函数input_unregister_device 与 input_free_device 函数事件处理层input_attach_handler…

A. Divide and Conquer

An array bb is good if the sum of elements of bb is even. You are given an array aa consisting of nn positive integers. In one operation, you can select an index ii and change ai:⌊ai2⌋ai:⌊ai2⌋. †† Find the minimum number of operations (possibly 00)…

ArcGIS 切片问题小结

1. 如果发布的切片缓存服务没有自动启动怎么办&#xff1f; 在进行切片时偶然情况下可能会遇到&#xff0c;你在切片时已经设置了server自动进行切片处理&#xff0c;但是在服务发布后&#xff0c;服务发布成功&#xff0c;但是服务没有成功启动&#xff0c;导致服务器没有自动…

免费数据恢复方法有哪些?分享这几种简单又实用的恢复方法(2023年最新)

很多时候&#xff0c;我们使用电脑总是容易误删一些数据。比如使用电脑&#xff0c;误删了办公资料&#xff1b;使用SD卡&#xff0c;出现照片没有办法打开的情况&#xff1b;使用移动硬盘&#xff0c;出现文档误格式化等问题。 不必要的数据被删除那就没问题&#xff0c;如果…

Java反序列化—Fastjson基础

0x01 前言 最近摆烂了很久&#xff0c;来学习一下fastjson 0x02 Fastjson 简介 Fastjson 是 Alibaba 开发的 Java 语言编写的高性能 JSON 库&#xff0c;用于将数据在 JSON 和 Java Object 之间互相转换。 提供两个主要接口来分别实现序列化和反序列化操作。 JSON.toJSONStr…

socket应用之从电脑发送图片到手机(1)之通信过程建立

本人曾经做了一个基于MPVd的C#开发的播放器&#xff0c;用于自娱自乐&#xff0c;后来又用websocket 写了个简单的远程控制器。由于websocket 要依赖于浏览器&#xff0c;因此有诸多不便&#xff0c;后来又用flutter写了一个&#xff0c;方便多了。 下面介绍具体实现。 1、通信…