# Cryptocurrency arbitrage search#

Cryptocurrency exchanges are web services that enable the purchase, sale, and exchange of cryptocurrencies. These exchanges provide liquidity for owners and establish the relative value of these currencies. Joining an exchange enables a user to maintain multiple currencies in a digital wallet, buy and sell currencies, and use cryptocurrencies for financial transactions.

In this example, we explore the efficiency of cryptocurrency exchanges by testing for arbitrage opportunities. An arbitrage exists if a customer can realize a net profit through a sequence of risk-free trades. The efficient market hypothesis assumes arbitrage opportunities are quickly identified and exploited by investors. As a result of their trading, prices reach a new equilibrium so that any arbitrage opportunities would be small and fleeting in an efficient market. The question here is whether it is possible, with real-time data and rapid execution, for a trader to profit from these fleeting arbitrage opportunities.

## Installations and Imports#

### AMPL and Solvers#

First we install AMPL, solvers and necessary modules. This notebook uses the open-source library `ccxt`

. `ccxt`

supports the real-time APIs of the largest and most common exchanges on which cryptocurrencies are traded.

```
# install dependencies and select solver
%pip install -q amplpy ccxt matplotlib networkx numpy pandas
SOLVER = "cbc"
from amplpy import AMPL, ampl_notebook
ampl = ampl_notebook(
modules=["cbc"], # modules to install
license_uuid="default", # license to use
) # instantiate AMPL object and register magics
```

```
import os
import sys
from time import time
from timeit import default_timer as timer
import ccxt
import matplotlib.pyplot as plt
import networkx as nx
import numpy as np
import pandas as pd
```

## Cryptocurrency exchanges#

Here we use the `ccxt`

library and list current exchanges supported by `ccxt`

.

```
print("Available exchanges:\n")
for i, exchange in enumerate(ccxt.exchanges):
print(f"{i+1:3d}) {exchange.ljust(20)}", end="" if (i + 1) % 4 else "\n")
```

```
Available exchanges:
1) ace 2) alpaca 3) ascendex 4) bequant
5) bigone 6) binance 7) binancecoinm 8) binanceus
9) binanceusdm 10) bit2c 11) bitbank 12) bitbay
13) bitbns 14) bitcoincom 15) bitfinex 16) bitfinex2
17) bitflyer 18) bitforex 19) bitget 20) bithumb
21) bitmart 22) bitmex 23) bitopro 24) bitpanda
25) bitrue 26) bitso 27) bitstamp 28) bitstamp1
29) bittrex 30) bitvavo 31) bkex 32) bl3p
33) blockchaincom 34) btcalpha 35) btcbox 36) btcmarkets
37) btctradeua 38) btcturk 39) bybit 40) cex
41) coinbase 42) coinbaseprime 43) coinbasepro 44) coincheck
45) coinex 46) coinfalcon 47) coinmate 48) coinone
49) coinsph 50) coinspot 51) cryptocom 52) currencycom
53) delta 54) deribit 55) digifinex 56) exmo
57) fmfwio 58) gate 59) gateio 60) gemini
61) hitbtc 62) hitbtc3 63) hollaex 64) huobi
65) huobijp 66) huobipro 67) idex 68) independentreserve
69) indodax 70) kraken 71) krakenfutures 72) kucoin
73) kucoinfutures 74) kuna 75) latoken 76) lbank
77) lbank2 78) luno 79) lykke 80) mercado
81) mexc 82) mexc3 83) ndax 84) novadax
85) oceanex 86) okcoin 87) okex 88) okex5
89) okx 90) paymium 91) phemex 92) poloniex
93) poloniexfutures 94) probit 95) tidex 96) timex
97) tokocrypto 98) upbit 99) wavesexchange 100) wazirx
101) whitebit 102) woo 103) yobit 104) zaif
105) zonda
```

## Representing an exchange as a directed graph#

First, we need some terminology. Trading between two specific currencies is called a market, with each exchange hosting multiple markets. `ccxt`

labels each market with a symbol common across exchanges. The market symbol is an upper-case string with abbreviations for a pair of traded currencies separated by a slash (\(/\)). The first abbreviation is the base currency, the second is the quote currency. Prices for the base currency are denominated in units of the quote currency. As an example, \(ETH/BTC\) refers to a market for the base currency Ethereum (ETH) quoted in units of the Bitcoin (BTC). The same market symbol can refer to an offer to sell the base currency (a ābidā) or to an offer to sell the base currency (an āaskā). For example, \(x\) ETH/BTC means you can buy \(x\) units of BTC with one unit of ETH.

An exchange can be represented by a directed graph constructed from the market symbols available on that exchange. There, currencies correspond to nodes on the directed graph. Market symbols correspond to edges in the directed graph, with the source indicating the quote currency and the destination indicating the base currency. The following code develops such a sample graph.

```
# global variables used in subsequent cells
# create an exchange object
exchange = ccxt.binanceus()
def get_exchange_dg(exchange, minimum_in_degree=1):
"""
Return a directed graph constructed from the market symbols on a specified exchange.
"""
markets = exchange.load_markets()
symbols = markets.keys()
# create an edge for all market symbols
dg = nx.DiGraph()
for base, quote in [symbol.split("/") for symbol in symbols]:
dg.add_edge(quote, base, color="k", width=1)
# remove base currencies with in_degree less than minimum_in_degree
remove_nodes = [
node
for node in dg.nodes
if dg.out_degree(node) == 0 and dg.in_degree(node) < minimum_in_degree
]
for node in reversed(list(nx.topological_sort(dg))):
if node in remove_nodes:
dg.remove_node(node)
else:
break
# color quote currencies in gold
for node in dg.nodes():
dg.nodes[node]["color"] = "gold" if dg.out_degree(node) > 0 else "lightblue"
return dg
def draw_dg(dg, rad=0.0):
"""
Draw directed graph of markets symbols.
"""
n_nodes = len(dg.nodes)
size = int(2.5 * np.sqrt(n_nodes))
fig = plt.figure(figsize=(size, size))
pos = nx.circular_layout(dg)
nx.draw(
dg,
pos,
with_labels=True,
node_color=[dg.nodes[node]["color"] for node in dg.nodes()],
edge_color=[dg.edges[u, v]["color"] for u, v in dg.edges],
width=[dg.edges[u, v]["width"] for u, v in dg.edges],
node_size=1000,
font_size=8,
arrowsize=15,
connectionstyle=f"arc3, rad={rad}",
)
nx.draw_networkx_edge_labels(
dg, pos, edge_labels={(src, dst): f"{src}/{dst}" for src, dst in dg.edges()}
)
return plt.gca()
minimum_in_degree = 5
exchange_dg = get_exchange_dg(exchange, minimum_in_degree)
ax = draw_dg(exchange_dg, 0.01)
ax.set_title(
exchange.name + "\n" + f"Minimum in Degree (Base Currencies) = {minimum_in_degree}"
)
print(f"Number of nodes = {len(exchange_dg.nodes()):3d}")
print(f"Number of edges = {len(exchange_dg.edges()):3d}")
```

