原文转载自「刘悦的技术博客」https://v3u.cn/a_id_192

如果你爱他,那么送他去股市,因为那里是天堂;如果你恨他,送他去股市,因为那里是地狱。

在过去的一年里,新冠疫情持续冲击世界经济,全球主要股票市场的波动都相对频繁,尤其是A股,正所谓:曾经跌停难为鬼,除非解套才做人;抄底时难抛亦难,反弹无力百花残。对于波谲云诡的股票市场,新投资人还是需要谨慎入场,本次我们来利用双队列的数据结构实现实时在线交易匹配引擎,探索股票交易的奥秘。

首先需要明确一点,证券交易和传统的B2C电商系统交易完全不同,证券交易系统提供的买卖标的物是标准的数字化资产,如美元、股票、比特币等等,它们的特点是数字计价,可分割买卖,也就是说,当我们发起买盘申请的时候,需要有价格对应的卖盘响应,才能真正完成交易,反之亦然。

具体逻辑是:所有买盘或者卖盘的订单队列都传递给匹配引擎,匹配引擎尝试将它们的价格进行匹配。该匹配队列分为买单(按价格升序排列,出价最高的优先交易)和卖单(按降序排列,卖价最低的优先交易)。如果股票订单找不到与匹配的价格,那么该订单就继续保存在订单队列中的原适当位置。

这里我们以实际的案例来看一下相关匹配算法的实现,假设我有两个订单队列,一个买盘,一个卖盘:

  1. #买盘
  2. 价格 数量
  3. 100 50
  4. 100 10
  5. 90 5
  6. 88 3
  7. #卖盘
  8. 价格 数量
  9. 170 50
  10. 180 40
  11. 199 10
  12. 200 5

最常见的匹配算法就是“价格/时间优先”队列。订单主要根据价格进行匹配,如果以相同的价格水平存在多个订单,则最早的订单将首先被匹配,这也和队列原理相同:先入先出。

如上所示,假设有两个订单紧挨着。第一个是以100块钱的价格买入50股的买入订单,第二个也是以相同价格买入10股的买入订单。鉴于订单与任何卖价都不匹配(由于其价格低于最低的卖价),所以它们都被放置在订单队列中。第一订单和第二订单以相同的价格水平存储,但是由于时间优先,前者比后者具有优先权。这基本上意味着,第一个订单将被放置在买入队列中的第二个订单的前面。

而卖盘同理,首先卖价最低的优先交易,如果卖价相同,则时间优先,先进队列的先交易,可是很多散户都遇见过一种情况,就是如果手里的一支股票连续跌停,就算拼命挂低价单也很难卖出去,甚至可能直接跌到退市血本无归,这是为什么呢?

因为当一只股票跌停时,也意味着有一大堆筹码堆积在跌停板上,想卖出去是不容易的,得排队,理论上按照“时间优先、价格优先”的交易原则排队成交,但跌停的情况下,只存在“时间优先”的考虑,也就是说,如果想在封死跌停板时把股票卖出去,就得尽早对该股票挂跌停板价格卖出。

可实际上,一只股票跌停,不光是小部分散户卖不出去,而是大多数散户都卖不出去,都在恐慌性出货,大家都在排队卖。更何况,股票买卖是通过券商进行的,而券商有VIP快速通道也不是什么秘密,一些大资金的大户、游资、机构享有券商优待,或通过租用通道实现对盘面的快速优先买卖,这也导致了在股票涨停板抢筹、跌停板出货时存在一定的“不公平”性,也就说,交易队列并非完全遵照“价格/时间”定序,还有可能出现优先级(加权)队列,所以,跌停时跑不了,涨停时买不进就不是什么新鲜事了。

另外,还需要注意匹配算法中的价格一直而数量匹配填充的问题,假设买单10块挂单50手,卖单10块挂单30手,则匹配的价格为10块钱,在买一卖一各显示30手,买单队列首位置就会有20手在排队,如下所示:

  1. #买盘
  2. 价格 数量
  3. 10 50
  4. #卖盘
  5. 价格 数量
  6. 10 30
  7. 11 50

