Skip to main content
Ctrl+K
Hands-On Mathematical Optimization with AMPL in Python - Home Hands-On Mathematical Optimization with AMPL in Python - Home
  • MO-BOOK: Hands-On Mathematical Optimization with AMPL in Python šŸ
  • 1. Mathematical Optimization
    • A Production Planning Problem
    • A Basic AMPL Model
    • A Data-Driven AMPL Model
  • 2. Linear Optimization
    • BIM production
    • LAD Regression
    • MAD portfolio optimization
    • Wine quality prediction with L1 regression
    • BIM production for worst case
    • BIM production variants
    • BIM production using demand forecasts
    • Extra material: Multi-product facility production
  • 3. Mixed Integer Linear Optimization
    • BIM production with perturbed data
    • Workforce shift scheduling
    • Production model using disjunctions
    • Machine Scheduling
    • Recharging strategy for an electric vehicle
    • BIM production revisited
    • Extra material: Cryptarithms puzzle
    • Extra material: Strip packing
    • Extra material: Job shop scheduling
    • Extra material: Maintenance planning
  • 4. Network Optimization
    • Dinner seating arrangement
    • Gasoline distribution
    • Cryptocurrency arbitrage search
    • Extra material: Energy dispatch problem
    • Forex Arbitrage
  • 5. Convex Optimization
    • Milk pooling and blending
    • Ordinary Least Squares (OLS) Regression
    • Markowitz portfolio optimization
    • Support Vector Machines for Binary Classification
    • Extra material: Refinery production and shadow pricing with CVXPY
    • Extra Material: Cutting Stock
  • 6. Conic Optimization
    • Economic Order Quantity
    • The Kelly Criterion
    • Markowitz portfolio optimization revisited
    • Optimal Design of Multilayered Building Insulation
    • Training Support Vector Machines with Conic Optimization
    • Extra material: Luenberger’s Investment Wheel
    • Extra material: Optimal Growth Portfolios with Risk Aversion
  • 7. Accounting for Uncertainty: Optimization Meets Reality
    • Fleet assignment problem
    • Robustness analysis of BIM production plan via simulations
  • 8. Robust Optimization - Single Stage Problems
    • Robust BIM microchip production problem
  • 9. Stochastic Optimization - Single Stage Problems
    • Pop-up shop
    • Markowitz portfolio optimization with chance constraints
    • Stock optimization for seafood distribution center
    • Economic dispatch in energy systems
  • 10. Two-Stage Problems
    • Airline seat allocation problem
    • Optimal power flow problem with recourse actions
    • Two-stage Production Planning
    • Extra: The farmer’s problem and its variants
    • Extra: Two-stage energy dispatch optimization with wind curtailment
  • Index
  • Colab logo Colab
  • Repository
  • Open issue
  • .ipynb
    AMPL Resources
  • AMPL.com
  • AMPL Support Forum
  • AMPL Download Portal
  • AMPL Development Resources
  • AMPL + Python 🐍
  • AMPL on Streamlit Cloud
  • Hands-On Optimization in Python
  • AMPL Model Colaboratory
  • amplpy: Python API
  • More Resources
  • AMPL Books
  • MP: Solver Interface Framework
  • AMPL’s Open-Source Projects
  • Licenses & Pricing
  • AMPL & Solvers pricing
  • Free Academic Licenses
  • Free AMPL Community Edition
  • Social Media
  • LinkedIn

Cryptocurrency arbitrage search

Contents

  • Installations and Imports
    • AMPL and Solvers
  • Cryptocurrency exchanges
  • Representing an exchange as a directed graph
  • Exchange order book
  • Modelling the arbitrage search problem as a graph
  • Trading and Arbitrage
  • Find order books that demonstrate arbitrage opportunities
  • Brute force search arbitrage with simple cycles
  • AMPL Model for Arbitrage with Capacity Constraints
  • Questions to the user
  • Real Time Downloads of Order Books from an Exchange

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
../../_images/2ac0af5f8fc6db451709c59afaa738c39cd16a201cd55f4388d8f5565b2a7896.png

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/ampl/mo-book.ampl.com/refs/heads/dev/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