```
Number of nodes = 170
Number of edges = 533
```

## Exchange order book#

The order book for a currency exchange is the real-time inventory of trading orders.

A **bid** is an offer to buy up to a specified amount of the base currency at the price not exceeding the ābid priceā in the quote currency. An **ask** is an offer to sell up to a specified amount of the base currency at a price no less than a value specified given in the quote currency.

The exchange attempts to match the bid to ask order at a price less than or equal to the bid price. If a transaction occurs, the buyer will receive an amount of base currency less than or equal to the bid volume and the ask volume, at a price less than or equal to the bid price and no less than the specified value.

The order book for currency exchange is the real-time inventory of orders. The exchange order book maintains a list of all active orders for symbols traded on the exchange. Incoming bids above the lowest ask or incoming asks below the highest bid will be immediately matched and transactions executed following the rules of the exchange.

The following cell reads and displays a previously saved order book. Cells at the end of this notebook demonstrate how to retrieve an order book from an exchange and save it as a Pandas DataFrame.

```
if "google.colab" in sys.modules:
csv = "Binance_US_orderbook_20230313_152616.csv"
order_book = pd.read_csv(
"https://raw.githubusercontent.com/mobook/MO-book/main/notebooks/04/" + csv,
index_col=0,
)
else:
import glob
# find all previously saved order books
fnames = sorted(glob.glob(f"*orderbook*".replace(" ", "_")))
fname = fnames[-1]
# read the last order book from the list of order books
print(f"\nReading: {fname}\n")
order_book = pd.read_csv(fname, index_col=0)
display(order_book)
```

```
Reading: Binance_US_orderbook_saved.csv
```

symbol | timestamp | base | quote | bid_price | bid_volume | ask_price | ask_volume | |
---|---|---|---|---|---|---|---|---|

0 | ETH/BTC | 2023-03-02 15:36:06.529 | ETH | BTC | 0.069735 | 0.012000 | 0.069759 | 0.050000 |

1 | BNB/BTC | 2023-03-02 15:36:06.583 | BNB | BTC | 0.012743 | 0.050000 | 0.012755 | 3.000000 |

2 | ADA/BTC | 2023-03-02 15:36:06.637 | ADA | BTC | 0.000015 | 2.000000 | 0.000015 | 2168.000000 |

3 | SOL/BTC | 2023-03-02 15:36:06.690 | SOL | BTC | 0.000935 | 1.420000 | 0.000936 | 15.120000 |

4 | MATIC/BTC | 2023-03-02 15:36:06.750 | MATIC | BTC | 0.000052 | 26.200000 | 0.000052 | 150.200000 |

5 | MANA/BTC | 2023-03-02 15:36:06.848 | MANA | BTC | 0.000027 | 831.000000 | 0.000027 | 1409.000000 |

6 | TRX/BTC | 2023-03-02 15:36:06.905 | TRX | BTC | 0.000003 | 4.000000 | 0.000003 | 25352.000000 |

7 | ADA/ETH | 2023-03-02 15:36:06.960 | ADA | ETH | 0.000214 | 994.900000 | 0.000214 | 891.600000 |

8 | BTC/USDT | 2023-03-02 15:36:07.012 | BTC | USDT | 23373.920000 | 0.118619 | 23376.000000 | 0.045275 |

9 | ETH/USDT | 2023-03-02 15:36:07.065 | ETH | USDT | 1630.200000 | 0.950000 | 1630.770000 | 0.500000 |

10 | BNB/USDT | 2023-03-02 15:36:07.118 | BNB | USDT | 297.857700 | 0.800000 | 297.891900 | 0.800000 |

11 | ADA/USDT | 2023-03-02 15:36:07.172 | ADA | USDT | 0.348630 | 1100.000000 | 0.348750 | 511.200000 |

12 | BUSD/USDT | 2023-03-02 15:36:07.226 | BUSD | USDT | 0.999500 | 293433.930000 | 0.999600 | 317175.730000 |

13 | SOL/USDT | 2023-03-02 15:36:07.288 | SOL | USDT | 21.857000 | 22.870000 | 21.863500 | 23.000000 |

14 | USDC/USDT | 2023-03-02 15:36:07.342 | USDC | USDT | 1.000100 | 307657.000000 | 1.000200 | 299181.000000 |

15 | MATIC/USDT | 2023-03-02 15:36:07.394 | MATIC | USDT | 1.203000 | 1664.600000 | 1.205000 | 5405.600000 |

16 | MANA/USDT | 2023-03-02 15:36:07.447 | MANA | USDT | 0.631000 | 157.000000 | 0.632200 | 571.000000 |

17 | TRX/USDT | 2023-03-02 15:36:07.501 | TRX | USDT | 0.069280 | 10824.900000 | 0.069330 | 10818.600000 |

18 | BTC/BUSD | 2023-03-02 15:36:07.612 | BTC | BUSD | 23371.440000 | 0.021500 | 23376.830000 | 0.021500 |

19 | BNB/BUSD | 2023-03-02 15:36:07.665 | BNB | BUSD | 297.763000 | 1.670000 | 297.952500 | 1.340000 |

20 | ETH/BUSD | 2023-03-02 15:36:07.719 | ETH | BUSD | 1630.210000 | 0.510000 | 1630.760000 | 0.255000 |

21 | MATIC/BUSD | 2023-03-02 15:36:07.772 | MATIC | BUSD | 1.203410 | 623.100000 | 1.204540 | 415.000000 |

22 | USDC/BUSD | 2023-03-02 15:36:07.893 | USDC | BUSD | 0.999900 | 329027.800000 | 1.000000 | 279879.620000 |

23 | MANA/BUSD | 2023-03-02 15:36:07.950 | MANA | BUSD | 0.630700 | 343.000000 | 0.632100 | 3054.000000 |

24 | ADA/BUSD | 2023-03-02 15:36:08.003 | ADA | BUSD | 0.348000 | 6582.900000 | 0.349000 | 997.000000 |

