Triangular Arbitrage in Cryptocurrency Trading: A Weekend Project
Alright, alright, alright! I wanted to write about this project for a long time, but it is so unrelated and random that I didn’t know if it would fit any topic. Oh well, it is a code and weekend project, so I guess it can relate to most of the programmers out there.
It was around 2015, when there was a second wave of cryptocurrency hype. The whole concept was rebellious and new. I was a postdoc at the chemical engineering department at the University of Utah at that time, working on CFD (computational fluid dynamics), CUDA, and GPUs. I was also an active crypto trader after hours and on weekends.
While I was trading, I noticed something: an instant imbalance in different markets. I didn’t know the term at that time, but it is called triangular arbitrage. The idea is simple: if you have three different currencies, you can exchange them in a loop and make a profit if the rates are not aligned. For example, if you have USD, BTC, and ETH, you can exchange USD to BTC, then BTC to ETH, and finally ETH back to USD. If the rates are not aligned properly, you can end up with more USD than you started with. But how does this happen? In order to understand this, you need to know how the order book works.
Order book
Every market on an exchange — BTC/USD, ETH/BTC, ETH/USD — is really just an order book: a live list of everyone willing to buy and everyone willing to sell, sorted by price. The buy orders are called bids, the sell orders are called asks. The highest bid and the lowest ask meet in the middle, and the small gap between them is the spread. When you place a market order, you don’t get one magical “current price” — your order walks through the book, filling against the best resting orders first, then the next best, and so on.
The key insight for this story: the order book is not static. It is a living thing that updates many times per second, and each market has its own book. When a whale drops a large market buy on BTC/USD, it eats through several ask levels and shoves the BTC price up in that book instantly — but the ETH/BTC and ETH/USD books don’t know about it yet. For a brief moment, the three markets disagree about what a bitcoin is worth. That disagreement is the imbalance I kept noticing on my screen.
Maker vs. Taker — the two sides of every trade
- Maker — someone who places a limit order that doesn’t fill immediately and instead rests in the order book, “making” liquidity for others to trade against. Makers set the prices you see at each level of the book.
- Taker — someone who places an order that fills immediately against those resting orders (e.g. a market order), “taking” liquidity out of the book. The whale sweeping the asks in Fig. 1 is a taker.
Algorithm
So, a sudden imbalance in one or more market order books can create an opportunity for triangular arbitrage.
One thing a textbook diagram hides: on a real exchange you don’t get to pick the direction of a pair. There is no USD/ETH or BTC/ETH market — the exchange lists ETH/BTC, ETH/USD, and BTC/USD, each quoting the price of one unit of the base currency. So on some legs of the triangle you sell the base currency (multiply by the price) and on others you buy it (divide by the price).
Let’s start with x amount of BTC (Bitcoin). In the ETH/BTC market you spend the BTC to buy ETH, so you divide by the price:
Then, you sell the y amount of ETH for USD in the ETH/USD market — here you are selling the base, so you multiply:
Finally, you take the z amount of USD and buy BTC back in the BTC/USD market — buying the base again, so you divide:
Chaining the three legs together — and paying the trading fee on every one of them — collapses the whole cycle into a single number, the cycle ratio R, so that w = x × R:
That one number is the go/no-go signal, and it directly gives the profit as a percentage:
In practice, R > 1 alone is not enough to pull the trigger. Between computing R and the three orders actually filling, prices move, orders partially fill, and rounding eats a little more. So the bot only trades when the edge clears a safety margin α (alpha):
With, say, α = 0.001 (0.1%), a cycle at R = 1.0005 is technically profitable but not worth the execution risk, while R = 1.0034 clears the bar comfortably. Try it below — the verdict compares the cycle ratio against 1 + α.
Each square below is a market on the exchange (its current price and fee inside); each arrow is money moving between markets, colored by the currency it carries — BTC, ETH, USD. Set the three prices (or pick a preset), then press Run the cycle and watch the token carry the money around the loop.
α, then run the cycle: 0.001 BTC enters the ETH/BTC market and the amount leaving each market is written on its outgoing edge, until it comes back as w, giving R = w / x. The verdict applies the trigger rule R ≥ 1 + α: aligned markets is a guaranteed loss once fees are charged (R ≤ 1), thin edge is profitable on paper but below α so the bot stands down, and sudden imbalance clears the bar and trades. The starting amount of 0.001 BTC is just for illustration; the code I developed traded either the exchange’s minimum allowable order size at the time, or a fraction of the smallest amount actually available across the three currencies in the order-book gap.If your calculations and timing are correct, you could end up with w BTC > x BTC, which means you made a profit. Usually the profit is very small, a fraction of a percent of x, but by having this code running 24/7 or on multiple markets, you can make a reasonable profit. Plus, the iteration time between the idea and an MVP looked very small — I could do it in a weekend!
Now imagine there are many, many different currencies and markets within one exchange (e.g. Coinbase) that you can run this algorithm on. The more currencies and markets you have, the more opportunities for triangular arbitrage you can find.
Easy, right? The algorithm and calculation are simple, the implementation seems easy… well, not so fast.
Implementation
In my case, I was working with the Bittrex exchange, where there were hundreds of different currencies and markets. Also, it had very comprehensive API documentation.
First iteration
I used the REST API to get the order book data and also called the POST method to place orders. It was a simple script.
The trading engine was also in Python, and I was using asyncio to run the script in a loop, where it would get the order book data, calculate the triangular arbitrage opportunities, and place orders if there was a profitable opportunity.
After running this script, I realized the time between the trading opportunity and the order placement was too long, and by the time the initial order was placed — in this example, BTC → ETH — the opportunity was gone.
The time lag was around 0.025 seconds!
The 0.025 seconds may not seem like a lot, but in that world, it is a lifetime!
Second iteration
The problem was the REST API and the time it took to get the order book data. Using the REST API, I was running the API call in a loop, and it was constantly asking the server for the order book, even when nothing changed. Most servers put a limit on the number of requests you can make in a certain time frame, and if you exceed that limit, they do not respond to your API call. So, I was constantly hitting the limit and getting blocked.
The solution was to use the WebSocket API, which is a more efficient way to get real-time data from the exchange. With the WebSocket API, you can subscribe to the order book data and get updates whenever there is a change, instead of constantly asking for the data. So, now I was able to get the order book data only when something changed in the order book.
This reduced the time lag to around 0.015 seconds — a huge improvement. But still not enough, my code couldn’t capture the opportunity!
429. A WebSocket subscribes once, and the exchange pushes an update the instant the book changes. The bar under each lane is the measured lag, drawn to the same scale.Third iteration
I was running this code on my laptop on my home network, in Salt Lake City, Utah. The exchange server was in New York or the Bay Area (my initial guesses), so it takes a while for data and API requests to travel from my laptop to the exchange server and back.
Solution: finding the best performing server on the cloud. I had GCP at that time, with $200 credit (thank you Google!). The code did not require a lot of resources (memory or CPU), so I kept creating compute instances all across the US and timing the code to find the best performing server, and somewhere in the middle of the US, I found a server that was able to reduce the time lag to around 0.008 seconds! Beautiful, I cried!
0.008 seconds.There was still some issue: by the time the first order was placed and then the third order, the opportunity was gone.
Fourth iteration
What I was doing was starting with x amount of BTC, changing it to y amount of ETH, then to z amount of USD, and finally back to w amount of BTC.
x BTC -> ETH/BTC -> y ETH -> ETH/USD -> z USD -> BTC/USD -> w BTC
In each step, I was waiting for the order to complete before moving on to the next step. And to make matters worse, I was confirming the order status by doing another API call to the exchange server, which was adding more time lag. So, although my code could capture the first or even the second order correctly, it couldn’t finish the third order (complete the cycle) before the opportunity was gone.
An improvement was to run these three orders in parallel:
x BTC -> ETH/BTC -> y ETH
y ETH -> ETH/USD -> z USD
z USD -> BTC/USD -> w BTC
Now, instead of waiting for each order to complete, confirming the status, and then moving on to the next order, all three orders were placed simultaneously. This significantly reduced the time lag and improved the chances of capturing the opportunity.
This approach had a couple of downsides:
- You must have enough liquidity in every currency. Meaning, I had to have enough BTC, ETH, and USD in my account to place all three orders at the same time.
- These three orders will execute anyway, regardless of capturing the opportunity or not. So, it means that when we couldn’t capture the opportunity, we paid 3 trading fees anyway.
- I had to add another engine to monitor the wallet amounts and make sure that there was enough liquidity in each wallet to place the orders.
Time for “Production”
I put the code into production, and it ran 24/7 for about a week. The majority of the transactions resulted in a loss; the ones that didn’t it was pure luck.
Obviously, this was a well-known trading strategy, and many other people with more resources and, most importantly, much lower latency, were doing the same thing. My code was simply too slow to capture the opportunities.
The code was running on the VMs for a week on more than 10 traingular arbitrage opportunities, and after processing more than half a million dollars’ worth of transactions with losing a couple hundred dollars, I just stopped the code!
Wrapping Up
I liked this project. When I started doing it, I didn’t know the name of the approach, and I had to explain to people the algorithm I was working on, which turned out to be very hard to describe, especially to people who had no background in trading, order books, or exchange markets.
However, thanks to the great, magical AI, I just described what I was doing in one paragraph, and the answer was Triangular Arbitrage — a very descriptive name. I should have known!
Regardless of the fact that it ultimately resulted in a financial loss, I still liked this project because:
- Detect an opportunity — I was not a trader by trade! I just observed the market and identified potential opportunities.
- Act — The most important virtue is to act on your idea. Just think about the minimum viable implementation and iterate from there. I didn’t worry about the best possible implementation at first; I just started and fixed issues along the way. Indeed, now that I know what I didn’t know back then, if I were to start today, I would start with a better and more optimized implementation.
- Iterate — Every solution can be improved. As my friend at NVIDIA says, “If it is not as fast as the speed of light, it is not fast enough.” As a code developer, I have always liked the “profiling” phase, where I analyze the performance of my code and look for bottlenecks to optimize.
It was not a successful project in terms of financial gain. There is a statistic that says 99% of startups fail. I like to think about this project as a weekend-developed, failed startup that took me deep into the world of trading algorithms and market dynamics.
A wise man once said: “You just have to win once!”
New Day, New Idea!
One of the main sources of the losses was the “transaction fee” charged by the exchange.
There was, however, a way to avoid paying that fee. At that time, in 2015, Coinbase did not charge transaction fees to “Makers” (see the Maker vs. Taker note back in the Order book section).
That gave me a couple of new ideas for designing another trading algorithm, which, if I have time, I’ll write about in another blog post.
