基于Python3双队列数据结构搭建股票外汇交易匹配撮合系统
如果你爱他,那么送他去股市,因为那里是天堂;如果你恨他,送他去股市,因为那里是地狱。
在过去的一年里,新冠疫情持续冲击世界经济,全球主要股票市场的波动都相对频繁,尤其是A股,正所谓:曾经跌停难为鬼,除非解套才做人;抄底时难抛亦难,反弹无力百花残。对于波谲云诡的股票市场,新投资人还是需要谨慎入场,本次我们来利用双队列的数据结构实现实时在线交易匹配引擎,探索股票交易的奥秘。
首先需要明确一点,证券交易和传统的B2C电商系统交易完全不同,证券交易系统提供的买卖标的物是标准的数字化资产,如美元、股票、比特币等等,它们的特点是数字计价,可分割买卖,也就是说,当我们发起买盘申请的时候,需要有价格对应的卖盘响应,才能真正完成交易,反之亦然。
具体逻辑是:所有买盘或者卖盘的订单队列都传递给匹配引擎,匹配引擎尝试将它们的价格进行匹配。该匹配队列分为买单(按价格升序排列,出价最高的优先交易)和卖单(按降序排列,卖价最低的优先交易)。如果股票订单找不到与匹配的价格,那么该订单就继续保存在订单队列中的原适当位置。
这里我们以实际的案例来看一下相关匹配算法的实现,假设我有两个订单队列,一个买盘,一个卖盘: #买盘 价格 数量 100 50 100 10 90 5 88 3 #卖盘 价格 数量 170 50 180 40 199 10 200 5
最常见的匹配算法就是"价格/时间优先"队列。订单主要根据价格进行匹配,如果以相同的价格水平存在多个订单,则最早的订单将首先被匹配,这也和队列原理相同:先入先出。
如上所示,假设有两个订单紧挨着。第一个是以100块钱的价格买入50股的买入订单,第二个也是以相同价格买入10股的买入订单。鉴于订单与任何卖价都不匹配(由于其价格低于最低的卖价),所以它们都被放置在订单队列中。第一订单和第二订单以相同的价格水平存储,但是由于时间优先,前者比后者具有优先权。这基本上意味着,第一个订单将被放置在买入队列中的第二个订单的前面。
而卖盘同理,首先卖价最低的优先交易,如果卖价相同,则时间优先,先进队列的先交易,可是很多散户都遇见过一种情况,就是如果手里的一支股票连续跌停,就算拼命挂低价单也很难卖出去,甚至可能直接跌到退市血本无归,这是为什么呢?
因为当一只股票跌停时,也意味着有一大堆筹码堆积在跌停板上,想卖出去是不容易的,得排队,理论上按照"时间优先、价格优先"的交易原则排队成交,但跌停的情况下,只存在"时间优先"的考虑,也就是说,如果想在封死跌停板时把股票卖出去,就得尽早对该股票挂跌停板价格卖出。
可实际上,一只股票跌停,不光是小部分散户卖不出去,而是大多数散户都卖不出去,都在恐慌性出货,大家都在排队卖。更何况,股票买卖是通过券商进行的,而券商有VIP快速通道也不是什么秘密,一些大资金的大户、游资、机构享有券商优待,或通过租用通道实现对盘面的快速优先买卖,这也导致了在股票涨停板抢筹、跌停板出货时存在一定的"不公平"性,也就说,交易队列并非完全遵照"价格/时间"定序,还有可能出现优先级(加权)队列,所以,跌停时跑不了,涨停时买不进就不是什么新鲜事了。
另外,还需要注意匹配算法中的价格一直而数量匹配填充的问题,假设买单10块挂单50手,卖单10块挂单30手,则匹配的价格为10块钱,在买一卖一各显示30手,买单队列首位置就会有20手在排队,如下所示: #买盘 价格 数量 10 50 #卖盘 价格 数量 10 30 11 50
经过匹配算法之后: #买盘 价格 数量 10 20 #卖盘 价格 数量 11 50
OK,了解了基本概念,让我们用Python3具体实现,首先需要定义两个类,订单和交易,订单对象作为匹配算法之前的元素,而交易对象则是匹配之后的成交对象: class Order: def __init__(self, order_type, side, price, quantity): self.type = order_type self.side = side.lower() self.price = price self.quantity = quantity class Trade: def __init__(self, price, quantity): self.price = price self.quantity = quantity
这里type是订单类型,side代表买单或者卖单,price为价格,quantity为数量。
紧接着我们来实现订单队列: class OrderBook: def __init__(self, bids=[], asks=[]): self.bids = sorted(bids, key = lambda order: -order.price) self.asks = sorted(asks, key = lambda order: order.price) def __len__(self): return len(self.bids) + len(self.asks) def add(self, order): if order.type == "buy": self.bids.append(order) elif order.type == "sell": self.asks.append(order) def remove(self, order): if order.type == "buy": self.bids.remove(order) elif order.type == "sell": self.asks.remove(order)
这里的订单队列很容易地实现为具有两个排序列表的数据结构,其中两个列表包含两个按价格排序的订单实例。一种按升序排序(买单),另一种按降序排序(卖单)。
下面来实现系统的核心功能,匹配引擎: from collections import deque class MatchingEngine: def __init__(self): self.queue = deque() self.orderbook = OrderBook() self.trades = deque()
首先,我们需要两个FIFO队列;一个用于存储所有传入的订单,另一个用于存储经过匹配后所有产生的交易。我们还需要存储所有没有匹配的订单。
之后,通过调用.process(order)函数将订单传递给匹配引擎。然后将匹配生成的交易存储在队列中,然后可以依次检索(通过匹配引擎交易队列),也可以通过调用.get_trades()函数将其存储在列表中。 def process(self, order): self.match(order) def get_trades(self): trades = list(self.trades) return trades
随后就是匹配方法: def match(self, order): if order.side == "buy": filled = 0 consumed_asks = [] for i in range(len(self.orderbook.asks)): ask = self.orderbook.asks[i] if ask.price > order.price: break # 卖价过高 elif filled == order.quantity: break # 已经匹配 if filled + ask.quantity <= order.quantity: filled += ask.quantity trade = Trade(ask.price, ask.quantity) self.trades.append(trade) consumed_asks.append(ask) elif filled + ask.quantity > order.quantity: volume = order.quantity-filled filled += volume trade = Trade(ask.price, volume) self.trades.append(trade) ask.quantity -= volume # 没匹配成功的 if filled < order.quantity: self.orderbook.add(Order("limit", "buy", order.price, order.quantity-filled)) # 成功匹配的移出订单队列 for ask in consumed_asks: self.orderbook.remove(ask) elif order.side == "sell": filled = 0 consumed_bids = [] for i in range(len(self.orderbook.bids)): bid = self.orderbook.bids[i] if bid.price < order.price: break if filled == order.quantity: break if filled + bid.quantity <= order.quantity: filled += bid.quantity trade = Trade(bid.price, bid.quantity) self.trades.append(trade) consumed_bids.append(bid) elif filled + bid.quantity > order.quantity: volume = order.quantity-filled filled += volume trade = Trade(bid.price, volume) self.trades.append(trade) bid.quantity -= volume if filled < order.quantity: self.orderbook.add(Order("limit", "sell", order.price, order.quantity-filled)) for bid in consumed_bids: self.orderbook.remove(bid) else: self.orderbook.add(order)
逻辑上并不复杂,基本上就是在订单队列中遍历,直到收到的订单被完全匹配为止。对于每个匹配成功的订单,都会创建一个交易对象并将其添加到交易队列中。如果匹配引擎无法完全完成匹配,则它将剩余量作为单独的订单再添加会订单队列中。
当然了,为了应对高并发场景,实现每秒成千上万的交易量,我们可以对匹配引擎进行改造,让它具备多任务异步执行的功能: from threading import Thread from collections import deque class MatchingEngine: def __init__(self, threaded=False): self.queue = deque() self.orderbook = OrderBook() self.trades = deque() self.threaded = threaded if self.threaded: self.thread = Thread(target=self.run) self.thread.start()
改造线程方法: def process(self, order): if self.threaded: self.queue.append(order) else: self.match(order)
最后,为了让匹配引擎能够以线程的方式进行循环匹配,添加启动入口: def run(self): while True: if len(self.queue) > 0: order = self.queue.popleft() self.match(order) print(self.get_trades()) print(len(self.orderbook))
大功告成,完整代码如下: class Order: def __init__(self, order_type, side, price, quantity): self.type = order_type self.side = side.lower() self.price = price self.quantity = quantity class Trade: def __init__(self, price, quantity): self.price = price self.quantity = quantity class OrderBook: def __init__(self, bids=[], asks=[]): self.bids = sorted(bids, key = lambda order: -order.price) self.asks = sorted(asks, key = lambda order: order.price) def __len__(self): return len(self.bids) + len(self.asks) def add(self, order): if order.type == "buy": self.bids.append(order) elif order.type == "sell": self.asks.append(order) def remove(self, order): if order.type == "buy": self.bids.remove(order) elif order.type == "sell": self.asks.remove(order) from threading import Thread from collections import deque class MatchingEngine: def __init__(self, threaded=False): order1 = Order(order_type="buy",side="buy",price=10,quantity=10) order2 = Order(order_type="sell",side="sell",price=10,quantity=20) self.queue = deque() self.orderbook = OrderBook() self.orderbook.add(order1) self.orderbook.add(order2) self.queue.append(order1) self.queue.append(order2) self.trades = deque() self.threaded = threaded if self.threaded: self.thread = Thread(target=self.run) self.thread.start() def run(self): while True: if len(self.queue) > 0: order = self.queue.popleft() self.match(order) print(self.get_trades()) print(len(self.orderbook)) def process(self, order): if self.threaded: self.queue.append(order) else: self.match(order) def get_trades(self): trades = list(self.trades) return trades def match(self, order): if order.side == "buy": filled = 0 consumed_asks = [] for i in range(len(self.orderbook.asks)): ask = self.orderbook.asks[i] if ask.price > order.price: break # 卖价过高 elif filled == order.quantity: break # 已经匹配 if filled + ask.quantity <= order.quantity: filled += ask.quantity trade = Trade(ask.price, ask.quantity) self.trades.append(trade) consumed_asks.append(ask) elif filled + ask.quantity > order.quantity: volume = order.quantity-filled filled += volume trade = Trade(ask.price, volume) self.trades.append(trade) ask.quantity -= volume # 没匹配成功的 if filled < order.quantity: self.orderbook.add(Order("limit", "buy", order.price, order.quantity-filled)) # 成功匹配的移出订单队列 for ask in consumed_asks: self.orderbook.remove(ask) elif order.side == "sell": filled = 0 consumed_bids = [] for i in range(len(self.orderbook.bids)): bid = self.orderbook.bids[i] if bid.price < order.price: break if filled == order.quantity: break if filled + bid.quantity <= order.quantity: filled += bid.quantity trade = Trade(bid.price, bid.quantity) self.trades.append(trade) consumed_bids.append(bid) elif filled + bid.quantity > order.quantity: volume = order.quantity-filled filled += volume trade = Trade(bid.price, volume) self.trades.append(trade) bid.quantity -= volume if filled < order.quantity: self.orderbook.add(Order("limit", "sell", order.price, order.quantity-filled)) for bid in consumed_bids: self.orderbook.remove(bid) else: self.orderbook.add(order)
测试一下: me = MatchingEngine(threaded=True) me.run()
返回结果: liuyue:mytornado liuyue$ python3 "/Users/liuyue/wodfan/work/mytornado/test_order_match.py" [<__main__.Trade object at 0x102c71750>] 2 [<__main__.Trade object at 0x102c71750>, <__main__.Trade object at 0x102c71790>] 1
没有问题。
结语:所谓天下熙熙,皆为利来;天下攘攘,皆为利往。太史公这句名言揭示了股票市场的本质,人性的本能就是追求利益,追求利益却要在决对原则之下,但是资本市场往往是残酷的,王霸雄图,荣华敝屣,到最后,也不过是尽归尘土。
威少真的回不去了吗?真的开始下滑快退役了吗?威斯布鲁克比窦娥还要冤,已经成了詹姆斯祭坛上的肉酱。湖人队赢球,詹姆斯个人的功劳湖人队输球,威斯布鲁克只能走向祭坛。一位巨星沦落到今天的地步,确实是咎由自取。我们宁可相信世上有鬼,
有人说中考甚至比高考还要重要,你怎么看?中考比高考还重要,这一点不足为过!这主要体现以下两个方面第一,中考录取率远低于高考的录取率!现在各地的中考录取率,都在50左右,很多地方由于城市化进程的加快,高中学校数量明显不足,
基金亏18,不加仓,能回本吗?能。基金亏损很正常。之前2008年的时候,我同事买了点基金,一直拿到了2015年,七年的时间终于回本了,还稍有盈利。2018年,我问他还买基金不?今年是个低点,是投资的好机会。他大
幼儿园教师现在做课件,除了常用的wps演示,ppt之外,还有用什么的?PowerPointwps演示kynotefocusky万彩动画flash希沃Smart几何画板101pptAuthorware等随着信息化时代的来临,多媒体教学课件越来越广泛地应
警察抓到失足妇女,是否能通过(支付记录)找出其他嫖客进行处罚?抓到失足妇女,能通过(支付记录)找出其他相关人员来。但你怎么就能证明通过(支付记录)找到的就是嫖客呢?不能确定是嫖客,怎么进行处罚呢?我认为,这么做,能找到一部分嫖客来,可以处罚。
重罚北京首钢!9分钟吃3个违体,是被针对还是球风如此?还是少了一个违体,原本都判罚了,28号汪梅让伙伴去掉了一个,翟晓川和女记者起纠纷邪门28号裁判最近很少吹罚比赛,只是36号闫军的声望越来越高,连辽宁队也很少说什么,郭艾伦见面都给鞠
女记者遭重罚,北京双雄一点事儿没有,CBA公司一碗水端平了吗?看球的和赛场上的球员直接发生对骂,还真难得一见,大概也就在CBA能见到。看球的特别还是名女记者,(是记者吗?我都怀疑),你干吗在这种场合指名道姓大呼小叫的怒斥场上球员,既是你有看法
对五十五岁以后的下岗职工,都有哪些保障性政策?感谢邀请,感谢楼主的提问。楼主您好,对于55岁以后的下岗职工严格来讲,那么是不允许解除劳动合同的,因为在临退休5年的时候,劳动合同法明确规定是不允许解除企业职工的一个劳动合同的。当
大龄下岗失业员工应如何自处?下岗职工50岁退休合法合情合理,望有管部门重视下岗工人,让生活有保障。一定要有一个健康的身体,任何事情不要勉强自己,钱挣得来,不见得能花得去。根据自己的情况来交社保,千万不要为了以
郑州会向西南发展吗?今天将本回答梳理了一下,重新进行了编辑,让内容显得更为丰富和饱满,希望读者喜欢。本次重新编辑,仍然是直接干货,不放图片。整体说一下东,南,西,北四个方向,用排除法,看哪些不可能重点
五菱之光和五菱荣光有什么区别?哪个好?五菱品牌的面包车分为两种类型四个等级综合性价比之光最高解析基础中置后驱面包车前置后驱MPV或轻客面包车的分类从年检的新规开始做出了硬性区分,车身尺寸的宽高比0。9,采用中置发动机的
马为什么会被驯化?马,在古代的战争中起到了非常重要的作用,拥有骑兵几乎拥有了绝对的胜利权。不过随着科技的进步,很多动物的作用都开始退化,比如马,比如牛,反倒是宠物越来越吃香,成为人们的朋友了。这时,
太平天国的悲剧,洪秀全不懂扑克牌平衡太平天国的开局是很不错的,永安建制形成的格局是大王洪秀全小王杨秀清,4个2则是萧朝贵冯云山韦昌辉石达开。至于秦日纲杨辅清韦俊胡以晃等人,则是4个A。太平天国一路凯歌行进,但骨干力量
安乐公主最美丽的公主唐中宗李显的小女儿安乐公主,拥有很多之最最美丽最受宠最狠毒最不孝最具野心比杨贵妃更美,比武则天更狠毒,她演出了唐朝历史上最悲惨的一幕。从小颠沛流的离的放逐生活,让她吃尽苦头,九死一
鉴赏121李白最霸气的一首诗,捧红了他的道士朋友隋大业七年(611年),周至县楼观道主持岐晖召集观中诸弟子,神秘地告诉大家,他夜观天象,得到一个重要的启示天道将改,当有老君子孙治世,此后吾教大兴。老君指的就是道教尊奉的祖师老子,
你所不知道的泥河湾2泥河湾再现百万年前远古盛宴神秘而古老的文化,惊艳的出土文物,庞大的遗址群这是泥河湾给人们的印象。泥河湾这片古老而神奇的土地几乎记录了200万年来人类起源和演变的全过程,在这里,不仅能看到人类天下第一餐距今近
初唐十道盛唐十五道晚唐四十八藩镇的变迁唐朝是中国历史上继隋之后的大一统王朝,从唐高祖李渊到唐哀帝李柷一共出现过21位皇帝,享国289年,以强盛和开放包容著称。唐朝疆域极盛时期东起日本海,南至罗伏州(今越南顺化一带),西
杨利伟在太空听到敲门声,13年后谜底才揭晓,原因是一种现象2003年10月15日,注定是个激动人心的日子。在全国人民的期盼下,杨利伟乘坐神舟五号成功进入太空。还没等杨利伟从喜悦的心情中平复下来,就听到太空舱传来一阵奇怪的声响。仿佛敲门声一
中国古代没有女厕所,女人是如何解决内急的?在我国历史上,科技水平以及生产能力都非常的落后,很是贫穷,所以民间很多基础设施都很不完善,在大街上很少有公共厕所,即便是有,那也是给男人用的,并没有女性专门的厕所。那么此时的女同志
吴楚争霸孙武一战封神,伍子胥鞭尸三百春秋时代,礼崩乐坏,天下动荡不安,诸侯互相征伐,实力为王是这个时代显明的特征。就拿周室来说,公元前520年,周景王去世,由于他生前特别宠爱庶子姬朝,就嘱咐心腹在自己死后拥护王子朝继
诸葛亮临死前留下一卧底,害死蜀汉两大奇才,为蜀汉延续30年国祚侍中侍郎郭攸之费祎董允等,此皆良实,志虑忠纯。出师表在三国的历史上,一吕二赵三典韦四关五马刘张飞,以及曹操刘备孙权和诸葛亮等皆是历史上有名知人,谈到费祎是谁很多人根本不知道他的存在
横城反击战美国史学界的伤疤,志愿军歼灭美军最多的一战1950年6月25日朝鲜战争爆发,10月19日第38军率先跨过鸭绿江进入朝鲜境内,抗美援朝战争正式打响,一场举世瞩目的大战一触即发。在整个抗美援朝战争中,有许多给后世留下深刻印象的