Crypto Arbitrage Trading on Binance

gk_
9 min readJun 9, 2021

--

Exploring the so-called ‘3-way arbitrage’ trading strategy on Binance crypto currencies. Is this hype or is it profitable?

What a concept! Make 3 trades in rapid succession when you find favorable exchange rates and voila! Profits in seconds and no exposure to volatility.

How does this work?

Let’s break this down using a ridiculously simple bartering scenario. When we exchange one crypto-currency for another we are bartering or exchanging fungible assets.

Let’s image the following scenario:

  • Jane has 10 almonds
  • Will has pineapples and will trade each for 5 almonds
  • Christine has mangoes and will trade evenly for a pineapple
  • Xavier has almonds and will trade 6 for each mango

So in this arbitrage opportunity, Jane trades 10 almonds for 2 pineapples, and these for 2 mangoes which then she trades for 12 almonds.

She has profited 2 almonds through these trades because of anomalies in the exchanges.

Above is the same type of 3-way arbitrage with crypto currencies.

What at first appears to be simple often is often not.

A few important things to note here in the real-world of crypto markets:

  • price discrepancies between markets are anomalies, they have to be sniffed out deliberately
  • once an arbitrage opportunity is found it must be executed very quickly or you’ll be left with an incomplete execution (1 or 2 trades rather than 3)
  • the trades must be done as a Limit-Order at the specific price identified in the arbitrage exploration (we’ll try this out in a bit)
  • transaction fees will quickly erode the profitability of these trades (we’ll examine this directly in our code)

There’s another key thing to understand about arbitrage trades but we’ll get into that once we’ve covered more details…

Sniffing out arbitrage opportunities

Our first step is to identify arbitrage opportunities in real-time. Time is of the essence here, once we’ve identified an exchange discrepancy between markets it must be executed quickly. How quickly? We’ll get to that later…

Our arbitrage data gathering notebook is here:

t/y https://gist.github.com/Valian for starter code

Let’s look at this more closely…

The transaction FEE here is very important. The Binance VIP 0 spot-trade transaction fee is 0.075%, hence the 0.00075 FEE setting. Each trade carries this fee. VIP 1 spot-trades are 0.0675%

Look closely at the trading fees screen in Binance. There are separate fees for “Makers” and “Takers”, the latter are non-market trades (eg. Limit-Orders) and our arbitrage orders must by Limit-Orders otherwise we will lose out on market volatility.

Our margins here are going to be very thin.

For ITERATIONS we will let it run for awhile, whatever you like here. It can take hundreds of iterations to find an arbitrage opportunity so let it breathe.

The PRIMARY coin handles are not comprehensive but sufficient for now.

In our main() section we’ll iterate and write results to a .csv file.

We define our starting_coin as ‘USDT’, this means we start and end with a stable coin not exposed to volatility. You can change this to BTC if you prefer.

You will need to create a config.py file with your Binance API info with the following contents:

API_KEY = 'yourbinanceapikey'
API_SECRET = 'yourbinancesecretkey'

Triangles!

Here is the Jupyter Notebook with some output...

2021-06-08 17:00:29.599878 USDT->BTC->AR->USDT          0.441% profit
BTC / USDT: 0.00002973
AR / BTC : 1937.23363038
USDT / AR : 17.20000000

________
2021-06-08 17:01:43.204487 USDT->SUSD->BTC->USDT 0.0077% profit
SUSD / USDT: 0.99950025
BTC / SUSD: 0.00002970
USDT / BTC : 33504.13000000

________
2021-06-08 17:01:44.334160 USDT->SUSD->BTC->USDT 0.0458% profit
SUSD / USDT: 0.99950025
BTC / SUSD: 0.00002970
USDT / BTC : 33500.00000000

________
2021-06-08 17:01:44.966052 USDT->SUSD->BTC->USDT 0.0144% profit
SUSD / USDT: 0.99950025
BTC / SUSD: 0.00002970
USDT / BTC : 33509.70000000

________
2021-06-08 17:01:46.036212 USDT->BTC->BADGER->USDT 0.1719% profit
BTC / USDT: 0.00002984
BADGER / BTC : 2573.34019557
USDT / BADGER: 12.87500000

2021-06-08 17:01:46.036612 USDT->SUSD->BTC->USDT 0.0242% profit
SUSD / USDT: 0.99950025
BTC / SUSD: 0.00002970
USDT / BTC : 33500.49000000
2021-06-08 17:01:46.707569 USDT->SUSD->BTC->USDT 0.0329% profit
SUSD / USDT: 0.99950025
BTC / SUSD: 0.00002970
USDT / BTC : 33500.50000000

________
2021-06-08 17:01:47.427442 USDT->SUSD->BTC->USDT 0.0025% profit
SUSD / USDT: 0.99950025
BTC / SUSD: 0.00002970
USDT / BTC : 33502.60000000

________
2021-06-08 17:01:48.161421 USDT->SUSD->BTC->USDT 0.0282% profit
SUSD / USDT: 0.99950025
BTC / SUSD: 0.00002970
USDT / BTC : 33502.62000000

These first 6 ticks (separated by ___ ) are quite interesting, let’s look closely:

2021-06-08 17:00:29.599878 USDT->BTC->AR->USDT          0.441%

The triangle identified here is USDT trade for BTC trade for AR (Arweave) trade for USDT, generating a 0.441%, so 100 USDT would have profited 44 cents in this arbitrage, itself taking perhaps no more than 2 secs. But the time involved is part of the challenge here, were these limit trades available for that long or not? This is the key question.

Broken triangles?

The data above proves a clue, because the next line did not show the same arbitrage available in 17:00:30 therefore it was gone. Had we initiated a trade for BTC it might have executed but then a trade for AR may not have. We cannot be sure with only this information.

