Atomic Arbitrage: A Quantitative Study

Crypto Chassis
Open Crypto Trading Initiative
8 min readOct 24, 2022
Photo by GR Stocks on Unsplash

Greetings, Ladies and Gentlemen! We are so glad to meet you again. It has been a long time since the last time we wrote on medium. We have been working very hard on the development of our library ccapi (https://github.com/crypto-chassis/ccapi) and our data service (https://github.com/crypto-chassis/cryptochassis-data-api-docs). We hope that our hard work can bring world-class high frequency trading tools and technologies to the open source community where ideas and discussions flow and flourish without constraints. For the past several months, one thing that we have been focusing on was improving the performance of our library using battlefield trading on small amount of capitals. From comprehensive experiments, we discovered many interesting aspects and phenomena related to high frequency order executions. Today we’d like to share our knowledge on an exciting and familiar topic from a quantitative perspective: atomic arbitrage.

Atomic arbitrage refers to a class of trading strategy in which a buy order on one instrument and a sell order on another equivalent instrument with the same order quantities are (almost) simultaneously filled. The two instruments can live on the same exchange or on two different exchanges, as long as they hedge against each other. Before the trades happen, the trader holds pure cash. After the trades happen, the trader holds a long position in the first instrument, a short position in the second instrument, and some remaining cash. If the trades are executed with the buy price lower than the sell price, the trader books a profit and the arbitrage is considered successful. On the other hand, if the trades are executed with the buy price higher than the sell price, the trader books a loss and the arbitrage is considered unsuccessful. There are many factors to consider when designing the details of such a strategy. Let us mention some important ones here: [1] Remember to take transaction fees into account. [2] If the instruments involve spot margin and/or perpetual contract, remember to take interest payments and funding payments into account. [3] No two persons are exactly alike. No two instruments hedge exactly against each other. The deviation from “exactness” might be small, but non-negligible. For example, SOL/USD and SOL-PERP on FTX have almost exactly the same price since their prices peg and follow each other. But a careful trader will take the small difference into account. There are several ways to handle this. We’ll leave it as an open question regarding how to incorporate the small difference into your pricing equation. If you are interested in knowing about our approach, feel free to contact us. [4] Assume the trades are executed with the buy price lower than the sell price. When the trader holds a long position in the first instrument and a short position in the second instrument, the profit generated from the buy/sell price difference is only theoretical, and is not materialized until later on the trader unwinds both positions and gets back to pure cash. This is a very critical step in making the whole process successful and sustainable. There are several ways to handle this. We’ll leave it as an open question regarding how to wisely unwind positions. If you are interested in knowing about our approach, feel free to contact us. [5] The trader’s order execution speed has to be faster than his or her peer competitors so that the trader gets the best execution prices, i.e. the largest buy/sell price difference. This is the focal point that we will discuss.

Over the past several months, we carefully performed a series of live trading experiments using one variant of the atomic arbitrage: the maker-taker arbitrage. Let us explain its details using SOL/USD and SOL-PERP on FTX as an example. To start, place a buy order on SOL/USD’s order book such that the order’s limit price is lower than SOL-PERP’s best bid price. Cancel and replace the order as needed. This first order is the maker order in the maker-taker arbitrage. At some point in time when this order gets filled to enter a long position in SOL/USD, immediately place a sell order to SOL-PERP with the same order quantity as the maker order’s filled quantity to enter a short position in SOL-PERP. This second order is the taker order in the maker-taker arbitrage. If the sell price is higher than the buy price, we may consider the trades as successful. A concrete example taken from the battlefield looks like this:

[2022-10-20T21:03:59.468482001Z] INFO:SOL:Private trade - pairName: SOL, exchangeName: ftx, instrumentName: SOL/USD, orderId: 192001508321, clientOrderId: 1666299657474, side: BUY, price: 28.02, quantity: 0.45, isMaker: 1
[2022-10-20T21:03:59.668631075Z] INFO:SOL:Private trade - pairName: SOL, exchangeName: ftx, instrumentName: SOL-PERP, orderId: 192001550395, clientOrderId: 1666299657485, side: SELL, price: 28.06, quantity: 0.45, isMaker: 0

Let us digest the details of what happened. Around UTC time 2022–10–20T21:03:59Z (note that the timestamps shown here are our machine timestamp rather than the exchange server timestamp), our trading system bought 0.45 SOL/USD at $28.02 with a maker order, then at lightning speed it sold 0.45 SOL-PERP at $28.06 with a taker order. The buy/sell price difference is about 14 bps. It is easy said than done: we worked very hard for several months before our trading system becomes capable of consistently producing such results for a variety of instruments. Immediately a careful trader will have a plethora of questions to ask. A few prominent ones are: [1] Is our trading system fast enough compared to our competitors? If not, how much faster do we have to become in order to beat them? [2] How will the trades look like if we increase the order quantity? [3] Given the same market conditions, is it possible to fill the sell order at an even higher price and/or the buy order at an even lower price? In the following discussions we will showcase how to use a quantitative approach to answer the first question and leave the second and the third questions to our readers as an exercise.

In order to answer the first question, we’d have to identify the footprints of our competitors only using publicly available information. Remember that our event happened around UTC time 2022–10–20T21:03:59Z, so let us obtain and analyze the public trade records for FTX’s SOL-PERP around that time. To obtain the data, you can directly query FTX at https://docs.ftx.com/#fills, or choose any crypto market data provider such as Kaiko, or simply use our free data service by visiting https://api.cryptochassis.com/v1/trade/ftx/sol-perp?startTime=2022-10-20 to download a gzipped csv file containing all the public trade records for FTX’s SOL-PERP on 2022–10–20. The csv file contains four columns: time_seconds is the exchange-server-recorded unix timestamp for exactly when the trades happened. price is the trade price. size is the trade size. is_buyer_maker is whether the buyer was a maker or a taker (One of our previous publications entitled “Leveraging Trade Directions: A Quantitative Journey, Part IV” provided detailed discussions about the column is_buyer_maker). Let us try to read the first record in the file:

time_seconds,price,size,is_buyer_maker
1666224002.092393,28.845,1.73,0

The first record tells us that at unix time exactly 1666224002.092393 seconds a trade happened at a price of $28.845 with a size of 1.73 SOL-PERP in which the buyer was a taker. Our event happened around UTC time 2022–10–20T21:03:59Z which translates to a unix time of 1666299839 seconds. By searching the string “1666299839” in the csv file and inspecting the trade records around that time, we are able to identify the footprints of our competitors and recreate the picture of what happened at that moment. To help our reader to visualize the analysis, we have added certain annotations and used bold fonts to highlight where the footprints of our own trade can be found.

time_seconds,price,size,is_buyer_maker
1666299839.411387,28.0875,100,1 <---- Competitor 1
1666299839.411387,28.085,17.84,1
1666299839.411387,28.0825,100,1
1666299839.411387,28.08,0.02,1
1666299839.411387,28.08,0.6,1
1666299839.411387,28.08,86.43,1
1666299839.411387,28.0775,3.56,1
1666299839.411387,28.075,0.01,1
1666299839.411387,28.075,1.11,1
1666299839.411387,28.075,10.64,1
1666299839.411387,28.075,86.43,1
1666299839.411387,28.075,21.54,1
1666299839.411387,28.075,138.57,1
1666299839.411387,28.0725,1,1
1666299839.411387,28.0725,0.75,1
1666299839.411387,28.0725,4.88,1
1666299839.411387,28.0725,106.81,1
1666299839.411387,28.0725,17.5,1
1666299839.411387,28.0725,208.83,1
1666299839.411387,28.0725,127.68,1
1666299839.411387,28.0725,113.98,1
1666299839.411387,28.0725,27.73,1
1666299839.411387,28.07,0.02,1
1666299839.411387,28.07,0.5,1
1666299839.411387,28.07,0.04,1
1666299839.411387,28.07,0.7,1
1666299839.411387,28.07,0.02,1
1666299839.411387,28.07,1,1
1666299839.411387,28.07,0.02,1
1666299839.411387,28.07,86.43,1
1666299839.411387,28.07,97.97,1
1666299839.411387,28.07,97.97,1
1666299839.411387,28.07,97.97,1
1666299839.411387,28.07,0.07,1
1666299839.411387,28.07,167.03,1
1666299839.411387,28.07,97.97,1
1666299839.411387,28.07,97.97,1
1666299839.411387,28.0675,183.11,1
1666299839.411387,28.0675,97.98,1
1666299839.411387,28.0675,97.98,1
1666299839.411387,28.0675,10,1
1666299839.411387,28.0675,97.97,1
1666299839.411387,28.065,37,1
1666299839.411387,28.065,1,1
1666299839.411387,28.065,0.01,1
1666299839.411387,28.065,0.2,1
1666299839.411387,28.065,0.01,1
1666299839.411387,28.065,1.15,1
1666299839.411387,28.065,0.1,1
1666299839.411387,28.065,86.43,1
1666299839.411387,28.065,186.64,1
1666299839.411387,28.065,13.89,1
1666299839.411387,28.065,225.63,1
1666299839.411387,28.065,116.1,1
1666299839.411387,28.065,97.99,1
1666299839.411387,28.065,97.99,1
1666299839.411387,28.065,57.12,1
1666299839.411387,28.065,107.98,1
1666299839.411387,28.065,114.01,1
1666299839.411387,28.065,30,1
1666299839.411387,28.0625,1.15,1
1666299839.411387,28.0625,71.19,1
1666299839.411387,28.0625,94.96,1
1666299839.411387,28.0625,10.75,1
1666299839.411387,28.0625,213.78,1
1666299839.411387,28.0625,213.79,1
1666299839.411387,28.0625,35.55,1 <---- Competitor 1
1666299839.414857,28.06,0.45,1 <---- CryptoChassis
1666299839.422782,28.06,0.05,1 <---- Competitor 2
1666299839.422782,28.06,0.12,1
1666299839.422782,28.06,0.02,1
1666299839.422782,28.06,1,1
1666299839.422782,28.06,20.21,1 <---- Competitor 2
1666299839.42378,28.06,21.4,1 <---- Competitor 3
1666299839.424914,28.06,21.4,1 <---- Competitor 4
1666299839.425297,28.06,21.4,1 <---- Competitor 5
1666299839.425342,28.06,22.5,1 <---- Competitor 6
1666299839.42539,28.06,21.4,1 <---- Competitor 7
1666299839.427256,28.06,21.3,1 <---- Competitor 8
1666299839.428883,28.06,181.76,1 <---- Competitor 9
1666299839.429493,28.06,23.6,1 <---- Competitor 10
1666299839.431774,28.06,18.6,1 <---- Competitor 11
1666299839.431835,28.06,126.43,1 <---- Competitor 12
1666299839.431835,28.06,0.2,1
1666299839.431835,28.06,35.59,1
1666299839.431835,28.06,0.1,1 <---- Competitor 12
1666299839.432734,28.0575,0.01,1 <---- Competitor 13
1666299839.432734,28.0575,0.01,1
1666299839.432734,28.0575,0.01,1
1666299839.432734,28.0575,0.01,1
1666299839.432734,28.0575,0.01,1
1666299839.432734,28.0575,8.56,1
1666299839.432734,28.0575,9.63,1
1666299839.432734,28.0575,4.26,1 <---- Competitor 13
1666299839.43672,28.0575,4.31,1 <---- Competitor 14
1666299839.43672,28.0575,19.19,1 <---- Competitor 14
1666299839.440008,28.0575,78.83,1 <---- Competitor 15
1666299839.440008,28.0575,98.01,1
1666299839.440008,28.0575,98.01,1
1666299839.440008,28.0575,17.65,1 <---- Competitor 15
1666299839.447806,28.055,0.02,1 <---- Competitor 16
1666299839.447806,28.055,0.75,1
1666299839.447806,28.055,0.75,1
1666299839.447806,28.055,0.75,1
1666299839.447806,28.055,0.1,1
1666299839.447806,28.055,0.21,1
1666299839.447806,28.055,18.82,1 <---- Competitor 16
1666299839.455397,28.0575,22.5,1 <---- Competitor 17
1666299839.459634,28.0625,21.62,1 <---- Competitor 18
1666299839.459634,28.0575,14.38,1 <---- Competitor 18
1666299839.464761,28.0575,71.1,1 <---- Competitor 19
1666299839.464761,28.0575,174.93,1 <---- Competitor 19
1666299839.46567,28.0575,267.85,1 <---- Competitor 20
1666299839.468318,28.0575,181.78,1 <---- Competitor 21
1666299839.468775,28.0575,375.44,1 <---- Competitor 22
1666299839.468775,28.055,45.08,1 <---- Competitor 22
1666299839.46897,28.055,337.39,1 <---- Competitor 23
1666299839.472303,28.055,21.4,1 <---- Competitor 24
1666299839.472875,28.0625,10.34,1 <---- Competitor 25
1666299839.472875,28.055,11.06,1 <---- Competitor 25
1666299839.473284,28.055,7.33,1 <---- Competitor 26
1666299839.473284,28.0525,9.63,1
1666299839.473284,28.0525,6.54,1 <---- Competitor 26
1666299839.4747,28.0525,0.95,1 <---- Competitor 27
1666299839.4747,28.0525,7.5,1
1666299839.4747,28.0525,7.06,1
1666299839.4747,28.0525,6.49,1 <---- Competitor 27
1666299839.477564,28.0525,14.7,1 <---- Competitor 28
1666299839.477564,28.05,0.6,1
1666299839.477564,28.05,0.1,1
1666299839.477564,28.05,0.01,1
1666299839.477564,28.05,0.02,1
1666299839.477564,28.05,1,1
1666299839.477564,28.05,5.57,1 <---- Competitor 28
1666299839.47934,28.05,2.43,1 <---- Competitor 29
1666299839.47934,28.05,0.01,1
1666299839.47934,28.05,1.15,1
1666299839.47934,28.05,18.91,1 <---- Competitor 29
1666299839.482507,28.05,23,1 <---- Competitor 30

By realizing that trades sharing the same exchange server timestamp represent one single taker order matching against many different maker orders, we can find out our competitors by simply grouping the exchange server timestamp of the public trade records. Using this method, we have identified approximately 30 competitors who were chasing the same arbitrage opportunities as we did. Now we can answer our first question:”Is our trading system fast enough compared to our competitors? If not, how much faster do we have to become in order to beat them?”Yes and no. We were fast enough to lead Competitor 2 time-wise by 7.9 ms but fell behind Competitor 1 time-wise by 3.4 ms. Interestingly and unfortunately, Competitor 1 is not only faster but also has a very large amount of capital (Jump Trading, Winter Mute, GSR…Make a wild guess.). Let us take a closer look at the public trade records for Competitor 1 and understand to what extent they have eaten way our trade profits. First, if Competitor 1 was absent, our sell price would have been $28.0875 instead of $28.06 and thus a potential 9.8 bps improvement. By being just a few milliseconds faster than us, Competitor 1 was able to get the most juicy part of the meat. This reflects a common phenomenon in high frequency trading: every single millisecond matters a lot. Second, Competitor 1‘s taker order size was very large. Filter out Competitor 1‘s trades, multiply the price column and the size column for each row and sum up the result: Competitor 1 submitted one single order whose value is $118,536. As a result, the price range between $28.0875 and $28.0625 was all taken away by Competitor 1, leaving us only $28.06 and below. So Competitor 1 not only took the most juicy part of the meat, s/he actually took a very large part of the meat. 😄

That’s it. As mentioned before, we’ll make this article succinct and leave the second and the third questions to our readers as an exercise. Hopefully after reading this article about the quantitative approaches that we used to analyze our trade results, you have learned some useful techniques and those techniques can help you to become a more competitive trader. In finance and trading, we always prefer quantitative answers over gut-feelings. How about you? If you are interested in our work or collaborating with us, join us on Discord: https://discord.gg/b5EKcp9s8T and find us on Github: https://github.com/crypto-chassis/ccapi 🎉. We specialize in market data collection, high speed trading system, infrastructure optimization, and proprietary market making.

Disclaimer: This is an educational article rather than investment/financial advice.

--

--