{"id":403458,"date":"2024-06-29T17:08:22","date_gmt":"2024-06-29T17:08:22","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=403458"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=403458","title":{"rendered":"<span>Millions of orders per second matching engine testing<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p> I had some experience in the matching engine development for cryptocurrency exchange some time ago. That was an interesting and challenging experience. I developed it in clear C++ from scratch. The testing of it is also quite a challenging task. You need to get data for testing, perform testing, collect some statistics, and at last, analyze collected data to find weak points and bottlenecks. I want to focus on testing the C++ matching engine and show how testing can give insights for optimizations even without the need to change the code. The matching engine I developed can do more than 1\u2019000\u2019000 TPS (transactions per second) and is 10x times faster than the matching engine of the Binance cryptocurrency exchange (see one <a href=\"https:\/\/www.binance.com\/en\/blog\/421499824684900642\/Binance-Futures-For-Institutions--Pioneering-Performance--Technology\" rel=\"noopener noreferrer nofollow\"><u>post<\/u><\/a> on Binance Blog).<\/p>\n<h3> What is the matching engine?<\/h3>\n<p> The matching engine is the heart of the cryptocurrency exchange as well as for other trading systems like stock exchanges. It is a point where the interests of buyers and the interests of sellers find each other and trades occur as a result of order matching. The matching engine holds a limit order book. It is the set of orders that haven&#8217;t matched yet. Here is a simplified block diagram of exchange.  <\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/586\/a74\/24d\/586a7424d60ad578cce8371f2afcffab.png\" width=\"1216\" height=\"565\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/586\/a74\/24d\/586a7424d60ad578cce8371f2afcffab.png\"\/><figcaption><\/figcaption><\/figure>\n<p> Fundamentally the matching process for one currency pair or other trading instruments is strictly sequential. Requests that came from users or API clients can be processed concurrently before they reach the matching engine. That is why the matching engine is the bottleneck for the throughput of the exchange. And the number of requests that the exchange can process per second is limited by the number of requests that the matching engine can process per second.<\/p>\n<h3> Testing data<\/h3>\n<p> To do testing we need some data. I want to be free with setting data parameters. So I developed the data generator which produces requests for the matching engine based on statistical models. Test data sample consists of 5\u2019000\u2019000 requests. Price is generated as a Markov chain stochastic model with Pareto distribution. This model produces a quite natural price chart.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/56a\/d4f\/f58\/56ad4ff58af67e6c900b20fbb766ee8e.png\" width=\"1266\" height=\"446\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/56a\/d4f\/f58\/56ad4ff58af67e6c900b20fbb766ee8e.png\"\/><figcaption><\/figcaption><\/figure>\n<p> The starting price is 60\u2019000 and the price step is 1. The orders are generated to follow the price and to get desired target values such as the number of orders in the limit order book, a proportion of the orders of different types, and etc. The average size of the limit order book for the test data is 10\u2019000 and the average number of active stop orders is 1\u2019000. And proportions of the different type orders are the next.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/703\/a8b\/be1\/703a8bbe12332377bff2438556d546a5.png\" width=\"640\" height=\"480\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/703\/a8b\/be1\/703a8bbe12332377bff2438556d546a5.png\"\/><figcaption><\/figcaption><\/figure>\n<p> There is the next meaning of the order types. The Market orders are the orders to buy or sell at the best market price. The Limit order is the order to buy or sell at the defined limit price and that does not cross the limit order book instantly and does not produce trades in response. The Limit taker order is the limit order that crosses the limit order book and produces trades. The Stop order has a stop price and triggers when the current market price crosses the stop price. The Stop order converted to Market order when triggered. The StopLimit order is the stop order that is converted to Limit order on triggering. The ModifyLimit, ModifyStop, ModifyStopLimit orders are the orders to change price or amount in correspondent orders that are held by the matching engine. The CancelLimit, CancelStop, CancelStopLimit orders are orders to cancel correspondent orders.<\/p>\n<h3> Latency measurement<\/h3>\n<p> To measure throughput I save the time points for each request when it enters the matching engine. The latency for processing one request was calculated as the difference of two such consecutive time points. I run the test on the AMD Ryzen 9 3900X processor mounted on the Prime X570-PRO motherboard. This is a histogram for the latencies.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/676\/094\/83a\/67609483a09c05c2f1f83d5ecabb76b8.png\" width=\"743\" height=\"480\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/676\/094\/83a\/67609483a09c05c2f1f83d5ecabb76b8.png\"\/><figcaption><\/figcaption><\/figure>\n<p> The average latency is 442 nanoseconds per order and this corresponds to about 2 million orders per second throughput. One request can produce zero or multiple trades. It is interesting to see how latency depends on the number of trades triggered by one request. And here is the chart.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/b32\/e0f\/020\/b32e0f020b9e7887b708ed3222c41a91.png\" width=\"559\" height=\"453\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/b32\/e0f\/020\/b32e0f020b9e7887b708ed3222c41a91.png\"\/><figcaption><\/figcaption><\/figure>\n<p> As we can see there is linear dependency and to process a request there are additional 220 nanoseconds needed per one additional trade. The total amount of trades done during this test is 1.7 million trades. There is another interesting chart that shows latencies for each type of order. The only requests which produce zero trades were taken into account for this chart.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/641\/e65\/de1\/641e65de1f922e6fd062481a7beafb5c.png\" width=\"743\" height=\"608\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/641\/e65\/de1\/641e65de1f922e6fd062481a7beafb5c.png\"\/><figcaption><\/figcaption><\/figure>\n<h3> Stress testing<\/h3>\n<p> It is time to do some stress testing. Let\u2019s 10 times increase the average number of orders in the Limit Order Book. And let\u2019s denote this test as Test B (the first test will be Test A). The average size of the limit order book for the test B data is 100\u2019000 and the average number of active stop orders is 10\u2019000. Let\u2019s look at the histogram for the latencies.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/1a1\/1ff\/010\/1a11ff01073ecdafd55c20de747ac410.png\" width=\"684\" height=\"480\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/1a1\/1ff\/010\/1a11ff01073ecdafd55c20de747ac410.png\"\/><figcaption><\/figcaption><\/figure>\n<p> As we can see the throughput was 4 times lower and is about 500\u2019000 orders per second. Let\u2019s examine what is the reason for such a dramatic speed down. Here is the chart for latency depends on the number of trades triggered by one request.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/f22\/fd0\/8a2\/f22fd08a268ad3fe0b35aa0d052a5499.png\" width=\"640\" height=\"480\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/f22\/fd0\/8a2\/f22fd08a268ad3fe0b35aa0d052a5499.png\"\/><figcaption><\/figcaption><\/figure>\n<p> There are additional 350 nanoseconds needed per one additional trade to process a request. It is 61% bigger than in Test A. Also, there are latencies for requests with zero trades increased by 270% and the standard deviation for these latencies is much bigger (see error bar on the chart). So, it is some problem with requests that produce zero trades. Let\u2019s look at latencies for each type of order for these requests.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/da6\/674\/2b2\/da66742b25a481cc01db6a21ad164e11.png\" width=\"773\" height=\"483\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/da6\/674\/2b2\/da66742b25a481cc01db6a21ad164e11.png\"\/><figcaption><\/figcaption><\/figure>\n<p> And here we can see that latencies for the ModifyLimit and the CancelLimit orders were increased more than 5 times. This is a useful hint for optimization. Processing the orders that share the same price in the Order Book is different for the ModifyLimit and the CancelLimit orders. This part should be optimized by choosing better data structures and algorithms. To prove the relation of performance drop with a number of orders that share the same price in the Order Book I plot the next chart. It shows how latencies for the ModifyLimit and the CancelLimit requests depend on the number of the orders in the Limit Order Book with the same price.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/79a\/6db\/add\/79a6dbadd9fe8af64d1d24e426c71d05.png\" width=\"821\" height=\"661\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/79a\/6db\/add\/79a6dbadd9fe8af64d1d24e426c71d05.png\"\/><figcaption><\/figcaption><\/figure>\n<p> This chart shows that to cancel the order on price X there are about 1000 nanoseconds needed if the Order Book has only one order with price X. And there about 10\u2019000 nanoseconds needed if the Order Book has 1000 orders with price X. This performance issue can be fixed even without changing the matching engine. I prepared data for Test C by changing only one parameter of the Test B data. I lowered the price step 10 times and use 0.1 value. By lowering the price step the number of orders with the same price will proportionally be lowered. The next chart shows the latencies histogram for Test C.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/cc7\/46d\/e09\/cc746de09e327083053f15884c28a8de.png\" width=\"681\" height=\"480\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/cc7\/46d\/e09\/cc746de09e327083053f15884c28a8de.png\"\/><figcaption><\/figcaption><\/figure>\n<p> As we can see, adjusting one parameter increases the performance 3 times.<\/p>\n<h3> Conclusion<\/h3>\n<p> The matching engine can process more than 1 million requests per second even with large order books with 100\u2019000 limit orders. These are quite impressive numbers and I would like to see such numbers on the websites of the top cryptocurrency exchanges.<\/p>\n<div class=\"persona\" persona=\"true\"><img decoding=\"async\" persona=\"true\" class=\"image persona__image\" src=\"https:\/\/habrastorage.org\/r\/w780q1\/getpro\/habr\/upload_files\/7ed\/868\/b3c\/7ed868b3c726e994ec5ad1aa21438f98.jpg\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/7ed\/868\/b3c\/7ed868b3c726e994ec5ad1aa21438f98.jpg\" data-blurred=\"true\"\/><\/p>\n<h5 class=\"persona__heading\" persona=\"true\">Oleksii Zaderykhin<\/h5>\n<p persona=\"true\" class=\"persona__text\">Senior Software Engineer<\/p>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/581170\/\"> https:\/\/habr.com\/ru\/articles\/581170\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p> I had some experience in the matching engine development for cryptocurrency exchange some time ago. That was an interesting and challenging experience. I developed it in clear C++ from scratch. The testing of it is also quite a challenging task. You need to get data for testing, perform testing, collect some statistics, and at last, analyze collected data to find weak points and bottlenecks. I want to focus on testing the C++ matching engine and show how testing can give insights for optimizations even without the need to change the code. The matching engine I developed can do more than 1\u2019000\u2019000 TPS (transactions per second) and is 10x times faster than the matching engine of the Binance cryptocurrency exchange (see one <a href=\"https:\/\/www.binance.com\/en\/blog\/421499824684900642\/Binance-Futures-For-Institutions--Pioneering-Performance--Technology\" rel=\"noopener noreferrer nofollow\"><u>post<\/u><\/a> on Binance Blog).<\/p>\n<h3> What is the matching engine?<\/h3>\n<p> The matching engine is the heart of the cryptocurrency exchange as well as for other trading systems like stock exchanges. It is a point where the interests of buyers and the interests of sellers find each other and trades occur as a result of order matching. The matching engine holds a limit order book. It is the set of orders that haven&#8217;t matched yet. Here is a simplified block diagram of exchange.  <\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p> Fundamentally the matching process for one currency pair or other trading instruments is strictly sequential. Requests that came from users or API clients can be processed concurrently before they reach the matching engine. That is why the matching engine is the bottleneck for the throughput of the exchange. And the number of requests that the exchange can process per second is limited by the number of requests that the matching engine can process per second.<\/p>\n<h3> Testing data<\/h3>\n<p> To do testing we need some data. I want to be free with setting data parameters. So I developed the data generator which produces requests for the matching engine based on statistical models. Test data sample consists of 5\u2019000\u2019000 requests. Price is generated as a Markov chain stochastic model with Pareto distribution. This model produces a quite natural price chart.<\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p> The starting price is 60\u2019000 and the price step is 1. The orders are generated to follow the price and to get desired target values such as the number of orders in the limit order book, a proportion of the orders of different types, and etc. The average size of the limit order book for the test data is 10\u2019000 and the average number of active stop orders is 1\u2019000. And proportions of the different type orders are the next.<\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p> There is the next meaning of the order types. The Market orders are the orders to buy or sell at the best market price. The Limit order is the order to buy or sell at the defined limit price and that does not cross the limit order book instantly and does not produce trades in response. The Limit taker order is the limit order that crosses the limit order book and produces trades. The Stop order has a stop price and triggers when the current market price crosses the stop price. The Stop order converted to Market order when triggered. The StopLimit order is the stop order that is converted to Limit order on triggering. The ModifyLimit, ModifyStop, ModifyStopLimit orders are the orders to change price or amount in correspondent orders that are held by the matching engine. The CancelLimit, CancelStop, CancelStopLimit orders are orders to cancel correspondent orders.<\/p>\n<h3> Latency measurement<\/h3>\n<p> To measure throughput I save the time points for each request when it enters the matching engine. The latency for processing one request was calculated as the difference of two such consecutive time points. I run the test on the AMD Ryzen 9 3900X processor mounted on the Prime X570-PRO motherboard. This is a histogram for the latencies.<\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p> The average latency is 442 nanoseconds per order and this corresponds to about 2 million orders per second throughput. One request can produce zero or multiple trades. It is interesting to see how latency depends on the number of trades triggered by one request. And here is the chart.<\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p> As we can see there is linear dependency and to process a request there are additional 220 nanoseconds needed per one additional trade. The total amount of trades done during this test is 1.7 million trades. There is another interesting chart that shows latencies for each type of order. The only requests which produce zero trades were taken into account for this chart.<\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<h3> Stress testing<\/h3>\n<p> It is time to do some stress testing. Let\u2019s 10 times increase the average number of orders in the Limit Order Book. And let\u2019s denote this test as Test B (the first test will be Test A). The average size of the limit order book for the test B data is 100\u2019000 and the average number of active stop orders is 10\u2019000. Let\u2019s look at the histogram for the latencies.<\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p> As we can see the throughput was 4 times lower and is about 500\u2019000 orders per second. Let\u2019s examine what is the reason for such a dramatic speed down. Here is the chart for latency depends on the number of trades triggered by one request.<\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p> There are additional 350 nanoseconds needed per one additional trade to process a request. It is 61% bigger than in Test A. Also, there are latencies for requests with zero trades increased by 270% and the standard deviation for these latencies is much bigger (see error bar on the chart). So, it is some problem with requests that produce zero trades. Let\u2019s look at latencies for each type of order for these requests.<\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p> And here we can see that latencies for the ModifyLimit and the CancelLimit orders were increased more than 5 times. This is a useful hint for optimization. Processing the orders that share the same price in the Order Book is different for the ModifyLimit and the CancelLimit orders. This part should be optimized by choosing better data structures and algorithms. To prove the relation of performance drop with a number of orders that share the same price in the Order Book I plot the next chart. It shows how latencies for the ModifyLimit and the CancelLimit requests depend on the number of the orders in the Limit Order Book with the same price.<\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p> This chart shows that to cancel the order on price X there are about 1000 nanoseconds needed if the Order Book has only one order with price X. And there about 10\u2019000 nanoseconds needed if the Order Book has 1000 orders with price X. This performance issue can be fixed even without changing the matching engine. I prepared data for Test C by changing only one parameter of the Test B data. I lowered the price step 10 times and use 0.1 value. By lowering the price step the number of orders with the same price will proportionally be lowered. The next chart shows the latencies histogram for Test C.<\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p> As we can see, adjusting one parameter increases the performance 3 times.<\/p>\n<h3> Conclusion<\/h3>\n<p> The matching engine can process more than 1 million requests per second even with large order books with 100\u2019000 limit orders. These are quite impressive numbers and I would like to see such numbers on the websites of the top cryptocurrency exchanges.<\/p>\n<div class=\"persona\" persona=\"true\">\n<h5 class=\"persona__heading\" persona=\"true\">Oleksii Zaderykhin<\/h5>\n<p persona=\"true\" class=\"persona__text\">Senior Software Engineer<\/p>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/581170\/\"> https:\/\/habr.com\/ru\/articles\/581170\/<\/a><br \/><\/br><\/br><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-403458","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/403458","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=403458"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/403458\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=403458"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=403458"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=403458"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}