\[\begin{split} \begin{align*} \Delta V_b & = - x_{b\rightarrow q} \\ \Delta V_q & = a_{b\rightarrow q} x_{b\rightarrow q}, \end{align*} \end{split}\]

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

\[ x_{b\rightarrow q} \leq c_{b\rightarrow q}. \]

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:

\[\begin{split} \begin{align*} a_{b\rightarrow q} & = \text{bid price} \\ c_{b\rightarrow q} & = \text{bid volume} \end{align*} \end{split}\]

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

\[\begin{split} \begin{align*} a_{q\rightarrow b} & = \frac{1}{\text{ask price}} \\ c_{q\rightarrow b} & = \text{ask volume} \times \text{ask volume} \end{align*} \end{split}\]

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()
../../_images/1b04b233d117cffdc2b173f593b58f3e77cb8dc107e05079a8af813a37c74734.png

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

\[ \begin{align*} i_0 \rightarrow i_1 \rightarrow i_2 \rightarrow \cdots \rightarrow i_{n-1} \rightarrow i_n \end{align*} \]

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

\[ \begin{equation} \prod_{k=0}^{n-1} a_{i_k\rightarrow i_{k+1}} > 1. \end{equation} \]

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:

\[ \begin{align*} -\log(\prod_{k=0}^{n-1} a_{i_k\rightarrow i_{k+1}}) = - \sum\limits_{k = 0}^{n-1} \log (a_{i_k\rightarrow i_{k+1}}) < 0, \end{align*} \]

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()
../../_images/a5614443a52a8092aaebcb99ea96bff35b7a53f91858bcd89cd763469969bb3d.png

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.
../../_images/1b564191d167bb0a3d1a7e0d8642cab4fcae5dc93cfaf30b140de5811b790c13.png

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()
../../_images/16016960fec02d5d49bb624a87f9cba29c0b2d1cab422e1e6aaa88867381fee8.png

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

\[v_{j}(t) = v_{j}(t-1) + \sum_{i\in I_j} a_{i\rightarrow j}x_{i\rightarrow j}(t) - \sum_{k\in O_j} x_{j\rightarrow k}(t)\]

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

\[\sum_{t=1}^T x_{j\rightarrow k}(t) \leq c_{j\rightarrow k}\]

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

\[\begin{split} \begin{align*} \max \quad & v_{USD}(T) \\ \\ \text{s.t.} \quad & v_{USD}(0) = v_0 \\ \\ & v_{j}(t) = v_{j}(t-1) + \sum_{i\in I_j} a_{i\rightarrow j}x_{i\rightarrow j}(t) - \sum_{k\in O_j} x_{j\rightarrow k}(t) & \forall j\in N, t=1, 2, \ldots, T \\ & v_j(t-1) \geq \sum_{k\in O_j} x_{j\rightarrow k}(t) & \forall j\in N, t = 1, 2, \ldots, T \\ & \sum_{t=1}^T x_{j\rightarrow k}(t) \leq c_{j\rightarrow k} & \forall (j, k) \in E, t = 1, 2, \ldots, T \\ & v_{j}(t), x_{i\rightarrow j}(t) \geq 0 && \forall t \end{align*} \end{split}\]

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.solve(solver=SOLVER)
    assert m.solve_result == "solved", m.solve_result

    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: >
../../_images/a665daa409106ed6b947df60108a46682d2960adb4bc04bd5e20a417be721719.png

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()
../../_images/e26eb750e010fdd13d98e36d007d32fed916e47bd94981243fbd5d3df3a10fca.png

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.

previous

Gasoline distribution

next

Extra material: Energy dispatch problem

Contents
  • Installations and Imports
    • AMPL and Solvers
  • Cryptocurrency exchanges
  • Representing an exchange as a directed graph
  • Exchange order book
  • Modelling the arbitrage search problem as a graph
  • Trading and Arbitrage
  • Find order books that demonstrate arbitrage opportunities
  • Brute force search arbitrage with simple cycles
  • AMPL Model for Arbitrage with Capacity Constraints
  • Questions to the user
  • Real Time Downloads of Order Books from an Exchange

By The MO Book Group

Ā© Copyright 2025.