Spatial arbitrage
Spatial arbitrage is the act of buying low at one exchange and selling high at another. That is pretty much all there is to it. However, as always, the devil is in the details. There are some important variables, such as transaction fees, that need to be taken into account before attempting to take advantage of an arbitrage opportunity.
Important variables
The following information needs to be known to determine if there is an exploitable and profitable arbitrage opportunity between two exchanges:
-
Bid-ask difference: If the highest bid on one exchange is higher than the lowest ask on another exchange, then there is a (potential) arbitrage opportunity present. You can then buy from the cheap exchange and sell on the expensive exchange, pocketing the difference.
-
Transaction fees: A percentage fee is paid to the exchange for every trade. This makes many apparent arbitrage opportunities unprofitable, because the actual prices are either lower (bid * (1 - fee)) or higher (ask * (1 + fee)). These fees can differ a lot between venues. Be aware that many exchanges use the maker/taker model, meaning that the fee you will get depends on the traded volume and on whether you're taking away liquidity (by crossing the spread) or providing liquidity (by letting your orders be filled).
-
Available arbitrage volume: How much volume you can profitably trade between two exchanges, taking transaction fees into account.
Arbitrage detection using aggregated order books
It is possible to aggregate order books from multiple exchanges into one big limit order book. Often this will result in a crossed order book (where bid prices are higher than ask prices). This means that there are arbitrage opportunities present, seeing as it is possible to buy low on one exchange and sell higher on another exchange. The aggregated limit order book then forms an easy, albeit rather slow method for finding all spatial arbitrage opportunities between several exchanges. Transaction fees can be taken into account by multiplying all bids and asks in the order book by respectively (1-fee) and (1+fee).
For example, in the following limit order book aggregated from exchanges A and B (no fees for simplicity), there is an arbitrage opportunity of $268.22 or 163 shares present.
BIDS ASKS
[ EXCH | QTY | PRICE - PRICE | QTY | EXCH ]
[-------------------------------------------]
[ B | 131 | 103.23 - 100.54 | 48 | A ]
[ A | 32 | 102.98 - 101.87 | 84 | A ]
[ A | 293 | 101.48 - 102.17 | 38 | B ]
[ A | 65 | 101.10 - 104.75 | 27 | B ]
The first two asks can be bought and sold completely into to two topmost bids, resulting in a profit of about $243:
$$(103.23 - 100.54) * 48 + (103.23 - 101.87) * 83 + (102.98 - 101.87) * 1 = 243.11$$
Thirty-one shares of the third ask can then be sold into the second bid for , after which no further profits can be made:
$$(102.98 - 102.17)*31 = 25.11$$
Exploitation methods
Arbitrage can be exploited by either moving assets between exchanges or by keeping an equal amount of money and assets on all exchanges.
The first method is rather impractical and risky depending on the time it takes to move funds between exchanges. Arbitrage opportunities dissapear rather quickly. By the time you have moved any bought assets the prices on other exchanges will have moved and you will most likely be forced to sell at a loss.
The second method incurs no such risk. This is because an equal amount of money and assets is kept on all exchanges. When an arbitrage opportunity appears you can then instantly and simultaneously sell on the expensive exchange and buy on the cheap exchange. The prices won't be able to change much and the expected arbitrage profits will be mostly realized. Funds will still need to be moved between exchanges (to avoid asset imbalances) over time, but not during trades. The downside to this method is the large amount of funds needed depending on the amount of exchanges you intend to arbitrage.
Python implementation
(In progress)
This strategy implementation uses an aggregated order book to find arbitrage opportunities between exchanges. It takes both fees and available volume into account.
from api.unifiedapi import UnifiedAPI
from exchange.orders import ExternalOrder, AggregatedOrderbook
from strategies.strategy import Strategy
from system.event import Signal
import time
class Arbitrage:
def __init__(self, path, delta, buy_price, sell_price, volume, fees, profit):
self.buy_price = buy_price
self.sell_price = sell_price
self.path = path
self.delta = delta
self.volume = volume
self.fees = fees
self.profit = profit
class SpatialArbitrage(Strategy):
def __init__(self, pair, exchanges):
Strategy.__init__(self, "SpatArb", [pair], exchanges)
self.api = UnifiedAPI()
self.pair = pair
self.book = AggregatedOrderbook()
self.exchanges = [exchange for exchange in exchanges if exchange in self.api.exchanges() and self.pair in self.api.pairs(exchange)]
self.fees = {exchange: self.api.fees(exchange)['taker'] for exchange in self.exchanges}
self.paths = []
def update(self):
orderbooks = self.api.order_books(self.exchanges, self.pair)
self.book = AggregatedOrderbook()
for orderbook in orderbooks:
exchange = orderbook['exchange']
bids = [ExternalOrder(exchange, "limit", "buy", bid['price'], bid['quantity']) for bid in orderbook['bids']]
asks = [ExternalOrder(exchange, "limit", "sell", ask['price'], ask['quantity']) for ask in orderbook['asks']]
self.book.merge(bids, asks)
def find_opportunities(self):
opportunities = []
possible_paths = [list(path) for path in itertools.permutations(self.exchanges, r=3 - 1)]
for path in possible_paths:
arbitrage = self.analyze_path(path)
if arbitrage:
opportunities.append(arbitrage)
return opportunities
def analyze_path(self, path):
buy_exch = path[0]
sell_exch = path[1]
asks = copy.deepcopy([ask for ask in self.book.asks if ask.exchange == buy_exch])
bids = copy.deepcopy([bid for bid in self.book.bids if bid.exchange == sell_exch])
ask = asks[0].price
bid = bids[0].price
delta = bid*(1-self.fees[sell_exch])-ask*(1+self.fees[buy_exch])
if delta > 0:
volume = 0
profit = 0
fees = 0
while bids[0].price * (1 - self.fees[buy_exch]) > asks[0].price * (1 + self.fees[buy_exch]): #while there is arbitrage profit possible
buy_order = asks.pop(0)
while buy_order.quantity > 0:# while the arbitrage order is not filled
sell_order = bids[0]
delta = sell_order.price * (1 - self.fees[buy_exch]) > buy_order.price * (1 + self.fees[buy_exch])
if not delta:
break
elif sell_order.quantity > buy_order.quantity:
profit += (buy_order.quantity * sell_order.price) - (buy_order.quantity * buy_order.price)
fees += (buy_order.quantity * sell_order.price * self.fees[sell_exch]) + (buy_order.quantity * buy_order.price * self.fees[buy_exch])
volume += buy_order.quantity
sell_order.quantity -= buy_order.quantity
buy_order.quantity = 0
elif sell_order.quantity == buy_order.quantity:
profit += (buy_order.quantity * sell_order.price) - (buy_order.quantity * buy_order.price)
fees += (buy_order.quantity * sell_order.price * self.fees[sell_exch]) + (
buy_order.quantity * buy_order.price * self.fees[buy_exch])
volume += buy_order.quantity
buy_order.quantity = 0
del bids[0]
elif sell_order.quantity < buy_order.quantity:
profit += (sell_order.quantity * sell_order.price) - (sell_order.quantity * buy_order.price)
buy_order.quantity -= sell_order.quantity
fees += (sell_order.quantity * sell_order.price * self.fees[sell_exch]) + (
sell_order.quantity * buy_order.price * self.fees[buy_exch])
volume += sell_order.quantity
buy_order.quantity -= sell_order.quantity
del bids[0]
return Arbitrage(path, delta, ask*(1+self.fees[buy_exch]), bid*(1-self.fees[sell_exch]), volume, fees, profit)
else:
pass
def generate_signal(self, ticker):
signals = []
self.update()
opportunities = self.find_opportunities()
opportunities.sort(key = lambda a: -a.profit)
for arbitrage in opportunities:
signals.append(Signal(arbitrage.path[0], self.pair, "BUY", arbitrage.buy_price, arbitrage.volume/arbitrage.buy_price, time.time()))
signals.append(Signal(arbitrage.path[1], self.pair, "SELL", arbitrage.sell_price, arbitrage.volume/arbitrage.sell_price, time.time()))
return signals