经过匹配算法之后:

  1. #买盘
  2. 价格 数量
  3. 10 20
  4. #卖盘
  5. 价格 数量
  6. 11 50

OK,了解了基本概念,让我们用Python3具体实现,首先需要定义两个类,订单和交易,订单对象作为匹配算法之前的元素,而交易对象则是匹配之后的成交对象:

  1. class Order:
  2. def __init__(self, order_type, side, price, quantity):
  3. self.type = order_type
  4. self.side = side.lower()
  5. self.price = price
  6. self.quantity = quantity
  7. class Trade:
  8. def __init__(self, price, quantity):
  9. self.price = price
  10. self.quantity = quantity

这里type是订单类型,side代表买单或者卖单,price为价格,quantity为数量。

紧接着我们来实现订单队列:

  1. class OrderBook:
  2. def __init__(self, bids=[], asks=[]):
  3. self.bids = sorted(bids, key = lambda order: -order.price)
  4. self.asks = sorted(asks, key = lambda order: order.price)
  5. def __len__(self):
  6. return len(self.bids) + len(self.asks)
  7. def add(self, order):
  8. if order.type == 'buy':
  9. self.bids.append(order)
  10. elif order.type == 'sell':
  11. self.asks.append(order)
  12. def remove(self, order):
  13. if order.type == 'buy':
  14. self.bids.remove(order)
  15. elif order.type == 'sell':
  16. self.asks.remove(order)

这里的订单队列很容易地实现为具有两个排序列表的数据结构,其中两个列表包含两个按价格排序的订单实例。一种按升序排序(买单),另一种按降序排序(卖单)。

下面来实现系统的核心功能,匹配引擎:

  1. from collections import deque
  2. class MatchingEngine:
  3. def __init__(self):
  4. self.queue = deque()
  5. self.orderbook = OrderBook()
  6. self.trades = deque()

首先,我们需要两个FIFO队列;一个用于存储所有传入的订单,另一个用于存储经过匹配后所有产生的交易。我们还需要存储所有没有匹配的订单。

之后,通过调用.process(order)函数将订单传递给匹配引擎。然后将匹配生成的交易存储在队列中,然后可以依次检索(通过匹配引擎交易队列),也可以通过调用.get_trades()函数将其存储在列表中。

  1. def process(self, order):
  2. self.match(order)
  3. def get_trades(self):
  4. trades = list(self.trades)
  5. return trades

随后就是匹配方法:

  1. def match(self, order):
  2. if order.side == 'buy':
  3. filled = 0
  4. consumed_asks = []
  5. for i in range(len(self.orderbook.asks)):
  6. ask = self.orderbook.asks[i]
  7. if ask.price > order.price:
  8. break # 卖价过高
  9. elif filled == order.quantity:
  10. break # 已经匹配
  11. if filled + ask.quantity <= order.quantity:
  12. filled += ask.quantity
  13. trade = Trade(ask.price, ask.quantity)
  14. self.trades.append(trade)
  15. consumed_asks.append(ask)
  16. elif filled + ask.quantity > order.quantity:
  17. volume = order.quantity-filled
  18. filled += volume
  19. trade = Trade(ask.price, volume)
  20. self.trades.append(trade)
  21. ask.quantity -= volume
  22. # 没匹配成功的
  23. if filled < order.quantity:
  24. self.orderbook.add(Order("limit", "buy", order.price, order.quantity-filled))
  25. # 成功匹配的移出订单队列
  26. for ask in consumed_asks:
  27. self.orderbook.remove(ask)
  28. elif order.side == 'sell':
  29. filled = 0
  30. consumed_bids = []
  31. for i in range(len(self.orderbook.bids)):
  32. bid = self.orderbook.bids[i]
  33. if bid.price < order.price:
  34. break
  35. if filled == order.quantity:
  36. break
  37. if filled + bid.quantity <= order.quantity:
  38. filled += bid.quantity
  39. trade = Trade(bid.price, bid.quantity)
  40. self.trades.append(trade)
  41. consumed_bids.append(bid)
  42. elif filled + bid.quantity > order.quantity:
  43. volume = order.quantity-filled
  44. filled += volume
  45. trade = Trade(bid.price, volume)
  46. self.trades.append(trade)
  47. bid.quantity -= volume
  48. if filled < order.quantity:
  49. self.orderbook.add(Order("limit", "sell", order.price, order.quantity-filled))
  50. for bid in consumed_bids:
  51. self.orderbook.remove(bid)
  52. else:
  53. self.orderbook.add(order)

