# Algorithmic Crypto Arbitraging

A complete introduction to crypto arbitraging with JavaScript and Mida.

Arbitrage is the simultaneous purchase and sale of the same asset in different markets in order to profit from differences in the asset's listed price. In crypto markets, arbitrage between two exchanges is done when the ask price of an asset pair in one exchange is lower than the bid price in the other one.

# Configuration

const binanceAccount = await login("Binance/Spot", { /* ... */ });
const krakenAccount = await login("Kraken/Spot", { /* ... */ });
const symbol = "ETHUSDT";

# Real-time exchange rates

import { createMarketWatcher, } from "@reiryoku/mida";

const exchangeRates = {};
const binanceWatcher = await createMarketWatcher(binanceAccount, {
    "ETHUSDT": { watchTicks: true, },
});
const krakenWatcher = await createMarketWatcher(krakenAccount, {
    "ETHUSDT": { watchTicks: true, },
});

binanceWatcher.on("tick", (event) => {
    const { bid, ask, } = event.descriptor.tick;
    exchangeRates["Binance"] = { bid, ask, };
    
    // Used to check for arbitrage opportunities
    onUpdate();
});
krakenWatcher.on("tick", (event) => {
    const { bid, ask, } = event.descriptor.tick;
    exchangeRates["Kraken"] = { bid, ask, };

    // Used to check for arbitrage opportunities
    onUpdate();
});

# Arbitrage execution

# onUpdate()

const onUpdate = async () => {
    const binanceRate = exchangeRates["Binance"];
    const krakenRate = exchangeRates["Kraken"];
    
    if (!binanceRate || !krakenRate) {
        return;
    }
    
    let fromAccount;
    let toAccount;
    let spread;
    
    // Arbitrage opportunity: BUY on Binance AND SELL on Kraken
    if (binanceRate.ask.lessThan(krakenRate.ask) && krakenRate.bid.greaterThan(binanceRate.ask)) {
        fromAccount = binanceAccount;
        toAccount = krakenAccount;
        spread = krakenRate.bid.subtract(binanceRate.ask);
    }
    else if (krakenRate.ask.lessThan(binanceRate.ask) && binanceRate.bid.greaterThan(krakenRate.ask)) {
        fromAccount = krakenAccount;
        toAccount = binanceAccount;
        spread = binanceRate.bid.subtract(krakenRate.ask);
    }
    
    if (!fromAccount || !toAccount || !spread) {
        return; // No arbitrage opportunities
    }

    console.log("Arbitrage Opportunity Detected");
    console.log(`Estimated gross profit for 1 ETH is ${spread}`);
    
    // <phase-1>
    await fromAccount.placeOrder({
        symbol: "ETHUSDT",
        direction: MidaOrderDirection.BUY,
        volume: 1,
    });
    await fromAccount.withdraw({
        asset: "ETH",
        address: toAccount.getCryptoAssetDepositAddress("ETH"),
        volume: 1,
    });
    // </phase-1>
    
    // <phase-2>
    await toAccount.placeOrder({
        symbol: "ETHUSDT",
        direction: MidaOrderDirection.SELL,
        volume: 1,
    });
    // Optionally, to repeat the arbitrage the next tick, if possible, move the USDT back
    await toAccount.withdraw({
        asset: "ETH",
        address: fromAccount.getCryptoAssetDepositAddress("ETH"),
        volume: 1,
    });
    // </phase-2>
};

# Risk reduction

To further elaborate the arbitrage execution, once the funds arrived on the target exchange, we can check if the arbitrage is still valid, in negative case we can avoid placing the sell order and limit the losses just to the transfer and buying fees.

# Arbitrage system

Suppose that we have access to 3, 5, 7, 10, 20 exchanges. In that case we can relaborate the arbitrage algorithm and make it generic.

class CryptoArbitrageSystem {
    #tradingAccounts;
    #symbols;
    #exchangeRates;
    #pendingArbitrages;
    
    constructor ({ tradingAccounts, symbols, }) {
        this.#tradingAccounts = tradingAccounts;
        this.#symbols = symbols;
        this.#exchangeRates = new WeakMap();
        this.#pendingArbitrages = new WeakMap();
    }
    
    async #scanMarkets () {
        for (const tradingAccountA of this.#tradingAccounts) {
            for (const tradingAccountB of this.#tradingAccounts) {
                if (tradingAccountA === tradingAccountB) {
                    continue;
                }
                
                for (const symbol of this.#symbols) {
                    const spread = await this.#getArbitrageSpread(tradingAccountA, tradingAccountB, symbol);
                    
                    if (spread) {
                        console.log("Arbitrage Opportunity Detected");
                        console.log(`Estimated gross profit for 1 ETH is ${spread}`);
                        await this.#doArbitrage(tradingAccountA, tradingAccountB, symbol);
                    }
                }
            }
        }
    }
    
    async getArbitrageSpread (tradingAccountA, tradingAccountB, symbol) {
        const exchangeRateA = this.#exchangeRates.get(tradingAccountA)?.get(symbol);
        const exchangeRateB = this.#exchangeRates.get(tradingAccountB)?.get(symbol);
        const { baseAsset, } = await tradingAccountA.getSymbol(symbol);

        if (!exchangeRateA || !exchangeRateB) {
            return undefined;
        }

        let fromAccount;
        let toAccount;
        let spread;
        
        if (exchangeRateA.ask.lessThan(exchangeRateB.ask) && exchangeRateB.bid.greaterThan(exchangeRateA.ask)) {
            fromAccount = tradingAccountA;
            toAccount = tradingAccountB;
            spread = exchangeRateB.bid.subtract(exchangeRateA.ask);
        }
        else if (exchangeRateB.ask.lessThan(exchangeRateA.ask) && exchangeRateA.bid.greaterThan(exchangeRateB.ask)) {
            fromAccount = tradingAccountB;
            toAccount = tradingAccountA;
            spead = exchangeRateA.bid.subtract(exchangeRateB.ask);
        }

        if (!fromAccount || !toAccount || !spread) {
            // No arbitrage opportunities found
            return undefined;
        }
        
        return spread;
    }
    
    async #doArbitrage (fromAccount, toAccount, symbol) {
        if (!this.#pendingArbitrages.has(fromAccount)) {
            const buyOrder = await fromAccount.placeOrder({
                symbol,
                direction: MidaOrderDirection.BUY,
                volume: 1,
            });

            this.#pendingArbitrages.set(fromAccount, {
                toAccount,
                buyPrice: buyOrder.executionPrice,
            });
            
            await fromAccount.withdraw({
                asset: baseAsset,
                address: await toAccount.getCryptoAssetDepositAddress("ETH"),
                volume: 1,
            });
        }
        
        const { freeVolume, } = await toAccount.getAssetBalance(baseAsset);
        
        if (freeVolume.greaterThanOrEqual(1)) {
            await toAccount.placeOrder({
                symbol,
                direction: MidaOrderDirection.SELL,
                volume: 1,
            });
            
            this.#pendingArbitrages.delete(fromAccount);
        }
    }
}