website/content/blog/crypto-arbitrage-strategies.mdx
"Arbitrage" covers half a dozen genuinely different strategies, with different capital requirements, speed requirements, and risks. This post maps the popular ones — what the trade is, why the edge exists, and the CCXT machinery each one runs on. It pairs with the hands-on spread scanner tutorial; this is the strategy layer above it.
The trade: the same asset is cheaper on venue A than venue B. Buy on A, sell on B.
The naive version — buy, withdraw, deposit, sell — rarely survives contact with reality: transfers take minutes to hours and the spread is gone long before the coins arrive. The version that works is inventory-based: hold both the asset and the quote currency on both venues in advance, execute the two legs simultaneously, and rebalance inventory occasionally during quiet periods (or accept the drift). No transfer sits on the critical path; the transfer cost becomes an amortized rebalancing cost.
Where the edge comes from: fragmented liquidity. Smaller venues lag larger ones by seconds, and retail flow pushes prices around on venues where books are thin.
CCXT gives you: one API across all venues — fetchTickers / watchOrderBook on both
sides, createOrder on both sides, fetchCurrencies to check whether deposits/withdrawals
on a network are live before you count on rebalancing through it. This strategy is why
people reach for a multi-exchange library in the first place.
The trade: three markets on one exchange form a cycle — for example BTC/USDT, ETH/BTC, ETH/USDT. Multiply the implied rates around the loop; if the product beats 1.0 after three sets of fees, convert around the cycle and end with more than you started.
Because everything happens on one venue there's no transfer risk and no inventory split — but the loop must execute fast, the three legs are not atomic (leg two can fail after leg one filled), and on major exchanges these gaps are hunted by very fast players. Realistic opportunities cluster in less-liquid pairs, where slippage is the counterweight.
CCXT gives you: loadMarkets() to build the currency graph and enumerate cycles,
watchOrderBookForSymbols to stream all three books in one loop, and precision/limits
metadata so each leg's amount is valid before you fire it.
The trade: perpetual swaps pay funding between longs and shorts every few hours to keep the perp pinned to spot. When funding is positive (the common state in bull markets), shorts get paid. So: buy spot, short the same notional in the perp. Price risk nets to roughly zero; you collect funding.
This is the institutional favorite because it scales and doesn't need speed. The risks are subtler: funding flips negative and the carry becomes a cost; the perp leg needs margin and can be liquidated if it's under-collateralized while the basis moves; and entry/exit execution costs two spreads.
CCXT gives you: fetchFundingRates() for a venue-wide snapshot, fetchFundingRateHistory
for backtesting the carry, unified derivatives symbols (BTC/USDT:USDT is the linear perp),
plus position and margin data on the private side. A funding scanner is a few lines:
import ccxt from 'ccxt';
for (const id of ['binance', 'bybit', 'okx', 'gate']) {
const exchange = new ccxt[id]();
const rates = await exchange.fetchFundingRates();
for (const [symbol, entry] of Object.entries(rates)) {
if (entry.fundingRate && Math.abs(entry.fundingRate) > 0.0005) {
console.log(id.padEnd(10), symbol.padEnd(22),
`${(entry.fundingRate * 100).toFixed(4)}%`);
}
}
await exchange.close();
}
import ccxt
for exchange_id in ['binance', 'bybit', 'okx', 'gate']:
exchange = getattr(ccxt, exchange_id)()
rates = exchange.fetch_funding_rates()
for symbol, entry in rates.items():
rate = entry['fundingRate']
if rate and abs(rate) > 0.0005: # 0.05% per interval
print(f'{exchange_id:10} {symbol:22} {rate * 100:+.4f}%')
<?php
foreach (['binance', 'bybit', 'okx', 'gate'] as $id) {
$class = "\\ccxt\\{$id}";
$exchange = new $class();
$rates = $exchange->fetch_funding_rates();
foreach ($rates as $symbol => $entry) {
$rate = $entry['fundingRate'];
if ($rate && abs($rate) > 0.0005) { // 0.05% per interval
printf("%-10s %-22s %+.4f%%\n", $id, $symbol, $rate * 100);
}
}
}
using ccxt;
foreach (var id in new[] { "binance", "bybit", "okx", "gate" })
{
var exchange = Exchange.DynamicallyCreateInstance(id);
var rates = await exchange.FetchFundingRates();
foreach (var (symbol, entry) in rates.fundingRates)
{
if (entry.fundingRate is double rate && Math.Abs(rate) > 0.0005)
{
Console.WriteLine($"{id,-10} {symbol,-22} {rate * 100:+0.0000;-0.0000}%");
}
}
}
package main
import (
"fmt"
"math"
ccxt "github.com/ccxt/ccxt/go/v4"
)
func main() {
for _, id := range []string{"binance", "bybit", "okx", "gate"} {
exchange := ccxt.CreateExchange(id, nil)
rates, err := exchange.FetchFundingRates()
if err != nil {
panic(err)
}
for symbol, entry := range rates.FundingRates {
if entry.FundingRate != nil && math.Abs(*entry.FundingRate) > 0.0005 {
fmt.Printf("%-10s %-22s %+.4f%%\n", id, symbol, *entry.FundingRate*100)
}
}
}
}
import io.github.ccxt.Exchange;
import java.util.Map;
public class FundingScanner {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
for (var id : new String[] { "binance", "bybit", "okx", "gate" }) {
var exchange = Exchange.dynamicallyCreateInstance(id, null);
var rates = (Map<String, Object>) exchange.fetchFundingRates().join();
for (var entry : rates.entrySet()) {
var data = (Map<String, Object>) entry.getValue();
var rate = (Number) data.get("fundingRate");
if (rate != null && Math.abs(rate.doubleValue()) > 0.0005) {
System.out.printf("%-10s %-22s %+.4f%%%n",
id, entry.getKey(), rate.doubleValue() * 100);
}
}
}
}
}
At 0.05% every 8 hours, the gross annualized carry is roughly 55% — which is why crowded trades compress it quickly. Compare across venues: the same perp often funds very differently on different exchanges, and shorting where funding is highest is itself a selection edge.
The dated-futures cousin of funding arbitrage: quarterly futures trade at a premium to spot,
and the premium must converge to zero at expiry. Buy spot, short the future, hold to expiry,
pocket the basis — a fixed-term, known-in-advance yield, at the cost of capital efficiency
and margin management on the short leg. CCXT exposes dated futures as unified markets
(market['future']) with expiry metadata, so scanning the term structure is the same
loadMarkets + fetchTickers pattern as everything else.
Everything above is (near-)riskless in principle. Stat-arb is not: it bets that a historical price relationship — between correlated alts, or an alt and its sector — will mean-revert after diverging. Model the spread, enter at a z-score threshold, exit on reversion. The "arbitrage" in the name is aspirational: relationships break, and 2021's cointegration is 2022's divergence.
CCXT gives you: fetchOHLCV across hundreds of venues for research, and the same
execution layer as everything else when the signal fires. The hard part here is the
statistics, not the plumbing.
market['taker'], market['maker']), depth before
tickers, testnets before mainnet, and the
seven classic mistakes apply
double when two venues are involved.