25 | SOL/BUSD | 2023-03-02 15:36:08.056 | SOL | BUSD | 21.830000 | 1.390000 | 21.900000 | 181.090000 |

26 | TRX/BUSD | 2023-03-02 15:36:08.114 | TRX | BUSD | 0.069290 | 10823.900000 | 0.069390 | 25220.400000 |

27 | BTC/USDC | 2023-03-02 15:36:08.170 | BTC | USDC | 23371.660000 | 0.020000 | 23376.990000 | 0.051000 |

28 | ETH/USDC | 2023-03-02 15:36:08.228 | ETH | USDC | 1630.160000 | 0.100000 | 1630.810000 | 0.215000 |

29 | SOL/USDC | 2023-03-02 15:36:08.298 | SOL | USDC | 21.830000 | 343.520000 | 21.880000 | 201.080000 |

30 | ADA/USDC | 2023-03-02 15:36:08.368 | ADA | USDC | 0.348200 | 8615.400000 | 0.349400 | 2433.100000 |

31 | BTC/DAI | 2023-03-02 15:36:08.433 | BTC | DAI | 23366.440000 | 0.049540 | 23394.360000 | 0.049500 |

32 | ETH/DAI | 2023-03-02 15:36:08.485 | ETH | DAI | 1629.890000 | 0.497400 | 1631.810000 | 1.490000 |

33 | BTC/USD | 2023-03-02 15:36:08.623 | BTC | USD | 23373.010000 | 0.007463 | 23376.720000 | 0.048805 |

34 | ETH/USD | 2023-03-02 15:36:08.675 | ETH | USD | 1630.490000 | 2.564550 | 1630.740000 | 0.580700 |

35 | USDT/USD | 2023-03-02 15:36:08.730 | USDT | USD | 1.000000 | 10407.870000 | 1.000100 | 954839.680000 |

36 | BNB/USD | 2023-03-02 15:36:08.782 | BNB | USD | 297.900200 | 2.100000 | 297.952500 | 1.342000 |

37 | ADA/USD | 2023-03-02 15:36:08.835 | ADA | USD | 0.348800 | 1500.000000 | 0.348900 | 3000.000000 |

38 | BUSD/USD | 2023-03-02 15:36:08.942 | BUSD | USD | 0.999500 | 79157.800000 | 0.999900 | 795593.480000 |

39 | MATIC/USD | 2023-03-02 15:36:08.998 | MATIC | USD | 1.204000 | 937.600000 | 1.204500 | 937.500000 |

40 | USDC/USD | 2023-03-02 15:36:09.051 | USDC | USD | 0.999900 | 5050.300000 | 1.000000 | 517682.170000 |

41 | MANA/USD | 2023-03-02 15:36:09.102 | MANA | USD | 0.631000 | 372.340000 | 0.631600 | 572.340000 |

42 | DAI/USD | 2023-03-02 15:36:09.155 | DAI | USD | 0.999100 | 4534.660000 | 1.000100 | 7591.820000 |

43 | SOL/USD | 2023-03-02 15:36:09.217 | SOL | USD | 21.858100 | 55.730000 | 21.865900 | 18.290000 |

44 | TRX/USD | 2023-03-02 15:36:09.270 | TRX | USD | 0.069200 | 225602.200000 | 0.069400 | 224245.500000 |

## Modelling the arbitrage search problem as a graph#

Our goal will be to find arbitrage opportunities, i.e., the possibility to start from a given currency and, through a sequence of executed trades, arrive back at the same currency with a higher balance than at the beginning. We will model this problem as a network one.

A bid appearing in the order book for market symbol \(b/q\) is an order from a prospective counter party to purchase an amount of the base currency \(b\) at a bid price given in a quote currency \(q\). For a currency trader, a bid in the order book is an opportunity to convert the base currency \(b\) into the quote currency \(q\).

The order book can be represented as a directed graph where nodes correspond to individual currencies. A directed edge \(b\rightarrow q\) from node \(b\) to node \(q\) describes an opportunity for us to convert currency \(b\) into units of currency \(q\). Let \(V_b\) and \(V_q\) denote the amounts of each currency held by us, and let \(x_{b\rightarrow q}\) denote the amount of currency \(b\) exchanged for currency \(j\). Following the transaction \(x_{b\rightarrow q}\) we have the following changes to the currency holdings

where \(a_{b\rightarrow q}\) is a *conversion coefficient* equal to the price of \(b\) expressed in terms of currency \(q\). The capacity \(c_{b\rightarrow q}\) of an trading along edge \(b\rightarrow q\) is specified by a relationship

Because the arcs in our graph correspond to two types of orders - bid and ask - we need to build a consistent way of expressing them in our \(a_{b\rightarrow q}\), \(c_{b\rightarrow q}\) notation. So now, imagine that we are the party that accepts the buy and ask bids existing in the graph.

For bid orders, we have a chance to convert the base currency \(b\) into the quote currency \(q\), for which we will use the following notation:

An ask order for symbol \(b/q\) is an order to sell the base currency at price not less than the āaskā price given in terms of the quote currency. The ask volume is the amount of base currency to be sold. For us, a sell order is an opportunity to convert the quoted currency into the base currency such that

The following cell creates a directed graph using data from an exchange order book. To distinguish between different order types, we will highlight the big orders with green color, and ask orders with red color.

```
def order_book_to_dg(order_book):
"""
Convert an order book dataframe into a directed graph using the NetworkX library.
Parameters:
-----------
order_book : pandas.DataFrame
A dataframe containing the order book information.
Returns:
--------
dg_order_book : networkx.DiGraph
A directed graph representing the order book.
"""
# create a dictionary of edges index by (src, dst)
dg_order_book = nx.DiGraph()
# loop over each order in the order book dataframe
for order in order_book.index:
# if the order is a 'bid', i.e., an order to purchase the base currency
if not np.isnan(order_book.at[order, "bid_volume"]):
src = order_book.at[order, "base"]
dst = order_book.at[order, "quote"]
# add an edge to the graph with the relevant attributes
dg_order_book.add_edge(
src,
dst,
kind="bid",
a=order_book.at[order, "bid_price"],
capacity=order_book.at[order, "bid_volume"],
weight=-np.log(order_book.at[order, "bid_price"]),
color="g",
width=0.5,
)
# if the order is an 'ask', i.e., an order to sell the base currency
if not np.isnan(order_book.at[order, "ask_volume"]):
src = order_book.at[order, "quote"]
dst = order_book.at[order, "base"]
# add an edge to the graph with the relevant attributes
dg_order_book.add_edge(
src,
dst,
kind="ask",
a=1.0 / order_book.at[order, "ask_price"],
capacity=order_book.at[order, "ask_volume"]
* order_book.at[order, "ask_price"],
weight=-np.log(1.0 / order_book.at[order, "ask_price"]),
color="r",
width=0.5,
)
# loop over each node in the graph and set the color attribute to "lightblue"
for node in dg_order_book.nodes():
dg_order_book.nodes[node]["color"] = "lightblue"
return dg_order_book
order_book_dg = order_book_to_dg(order_book)
```