逻辑上并不复杂,基本上就是在订单队列中遍历,直到收到的订单被完全匹配为止。对于每个匹配成功的订单,都会创建一个交易对象并将其添加到交易队列中。如果匹配引擎无法完全完成匹配,则它将剩余量作为单独的订单再添加会订单队列中。

当然了,为了应对高并发场景,实现每秒成千上万的交易量,我们可以对匹配引擎进行改造,让它具备多任务异步执行的功能:

  1. from threading import Thread
  2. from collections import deque
  3. class MatchingEngine:
  4. def __init__(self, threaded=False):
  5. self.queue = deque()
  6. self.orderbook = OrderBook()
  7. self.trades = deque()
  8. self.threaded = threaded
  9. if self.threaded:
  10. self.thread = Thread(target=self.run)
  11. self.thread.start()

改造线程方法:

  1. def process(self, order):
  2. if self.threaded:
  3. self.queue.append(order)
  4. else:
  5. self.match(order)

最后,为了让匹配引擎能够以线程的方式进行循环匹配,添加启动入口:

  1. def run(self):
  2. while True:
  3. if len(self.queue) > 0:
  4. order = self.queue.popleft()
  5. self.match(order)
  6. print(self.get_trades())
  7. print(len(self.orderbook))

大功告成,完整代码如下:

  1. class Order:
  2. def __init__(self, order_type, side, price, quantity):
  3. self.type = order_type
  4. self.side = side.lower()
  5. self.price = price
  6. self.quantity = quantity
  7. class Trade:
  8. def __init__(self, price, quantity):
  9. self.price = price
  10. self.quantity = quantity
  11. class OrderBook:
  12. def __init__(self, bids=[], asks=[]):
  13. self.bids = sorted(bids, key = lambda order: -order.price)
  14. self.asks = sorted(asks, key = lambda order: order.price)
  15. def __len__(self):
  16. return len(self.bids) + len(self.asks)
  17. def add(self, order):
  18. if order.type == 'buy':
  19. self.bids.append(order)
  20. elif order.type == 'sell':
  21. self.asks.append(order)
  22. def remove(self, order):
  23. if order.type == 'buy':
  24. self.bids.remove(order)
  25. elif order.type == 'sell':
  26. self.asks.remove(order)
  27. from threading import Thread
  28. from collections import deque
  29. class MatchingEngine:
  30. def __init__(self, threaded=False):
  31. order1 = Order(order_type="buy",side="buy",price=10,quantity=10)
  32. order2 = Order(order_type="sell",side="sell",price=10,quantity=20)
  33. self.queue = deque()
  34. self.orderbook = OrderBook()
  35. self.orderbook.add(order1)
  36. self.orderbook.add(order2)
  37. self.queue.append(order1)
  38. self.queue.append(order2)
  39. self.trades = deque()
  40. self.threaded = threaded
  41. if self.threaded:
  42. self.thread = Thread(target=self.run)
  43. self.thread.start()
  44. def run(self):
  45. while True:
  46. if len(self.queue) > 0:
  47. order = self.queue.popleft()
  48. self.match(order)
  49. print(self.get_trades())
  50. print(len(self.orderbook))
  51. def process(self, order):
  52. if self.threaded:
  53. self.queue.append(order)
  54. else:
  55. self.match(order)
  56. def get_trades(self):
  57. trades = list(self.trades)
  58. return trades
  59. def match(self, order):
  60. if order.side == 'buy':
  61. filled = 0
  62. consumed_asks = []
  63. for i in range(len(self.orderbook.asks)):
  64. ask = self.orderbook.asks[i]
  65. if ask.price > order.price:
  66. break # 卖价过高
  67. elif filled == order.quantity:
  68. break # 已经匹配
  69. if filled + ask.quantity <= order.quantity:
  70. filled += ask.quantity
  71. trade = Trade(ask.price, ask.quantity)
  72. self.trades.append(trade)
  73. consumed_asks.append(ask)
  74. elif filled + ask.quantity > order.quantity:
  75. volume = order.quantity-filled
  76. filled += volume
  77. trade = Trade(ask.price, volume)
  78. self.trades.append(trade)
  79. ask.quantity -= volume
  80. # 没匹配成功的
  81. if filled < order.quantity:
  82. self.orderbook.add(Order("limit", "buy", order.price, order.quantity-filled))
  83. # 成功匹配的移出订单队列
  84. for ask in consumed_asks:
  85. self.orderbook.remove(ask)
  86. elif order.side == 'sell':
  87. filled = 0
  88. consumed_bids = []
  89. for i in range(len(self.orderbook.bids)):
  90. bid = self.orderbook.bids[i]
  91. if bid.price < order.price:
  92. break
  93. if filled == order.quantity:
  94. break
  95. if filled + bid.quantity <= order.quantity:
  96. filled += bid.quantity
  97. trade = Trade(bid.price, bid.quantity)
  98. self.trades.append(trade)
  99. consumed_bids.append(bid)
  100. elif filled + bid.quantity > order.quantity:
  101. volume = order.quantity-filled
  102. filled += volume
  103. trade = Trade(bid.price, volume)
  104. self.trades.append(trade)
  105. bid.quantity -= volume
  106. if filled < order.quantity:
  107. self.orderbook.add(Order("limit", "sell", order.price, order.quantity-filled))
  108. for bid in consumed_bids:
  109. self.orderbook.remove(bid)
  110. else:
  111. self.orderbook.add(order)