It is possible that one second later the USDT / BTC exchange was no longer available at the limit price: BTC / USDT: 0.00002973 but now that we have the BTC perhaps the remaining 2 trades are still possible. We simply cannot know this when we initiate the arbitrage exchange.

AR   / BTC :     1937.23363038 ?
USDT / AR : 17.20000000 ?

Each Binance REST API call takes no less than 200ms, depending on where we are located (where your code is running). Binance servers are located in Japan. A limit order (a ‘Taker’) is not instantaneous, it may take another 500ms+ to return so our total time for 3 limit orders could realistically extend out to ~2secs. Of course there could be some inability to execute a limit order as specified in that instant so there are numerous ways an arbitrage execution may fail to complete.

There are numerous ways an arbitrage execution may fail to complete within the window the arbitrage opportunity exists.

Longer windows

But look at the next arbitrage opportunity:

2021-06-08 17:01:43.204487 USDT->SUSD->BTC->USDT       0.0077%

It remains open for several seconds, enough time to execute all 3 trades, however the profit varies between 0.0077% and 0.0282%. If we used 100 USDT this would result in a profit of less than 1 cent after commissions.

Let’s pause and review where we are, and what we have to analyze.

What have we learned?

1. arbitrage opportunities exist but they are limited due to trade commissions

Remember that Binance will charge 0.075% of the trade value in BNB coin for each of the 3 transactions in an arbitrage. Also remember these are ‘Taker’ transactions because they are limit orders.

2. arbitrage triangles often don’t last beyond the tick (second) they are identified

Don’t confuse the PING time to Binance.us (~20ms) with the round-trip time of a BUY or SELL limit order. Even a lookup type API call (account status) will take at least 200ms.

3. there is no way to know if an arbitrage set of (3) exchanges will remain available at the time of discovering it

We cannot know if 50ms after our first exchange the price of the next currency will shift against us.

4. some long-lasting arbitrage opportunity are identified as having very shallow profit margin

We have to balance the profit margins with relative risk, there is real risk (as noted above) of incomplete trades, so the profits have to justify this.

Analysis of the arbitrage data

What we need to do now is to capture arbitrage data using the above code and then analyze it to see how many arbitrage opportunities there were and how many of these lasted at least 2–3 secs. Of these we want to see the profit margin.

Here is a sample run capturing ~60 arbitrage opportunities. You should create your own data using the code above.

First thing we’ll do with our results data is to turn it into a dictionary so we can organize each arbitrage time:

replace ‘USDT->BNB->TROY->USDT’ for an exchange sequence existing in your data.

We consider 2 results to be consecutive if they arrive within some time interval, in this case 1.5 secs. This accounts for slight delays in response times for our data retrievals. You can increase this to deal with slower networks.

if (datetime.strptime(t[0], FMT) — datetime.strptime(prev_time, FMT)).total_seconds() < 1.5:
sequence += 1

Normalization

Next we process our normalized dictionary to count the # of arbitrage opportunities in consecutive ticks. Let’s look at an example:

norm['USDT->BNB->STMX->USDT'][['15:41:11.01', '0.066'],
['15:41:11.54', '0.0752'],
['15:41:12.12', '0.0101'],
['15:41:13.45', '0.0073'],
['15:41:15.04', '0.0217'],
['15:41:15.74', '0.0292']]

In this we see a sequence spanning nearly 5 secs, from 11.01 to 15.74, this would likely be a reasonable amount of time to execute 3 trades as limit orders.

Distill our sequences

So we’ll organize our normalized dictionary of results to distill the # of sequences for each unique exchange found during our data gather.

We can easily see in the results that most arbitrage opportunities found here are of a single sequence. In fact we need a sequence of at least 4–5 to have sufficient time, ie. for a sufficiently long opening in time to execute our trades.

No bueno.

To distill this one step further we’ll count the # of sequences over 4 ticks and see what % of our trades for this data gather would have been lengthy enough to trade through.

10% of the arbitrage opportunities during this run would have remained open long enough for us to execute our trades, assuming a reasonable round-trip response for our API calls.

But this means 90% of these arbitrage opportunities would have likely left us with incomplete trade ‘triangles’, with some crypto-coin in our wallet that didn’t get exchanged as planned.

90% of these arbitrage opportunities would have likely left us with incomplete trade ‘triangles’, with some crypto-coin in our wallet that didn’t get exchanged as planned and were left exposed to volatility.

Worse: the average profit margin for the long trades in this run would have been 0.05% ! not 5% but 1/100 of that.

If we had executed exchanges starting with 100 USDT we would have averaged a profit of 5 cents. However the 90% of incomplete trades would have left us with exposure to volatility in those coins, easily resulting in losses of far more than our projected profit.

We can see now why a lot of crypto quant traders have given up using arbitrage as a strategy.

First of all: the transaction fees, while small, have a significant effect here, unless you get to a high enough VIP level, trading tens of $M per month.

Second: API latency, with a limited time-window of somewhere between a fraction of a second and 1–2 secs this is little time within which to reliably execute 3 trades. Even if your trading code is located near the Binance servers (in Japan) the latency of a REST API call is real.

Third: Margins are often extremely thin, even if the transaction fees are low. And margins using this strategy must cover the inherent risk of trades that don’t complete and have left-over coins exposed to volatility, which is inevitable.

Years ago when crypto programmatic trading was relatively new, many jumped on this bandwagon, often with poor github repositories featuring tri-arbitrage python bots, and then it is claimed that the major exchanges have their internal arbitrage systems which will always beat the retail trader, irrespective of tier level — the net result is that the alpha of the strategy eroded.

Is arbitrage crypto trading just a bunch of hype? Not necessarily, it is real and we understand how it works, however it has been seriously marginalized.

And now you know.

--

--