First, we simply print the content of the order book as a list of arcs.

```
# display contents of the directed graph
print(f"src --> dst kind a c")
print(f"------------------------------------------------------")
for src, dst in order_book_dg.edges():
print(
f"{src:5s} --> {dst:5s} {order_book_dg.edges[(src, dst)]['kind']}"
+ f"{order_book_dg.edges[(src, dst)]['a']: 16f} {order_book_dg.edges[(src, dst)]['capacity']: 16f} "
)
```

```
src --> dst kind a c
------------------------------------------------------
ETH --> BTC bid 0.069735 0.012000
ETH --> ADA ask 4668.534080 0.190981
ETH --> USDT bid 1630.200000 0.950000
ETH --> BUSD bid 1630.210000 0.510000
ETH --> USDC bid 1630.160000 0.100000
ETH --> DAI bid 1629.890000 0.497400
ETH --> USD bid 1630.490000 2.564550
BTC --> ETH ask 14.335068 0.003488
BTC --> BNB ask 78.403701 0.038263
BTC --> ADA ask 66844.919786 0.032433
BTC --> SOL ask 1068.261938 0.014154
BTC --> MATIC ask 19391.118868 0.007746
BTC --> MANA ask 36968.576710 0.038113
BTC --> TRX ask 335570.469799 0.075549
BTC --> USDT bid 23373.920000 0.118619
BTC --> BUSD bid 23371.440000 0.021500
BTC --> USDC bid 23371.660000 0.020000
BTC --> DAI bid 23366.440000 0.049540
BTC --> USD bid 23373.010000 0.007463
BNB --> BTC bid 0.012743 0.050000
BNB --> USDT bid 297.857700 0.800000
BNB --> BUSD bid 297.763000 1.670000
BNB --> USD bid 297.900200 2.100000
ADA --> BTC bid 0.000015 2.000000
ADA --> ETH bid 0.000214 994.900000
ADA --> USDT bid 0.348630 1100.000000
ADA --> BUSD bid 0.348000 6582.900000
ADA --> USDC bid 0.348200 8615.400000
ADA --> USD bid 0.348800 1500.000000
SOL --> BTC bid 0.000935 1.420000
SOL --> USDT bid 21.857000 22.870000
SOL --> BUSD bid 21.830000 1.390000
SOL --> USDC bid 21.830000 343.520000
SOL --> USD bid 21.858100 55.730000
MATIC --> BTC bid 0.000052 26.200000
MATIC --> USDT bid 1.203000 1664.600000
MATIC --> BUSD bid 1.203410 623.100000
MATIC --> USD bid 1.204000 937.600000
MANA --> BTC bid 0.000027 831.000000
MANA --> USDT bid 0.631000 157.000000
MANA --> BUSD bid 0.630700 343.000000
MANA --> USD bid 0.631000 372.340000
TRX --> BTC bid 0.000003 4.000000
TRX --> USDT bid 0.069280 10824.900000
TRX --> BUSD bid 0.069290 10823.900000
TRX --> USD bid 0.069200 225602.200000
USDT --> BTC ask 0.000043 1058.348400
USDT --> ETH ask 0.000613 815.385000
USDT --> BNB ask 0.003357 238.313520
USDT --> ADA ask 2.867384 178.281000
USDT --> BUSD ask 1.000400 317048.859708
USDT --> SOL ask 0.045738 502.860500
USDT --> USDC ask 0.999800 299240.836200
USDT --> MATIC ask 0.829876 6513.748000
USDT --> MANA ask 1.581778 360.986200
USDT --> TRX ask 14.423770 750.053538
USDT --> USD bid 1.000000 10407.870000
BUSD --> USDT bid 0.999500 293433.930000
BUSD --> BTC ask 0.000043 502.601845
BUSD --> BNB ask 0.003356 399.256350
BUSD --> ETH ask 0.000613 415.843800
BUSD --> MATIC ask 0.830192 499.884100
BUSD --> USDC ask 1.000000 279879.620000
BUSD --> MANA ask 1.582028 1930.433400
BUSD --> ADA ask 2.865330 347.953000
BUSD --> SOL ask 0.045662 3965.871000
BUSD --> TRX ask 14.411298 1750.043556
BUSD --> USD bid 0.999500 79157.800000
USDC --> USDT bid 1.000100 307657.000000
USDC --> BUSD bid 0.999900 329027.800000
USDC --> BTC ask 0.000043 1192.226490
USDC --> ETH ask 0.000613 350.624150
USDC --> SOL ask 0.045704 4399.630400
USDC --> ADA ask 2.862049 850.125140
USDC --> USD bid 0.999900 5050.300000
DAI --> BTC ask 0.000043 1158.020820
DAI --> ETH ask 0.000613 2431.396900
DAI --> USD bid 0.999100 4534.660000
USD --> BTC ask 0.000043 1140.900820
USD --> ETH ask 0.000613 946.970718
USD --> USDT ask 0.999900 954935.163968
USD --> BNB ask 0.003356 399.852255
USD --> ADA ask 2.866151 1046.700000
USD --> BUSD ask 1.000100 795513.920652
USD --> MATIC ask 0.830220 1129.218750
USD --> USDC ask 1.000000 517682.170000
USD --> MANA ask 1.583281 361.489944
USD --> DAI ask 0.999900 7592.579182
USD --> SOL ask 0.045733 399.927311
USD --> TRX ask 14.409222 15562.637700
```

Next, we draw the graph itself.

```
draw_dg(order_book_dg, 0.05)
plt.show()
```

## Trading and Arbitrage#

With the unified treatment of the bid and ask orders, we are ready to pose the mathematical problem of finding an arbitrage opportunity. An arbitrage exists if it is possible to find a closed path and a sequence of transactions in the directed graph resulting in a net increase in currency holdings. Given a path