测试一下:

  1. me = MatchingEngine(threaded=True)
  2. me.run()

返回结果:

  1. liuyue:mytornado liuyue$ python3 "/Users/liuyue/wodfan/work/mytornado/test_order_match.py"
  2. [<__main__.Trade object at 0x102c71750>]
  3. 2
  4. [<__main__.Trade object at 0x102c71750>, <__main__.Trade object at 0x102c71790>]
  5. 1

没有问题。

结语:所谓天下熙熙,皆为利来;天下攘攘,皆为利往。太史公这句名言揭示了股票市场的本质,人性的本能就是追求利益,追求利益却要在决对原则之下,但是资本市场往往是残酷的,王霸雄图,荣华敝屣,到最后,也不过是尽归尘土。

原文转载自「刘悦的技术博客」 https://v3u.cn/a_id_192

王霸雄图荣华敝屣,谈笑间尽归尘土|基于Python3双队列数据结构搭建股票/外汇交易匹配撮合系统的更多相关文章

  1. 『王霸之路』从0.1到2.0一文看尽TensorFlow奋斗史

    ​ 0 序篇 2015年11月,Google正式发布了Tensorflow的白皮书并开源TensorFlow 0.1 版本. 2017年02月,Tensorflow正式发布了1.0.0版本,同时也标志 ...

  2. 统计Go, Go, Go

    作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢!   结束了概率论,我们数据之旅的下一站是统计.这一篇,是统计的一个小介绍.   统 ...

  3. java并发库 Lock 公平锁和非公平锁

    jdk1.5并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,关于两者区别,java并发编程实践里面有解释 公平锁:   Threads acquir ...

  4. java多线程之:Java中的ReentrantLock和synchronized两种锁定机制的对比 (转载)

    原文:http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html 多线程和并发性并不是什么新内容,但是 Java 语言设计中的创新之 ...

  5. Java 理论与实践: JDK 5.0 中更灵活、更具可伸缩性的锁定机制

    新的锁定类提高了同步性 —— 但还不能现在就抛弃 synchronized JDK 5.0为开发人员开发高性能的并发应用程序提供了一些很有效的新选择.例如,java.util.concurrent.l ...

  6. Java中的ReentrantLock和synchronized两种锁机制的对比

    原文:http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html 多线程和并发性并不是什么新内容,但是 Java 语言设计中的创新之 ...

  7. B站资源索引

    自从搭建了B站的监控之后,就收集了一堆up主,下面分类整理一下,排名不分先后,内容会持续更新……2019-4-10 19:04:08 一.酷玩&装机&开箱 1.AS极客 2.Virtu ...

  8. Java中的ReentrantLock和synchronized两种锁定

    原文:http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html 多线程和并发性并不是什么新内容,但是 Java 语言设计中的创新之 ...

  9. Java中的ReentrantLock和synchronized两种锁定机制

    原文:http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html 多线程和并发性并不是什么新内容,但是 Java 语言设计中的创新之 ...