the path is closed if \(i_n = i_0\). The path has finite capacity if each edge in the path has a non-zero capacity. For a sufficiently small holding \(w_{i_0}\) of currency \(i_0\) (because of the capacity constraints), a closed path with \(i_0 = i_n\) represents an arbitrage opportunity if

If all we care about is simply finding an arbitrage cycle, regardless of the volume traded, we can use one of the many shortest path algorithms from the `networkx`

library. To convert the problem of finding a path meeting the above condition into a sum-of-terms to be minimized, we can take the negative logarithm of both sides to obtain the condition:

In other words, if we assign the negative logarithm as the weight of arcs in a graph, then our problem just became translated into the problem of searching for a cycle with a total sum of weights along it to be negative.

## Find order books that demonstrate arbitrage opportunities#

A simple cycle is a closed path where no node appears twice. Simple cycles are distinct if they are not cyclic permutations (essentially, rewriting the same path but with a different start=end point) of each other. One could check for arbitrage opportunities by checking if there are any negative simple cycles in the graph.

However, looking for a negative-weight cycle through searching for an arbitrage opportunity can be a daunting task - a brute-force search over all simple cycles has complexity \((n + e)(c + 1)\) which is impractical for large-scale applications. A more efficient search based on the Bellman-Ford algorithm is embedded in the NetworkX function `negative_edge_cycle`

that returns a logical True if a negative cycle exists in a directed graph.

```
order_book_dg = order_book_to_dg(order_book)
nx.negative_edge_cycle(order_book_dg, weight="weight", heuristic=True)
```

```
True
```

The function `negative_edge_cycle`

is fast, but it only indicates if there is a negative cycle or not, and we donāt even know what kind of a cycle is it so it would be hard to use that information to perform an arbitrage.

Luckily, the `networkx`

library includes the function `find_negative_cycle`

that locates a single negative edge cycle if one exists. We can use this to demonstrate the existence of an arbitrage opportunity and to highlight that opportunity on the directed graph of all possible trades. The following cell reports the cycle found and the trading return measured in basis points (1 bp = 0.01%), and marks it with thicker arcs in the graph.

```
# compute the sum of weights given a list of nodes
def sum_weights(cycle):
return sum(
[
order_book_dg.edges[edge]["weight"]
for edge in zip(cycle, cycle[1:] + cycle[:1])
]
)
order_book_dg = order_book_to_dg(order_book)
arb = nx.find_negative_cycle(order_book_dg, weight="weight", source="USD")[:-1]
bp = 10000 * (np.exp(-sum_weights(arb)) - 1)
for src, dst in zip(arb, arb[1:] + arb[:1]):
order_book_dg[src][dst]["width"] = 5
ax = draw_dg(order_book_dg, 0.05)
ax.set_title(
f"Trading cycle with {len(list(arb))} trades: {' -> '.join(list(arb))}\n\n Return = {bp:6.3f} basis points "
)
plt.show()
```

Note this may or may not be the trading cycle with maximum return. There may be other cycles with higher or lower returns, and that allow higher or lower trading volumes.

## Brute force search arbitrage with simple cycles#

Not all arbitrage cycles are the same - some yield a higher relative return (per dollar invested) than the others, and some yield a higher absolute return (maximum amount of money to be made risk-free) than others. This is because the amount of money that flows through a negative cycle is upper bounded by the size of the smallest order in that cycle. Thus, if one is looking for the best possible arbitrage sequence of trades, finding just āa cycleā might not be enough.

A crude way to search for a good arbitrage opportunity would be to enumerate all possible simple cycles in a graph and pick the one thatās best according to whatever criterion we pick. A brute force search over for all simple cycles has order \((N_{nodes} + N_{edges})(N_{cycles} + 1)\) complexity, which is prohibitive for large order books. Nevertheless, we explore this option here to better understand the problem of finding and valuing arbitrage opportunities.

In the following cell, we compute the loss function for all simple cycles that can be constructed within a directed graph using the function `simple_cycles`

from the `networkx`

library to construct a dictionary of all distinct simple cycles in the order book. Each cycle is represented by an ordered list of nodes. For each cycle, the financial return is computed, and a histogram is constructed to show the distribution of potential returns. Several paths with the highest return are then overlaid on the graph of the order book.

Again, note that no account is taken of the trading capacity available on each path.

```
# This cell iterates over all simple cycles in a directed graph. This
# can a long time for a large, well connected graph.
# convert order book to a directed graph
order_book_dg = order_book_to_dg(order_book)
# compute the sum of weights given a list of nodes
def sum_weights(cycle):
return sum(
[
order_book_dg.edges[edge]["weight"]
for edge in zip(cycle, cycle[1:] + cycle[:1])
]
)
# create a dictionary of all simple cycles and sum of weights
cycles = {
tuple(cycle): 10000 * (np.exp(-sum_weights(cycle)) - 1)
for cycle in nx.simple_cycles(order_book_dg)
}
print(
f"There are {len(cycles)} distinct simple cycles in the order book, {len([cycle for cycle in cycles if cycles[cycle] > 0])} of which have positive return."
)
# create histogram
plt.hist(cycles.values(), bins=int(np.sqrt(len(cycles))))
ax = plt.gca()
ax.set_ylabel("count")
ax.set_xlabel("Basis Points")
ax.set_title("Histogram of Returns for all Simple Cycles")
ax.grid(True)
ax.axvline(0, color="r")
plt.show()
```

```
There are 203147 distinct simple cycles in the order book, 974 of which have positive return.
```

Next, we sort out the negative cycles from this list and present them along with their basis-points (1% is 100 basis points) return.

```
arbitrage = [
cycle for cycle in sorted(cycles, key=cycles.get, reverse=True) if cycles[cycle] > 0
]
n_cycles_to_list = 5
print(f"Top {n_cycles_to_list}\n")
print(f"Basis Points Arbitrage Cycle")
for cycle in arbitrage[0 : min(n_cycles_to_list, len(arbitrage))]:
t = list(cycle)
t.append(cycle[0])
print(f"{cycles[cycle]:6.3f} {len(t)} trades: {' -> '.join(t)}")
```

```
Top 5
Basis Points Arbitrage Cycle
14.774 8 trades: ETH -> USD -> BUSD -> USDC -> USDT -> ADA -> BTC -> ETH
14.747 8 trades: ETH -> USD -> BUSD -> USDC -> USDT -> TRX -> BTC -> ETH
14.699 7 trades: ADA -> BTC -> USD -> BUSD -> USDC -> USDT -> ADA
14.673 7 trades: USDC -> USDT -> TRX -> BTC -> USD -> BUSD -> USDC
13.772 7 trades: ETH -> USD -> USDC -> USDT -> ADA -> BTC -> ETH
```

In the end, we draw an example arbitrage cycle on our graph to illustrate the route that the money must travel.

```
n_cycles_to_show = 1
for cycle in arbitrage[0 : min(n_cycles_to_show, len(arbitrage))]:
# get fresh graph to color nodes
order_book_dg = order_book_to_dg(order_book)
# color nodes red
for node in cycle:
order_book_dg.nodes[node]["color"] = "red"
# makes lines wide
for edge in zip(cycle, cycle[1:] + cycle[:1]):
order_book_dg.edges[edge]["width"] = 4
ax = draw_dg(order_book_dg, rad=0.05)
t = list(cycle)
t.append(cycle[0])
ax.set_title(
f"Trading cycle with {len(t)} trades: {' -> '.join(t)}\n\n Return = {cycles[cycle]:6.3f} basis points "
)
plt.savefig("crypto3.pdf", bbox_inches="tight")
plt.show()
```

## AMPL Model for Arbitrage with Capacity Constraints#

The preceding analysis demonstrates some of the practical limitations of relying on generic implementations of network algorithms:

First of all, more than one negative cycle may exist, so more than one arbitrage opportunity may exist, i.e. an optimal strategy consists of a combination of cycles.

Secondly, simply searching for a negative cycle using shortest path algorithms does not account for capacity constraints, i.e., the maximum size of each of the exchanges. For that reason, one may end up with a cycle on which a good `rateā of arbitrage is available, but where the absolute gain need not be large due to the maximum amounts that can be traded.

Instead, we can formulate the problem of searching for a maximum-gain arbitrage via linear optimization. We assume we are given a directed graph where each edge \(i\rightarrow j\) is labeled with a āmultiplierā \(a_{i\rightarrow j}\) indicating how many units of currency \(j\) will be received for one unit of currency \(i\), and a ācapacityā \(c_{i\rightarrow j}\) indicating how many units of currency \(i\) can be converted to currency \(j\).

We will break the trading process down into steps indexed by \(t = 1, 2, \ldots, T\), where currencies are exchanged between two adjacent nodes within a single step. We shall denote by \(x_{i\rightarrow j}(t)\) the currency amount traded from node \(i\) to \(j\) in step \(t\). In this way, we start with the amount \(w_{USD}(0)\) at time \(0\) and aim to maximize the amount \(w_{USD}(T)\) at time \(T\). Denote by \(O_j\) the set of nodes to which outgoing arcs from \(j\) lead, and by \(I_j\) the set of nodes from which incoming arcs lead.

A single transaction converts \(x_{i\rightarrow j}(t)\) units of currency \(i\) to currency \(j\). Following the all transactions at event \(t\), the trader will hold \(v_j(t)\) units of currency \(j\) where

For every edge \(i\rightarrow j\), the sum of all transactions must satisfy

The objective of the optimization model is to find a sequence of currency transactions that increase the holdings of a reference currency. The solution is constrained by assuming the trader cannot short sell any currency. The resulting model is

where the subsequent constraints are the:

initial amount condition,

balance equations linking the state of the given node in the previous and subsequent time periods,

constraint that we cannot trade at time step \(t\) more of a given currency than we had in this currency from time step \(t - 1\). This constraint āenforcesā the time order of trades, i.e., we cannot trade in time period \(t\) units which have been received in the same time period.

the capacity constraints related to the maximum allowed trade volumes,

non-negativity constraints.

The following Python code illustrates this formulation.

```
%%writefile crypto_model.mod
param T;
# length of the trading chain
set T0 := 0..T;
set T1 := 1..T;
# currency nodes and trading edges
set NODES;
set EDGES within NODES cross NODES;
# currency on hand at each node
var v{NODES, T0} >= 0;
# amount traded on each edge at each trade
var x{EDGES, T1} >= 0;
# total amount traded on each edge over all trades
var z{EDGES} >= 0;
# "multiplier" on each trading edge
param a{EDGES};
param c{EDGES};
param v0;
maximize wealth: v["USD", T];
s.t. total_traded {(src, dst) in EDGES}:
z[src, dst] == sum{t in T1} x[src, dst, t];
s.t. edge_capacity {(src, dst) in EDGES}:
z[src, dst] <= c[src, dst];
# initial assignment of V0 units on a selected currency
s.t. initial {node in NODES}:
v[node, 0] == (if node == "USD" then v0 else 0);
s.t. no_shorting {node in NODES, t in T1}:
v[node, t - 1] >= sum{(src, dst) in EDGES: src == node} x[node, dst, t];
s.t. balances {node in NODES, t in T1}:
v[node, t] == v[node, t - 1] +
sum{(src, dst) in EDGES: dst == node} a[src, node] * x[src, node, t] -
sum{(src, dst) in EDGES: src == node} x[node, dst, t]
;
```

```
Overwriting crypto_model.mod
```

```
def crypto_model(order_book_dg, T=10, v0=100.0):
m = AMPL()
m.read("crypto_model.mod")
m.set["NODES"] = order_book_dg.nodes
m.set["EDGES"] = order_book_dg.edges
m.param["T"] = T
m.param["a"] = [
order_book_dg.edges[(src, dst)]["a"] for (src, dst) in order_book_dg.edges
]
m.param["c"] = [
order_book_dg.edges[(src, dst)]["capacity"]
for (src, dst) in order_book_dg.edges
]
m.param["v0"] = v0
m.option["solver"] = SOLVER
m.solve()
return m
```

Using this function, we are able to compute the optimal (absolute) return from an order book while respecting the order capacities and optimally using all the arbitrage opportunities inside it.

```
order_book_dg = order_book_to_dg(order_book)
v0 = 10000.0
T = 8
m = crypto_model(order_book_dg, T=T, v0=v0)
vT = m.obj["wealth"].value()
print(f"Starting wealth = {v0:0.2f} USD")
print(f"Weath after {T:2d} transactions = {vT:0.2f} USD")
print(f"Return = {10000 * (vT - v0)/v0:0.3f} basis points")
```

```
cbc 2.10.7: cbc 2.10.7: optimal solution; objective 10009.00655
0 simplex iterations
"option abs_boundtol 3.1086244689504383e-15;"
or "option rel_boundtol 7.771561172376096e-16;"
will change deduced dual values.
Starting wealth = 10000.00 USD
Weath after 8 transactions = 10009.01 USD
Return = 9.007 basis points
```

To track the evolution of the trades throughout time, the script in the following cell illustrates, for each currency (rows) the amount of money held in that currency at each of the time steps \(t = 0, \dots, 8\). It is visible from this scheme that the sequence of trades is not a simple cycle, but rather a more sophisticated sequence of trades which we would not have discovered with simple-cycle exploration alone, especially not when considering also the arc capacities.

```
NODES = m.set["NODES"].to_list()
T0 = m.set["T0"].to_list()
v = m.var["v"].to_dict()
for node in NODES:
print(f"{node:5s}", end="")
for t in T0:
print(f" {v[node, t]:11.5f}", end="")
print()
```

```
ETH 0.00000 0.00000 0.00000 0.00000 0.00000 -0.00000 0.00000 0.00000 0.00000
BTC 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0.00004 0.00000
BNB 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000
ADA 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 2.00000 0.00000 0.00000
SOL 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000
MATIC 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 -0.00000 -0.00000
MANA 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000
TRX 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 4.00000 0.00000 0.00000
USDT 0.00000 9998.02596 0.97433 0.00000 10003.02698 0.97482 0.00000 10008.03049 0.00000
BUSD 0.00000 0.00000 10002.02677 0.97472 0.00000 10007.02979 0.00000 0.00000 0.00000
USDC 0.00000 0.97424 0.00000 10002.02677 0.97472 0.00000 10007.02979 0.00000 0.00000
DAI 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 -0.00000 -0.00000 0.00000
USD 10000.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 10009.00655
```

To be even more specific, the following cell lists the sequence of transcations executed.

```
T1 = m.set["T1"].to_list()
EDGES = m.set["EDGES"].to_list()
x = m.var["x"].to_dict()
a = m.param["a"].to_dict()
print("\nTransaction Events")
for t in T1:
print(f"t = {t}")
for src, dst in EDGES:
if x[src, dst, t] > 1e-6:
print(
f" {src:8s} -> {dst:8s}: {x[src, dst, t]:14.6f} {a[src, dst] * x[src, dst, t]:14.6f}"
)
print()
```

```
Transaction Events
t = 1
USD -> USDT : 9999.025765 9998.025962
USD -> USDC : 0.974235 0.974235
t = 2
USDT -> BUSD : 9998.025962 10002.026773
USDC -> USDT : 0.974235 0.974333
t = 3
USDT -> BUSD : 0.974333 0.974723
BUSD -> USDC : 10002.026773 10002.026773
t = 4
BUSD -> USDC : 0.974723 0.974723
USDC -> USDT : 10002.026773 10003.026976
t = 5
USDT -> BUSD : 10003.026976 10007.029787
USDC -> USDT : 0.974723 0.974820
t = 6
USDT -> ADA : 0.697500 2.000000
USDT -> TRX : 0.277320 4.000000
BUSD -> USDC : 10007.029787 10007.029787
t = 7
ADA -> BTC : 2.000000 0.000030
TRX -> BTC : 4.000000 0.000012
USDC -> USDT : 10007.029787 10008.030490
t = 8
BTC -> USD : 0.000042 0.976057
USDT -> USD : 10008.030490 10008.030490
```

We next illustrate the arbitrage strategy in the graph by marking all the corresponding arcs thicker.

```
# add comment in the text to remind the reader about bids and asks
# for each currency we took only one ask and one bid, this is why we are unique between each pair of nodes
z = m.var["z"].to_dict()
# report what orders to issue
for src, dst in EDGES:
if z[src, dst] > 0.0000002:
order_book_dg.nodes[src]["color"] = "red"
order_book_dg.nodes[dst]["color"] = "red"
order_book_dg[src][dst]["width"] = 4
draw_dg(order_book_dg, 0.05)
```

```
<Axes: >
```

If we want to be even more precise about the execution of the trading strategy, we can formulate a printout of the list of orders that we, as the counter party to the orders stated in the order book, should issue for our strategy to take place.

```
# report what orders to issue
c = m.param["c"].to_dict()
print("Trading Summary for the Order Book")
print(f" Order Book Type Capacity Traded")
for src, dst in EDGES:
if z[src, dst] > 0.0000002:
kind = order_book_dg.edges[(src, dst)]["kind"]
s = f"{src:>5s} -> {dst:<5s} {kind} {c[src, dst]:12.5f} {z[src, dst]:14.5f}"
s += " >>> "
if kind == "ask":
base = dst
quote = src
symbol = base + "/" + quote
price = 1.0 / order_book_dg.edges[(src, dst)]["a"]
volume = z[src, dst] / price
s += f"sell {volume:15.6f} {symbol:11s} at {price:12.6f}"
if kind == "bid":
base = src
quote = dst
symbol = base + "/" + quote
price = order_book_dg.edges[(src, dst)]["a"]
volume = z[src, dst]
s += f"buy {volume:16.6f} {symbol:11s} at {price:12.6f}"
print(s)
```

```
Trading Summary for the Order Book
Order Book Type Capacity Traded
BTC -> USD bid 0.00746 0.00004 >>> buy 0.000042 BTC/USD at 23373.010000
ADA -> BTC bid 2.00000 2.00000 >>> buy 2.000000 ADA/BTC at 0.000015
TRX -> BTC bid 4.00000 4.00000 >>> buy 4.000000 TRX/BTC at 0.000003
USDT -> ADA ask 178.28100 0.69750 >>> sell 2.000000 ADA/USDT at 0.348750
USDT -> BUSD ask 317048.85971 20002.02727 >>> sell 20010.031283 BUSD/USDT at 0.999600
USDT -> TRX ask 750.05354 0.27732 >>> sell 4.000000 TRX/USDT at 0.069330
USDT -> USD bid 10407.87000 10008.03049 >>> buy 10008.030490 USDT/USD at 1.000000
BUSD -> USDC ask 279879.62000 20010.03128 >>> sell 20010.031283 USDC/BUSD at 1.000000
USDC -> USDT bid 307657.00000 20011.00552 >>> buy 20011.005518 USDC/USDT at 1.000100
USD -> USDT ask 954935.16397 9999.02576 >>> sell 9998.025962 USDT/USD at 1.000100
USD -> USDC ask 517682.17000 0.97424 >>> sell 0.974235 USDC/USD at 1.000000
```

In the end, we can illustrate the time-journey of our balances in different currencies using time-indexed bar charts.

```
# display currency balances
balances = pd.DataFrame()
for node in order_book_dg.nodes:
if sum(v[node, t] for t in T0) > 0.0000002:
for t in T0:
balances.loc[t, node] = v[node, t]
balances.plot(
kind="bar",
subplots=True,
figsize=(8, 10),
xlabel="Transaction",
ylabel="Currency Units",
)
plt.gcf().tight_layout()
plt.show()
```

## Questions to the user#

The previous notebook cells made certain assumptions that we need to consider. The first assumption was that there was at most one bid and one ask order between any pair of currencies in an exchange. This assumption was based on the number of orders we downloaded from the online database, but in reality, there may be more orders. How would the presence of multiple orders per pair affect our graph formulation? How can we adjust the MILO formulation to account for this?

Another assumption was that we only traded currencies within one exchange. However, in reality, we can trade across multiple exchanges. How can we modify the graph-based problem formulation to accommodate this scenario?

Further, we have assigned no cost to the number of trades required to implement the strategy produced by optimization. How can the modeled be modified to either minimize the number of trades, or to explicitly include trading costs?

Finally, a tool like this needs to operate in real time. How can this model be incorporated into, say, a streamlit application that could be used to monitor for arbitrage opportunities in real time?

## Real Time Downloads of Order Books from an Exchange#

The goal of this notebook was to show how network algorithms and optimization can be utilized to detect arbitrage opportunities within an order book that has been obtained from a cryptocurrency exchange.

The subsequent cell in the notebook utilizes `ccxt.exchange.fetch_order_book`

to obtain the highest bid and lowest ask orders from an exchange for market symbols that meet the criteria of having a minimum in-degree for their base currencies.

```
import pandas as pd
def get_order_book(exchange, exchange_dg):
def get_orders(base, quote, limit=1):
"""
Return order book data for a specified symbol.
"""
result = exchange.fetch_order_book("/".join([base, quote]), limit)
if not result["asks"] or not result["bids"]:
result = None
else:
result["base"], result["quote"] = base, quote
result["timestamp"] = exchange.milliseconds()
result["bid_price"], result["bid_volume"] = result["bids"][0]
result["ask_price"], result["ask_volume"] = result["asks"][0]
return result
# fetch order book data and store in a dictionary
order_book = filter(
lambda r: r is not None,
[get_orders(base, quote) for quote, base in exchange_dg.edges()],
)
# convert to pandas dataframe
order_book = pd.DataFrame(order_book)
order_book["timestamp"] = pd.to_datetime(order_book["timestamp"], unit="ms")
return order_book[
[
"symbol",
"timestamp",
"base",
"quote",
"bid_price",
"bid_volume",
"ask_price",
"ask_volume",
]
]
minimum_in_degree = 5
# get graph of market symbols with mininum_in_degree for base currencies
exchange_dg = get_exchange_dg(exchange, minimum_in_degree)
# get order book for all markets in the graph
order_book = get_order_book(exchange, exchange_dg)
order_book_dg = order_book_to_dg(order_book)
# find trades
v0 = 10000.0
m = crypto_model(order_book_dg, T=12, v0=v0)
vT = m.obj["wealth"].value()
print(f"Potential Return = {10000*(vT - v0)/v0:0.3f} basis points")
display(order_book)
```

```
cbc 2.10.7: cbc 2.10.7: optimal solution; objective 10000.00128
0 simplex iterations
Potential Return = 0.001 basis points
```

symbol | timestamp | base | quote | bid_price | bid_volume | ask_price | ask_volume | |
---|---|---|---|---|---|---|---|---|

0 | ETH/BTC | 2023-07-21 19:45:31.588 | ETH | BTC | 0.063325 | 0.02360 | 0.063512 | 0.01890 |

1 | BNB/BTC | 2023-07-21 19:45:31.693 | BNB | BTC | 0.008148 | 6.18300 | 0.008156 | 3.84100 |

2 | LTC/BTC | 2023-07-21 19:45:31.738 | LTC | BTC | 0.003155 | 0.11700 | 0.003156 | 4.00000 |

3 | ADA/BTC | 2023-07-21 19:45:31.888 | ADA | BTC | 0.000010 | 176.00000 | 0.000011 | 40.10000 |

4 | LINK/BTC | 2023-07-21 19:45:31.939 | LINK | BTC | 0.000272 | 5.50000 | 0.000274 | 13.96000 |

... | ... | ... | ... | ... | ... | ... | ... | ... |

171 | SOL/USDC | 2023-07-21 19:45:42.132 | SOL | USDC | 25.720000 | 19.40000 | 25.790000 | 3.07000 |

172 | ADA/USDC | 2023-07-21 19:45:42.180 | ADA | USDC | 0.313200 | 5193.80000 | 0.316200 | 1135.70000 |

173 | BTC/DAI | 2023-07-21 19:45:42.229 | BTC | DAI | 30066.840000 | 0.01042 | 30091.810000 | 0.00172 |

174 | ETH/DAI | 2023-07-21 19:45:42.278 | ETH | DAI | 1775.010000 | 0.00780 | 1974.530000 | 0.01890 |

175 | USDT/USD | 2023-07-21 19:45:42.583 | USDT | USD | 0.998700 | 5996.00000 | 0.998800 | 13173.00000 |

176 rows Ć 8 columns

The following cell can be used to download additional order book data sets for testing.

```
from datetime import datetime
import time
import glob
# get graph of market symbols with mininum_in_degree for base currencies
minimum_in_degree = 5
exchange_dg = get_exchange_dg(exchange, minimum_in_degree)
# search time
search_time = 20
timeout = time.time() + search_time
# threshold in basis points
arb_threshold = 1.0
# wait for arbitrage opportunity
print(f"Search for {search_time} seconds.")
while time.time() <= timeout:
# print("bp = ", end="")
order_book = get_order_book(exchange, exchange_dg)
order_book_dg = order_book_to_dg(order_book)
v0 = 10000.0
m = crypto_model(order_book_dg, T=12, v0=10000)
vT = m.obj["wealth"].value()
bp = 10000 * (vT - v0) / vT
print(f"bp = {bp:0.3f}")
if bp >= arb_threshold:
print("arbitrage found!")
fname = f"{exchange} orderbook {datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.csv".replace(
" ", "_"
)
order_book.to_csv(fname)
print(f"order book saved to: {fname}")
print("Search complete.")
```

```
Search for 20 seconds.
cbc 2.10.7cbc 2.10.7: optimal solution; objective 10000.00128
0 simplex iterations
bp = 0.001
Search complete.
```