随机推荐

  1. 论文解读《Deep Attention-guided Graph Clustering with Dual Self-supervision》

    论文信息 论文标题:Deep Attention-guided Graph Clustering with Dual Self-supervision论文作者:Zhihao Peng, Hui Liu ...

  2. WPF全局异常处理

    private void RegisterEvents() { //Task线程内未捕获异常处理事件 TaskScheduler.UnobservedTaskException += TaskSche ...

  3. 如何定制.NET6.0的日志记录

    在本章中,也就是整个系列的第一部分将介绍如何定制日志记录.默认日志记录仅写入控制台或调试窗口,这在大多数情况下都很好,但有时需要写入到文件或数据库,或者,您可能希望扩展日志记录的其他信息.在这些情况下 ...

  4. linux篇-linux数据库mysql的安装

    1数据库文件放到opt下面 2赋予权限775 3运行脚本 4运行成功 5数据库操作 密码修改并刷新 权限修改,允许外部设备访问 6工具连接 7附录 1.显示当前数据库服务器中的数据库列表: mysql ...

  5. CefSharp 白屏问题

    原文 现象 我正在使用 cefsharp + winform 建立一个桌面程序用于显示网页.使用过程中程序会突然白屏,经过观察发现,在网页显示GIF动图时,浏览器子程序会突然占用较高内存(从80M上升 ...

  6. 使用C#和MonoGame开发俄罗斯方块游戏

    小的时候就看到有同学使用C语言在DOS下做过一款俄罗斯方块的游戏,当时是启用了DOS的图形化模式,感觉也挺有意思.最近上海疫情封控在家,周末也稍微有点空余时间,于是使用Visual Studio 20 ...

  7. 解决Mysql搭建成功后执行sql语句报错以及区分大小写问题

    刚搭建完mysql 8.0以后会: 一.表区分大小写, 二.执行正确的sql语句成功且会报:[Err] 1055 - Expression #1 of ORDER BY clause is not i ...

  8. 我是一个Dubbo数据包...

    hello,大家好呀,我是小楼! 今天给大家带来一篇关于Dubbo IO交互的文章,本文是一位同事写的文章,用有趣的文字把枯燥的知识点写出来,通俗易懂,非常有意思,所以迫不及待找作者授权然后分享给大家 ...

  9. CVE-2021-3156漏洞复现

    CVE-2021-3156linux sudo 权限提升 版本ubantu18.04 使用这个命令可以是普通用户直接提升至管理员权限. 手动测试终端输入 sudoedit -s / 不知道什么原因ub ...

  10. ssh-免密钥登陆

    实现openssh免密钥登陆(公私钥验证) 在主机A上,通过root用户,使用ssh-keygen生成的两个密钥:id_rsa和id_rsa.pub 私钥(id_rsa)保存在本地主机,公钥(id_r ...