◈   ∿ algotrading · Intermediate

Triangular Arbitrage Python Tutorial for Crypto Traders

Build a working triangular arbitrage scanner in Python. Learn to detect price loops across BTC, ETH, and USDT pairs on Binance, OKX, and Bybit with real code.

Uncle Solieditor · voc · 06.05.2026 ·views 19
◈   Contents
  1. → What Is Triangular Arbitrage?
  2. → The Math Behind the Profit Calculation
  3. → Setting Up Your Python Environment
  4. → Building the Arbitrage Detection Scanner
  5. → Handling Fees, Slippage, and Real-World Execution
  6. → Extending the Scanner: Automated Triangle Discovery
  7. → Frequently Asked Questions

Triangular arbitrage is one of those strategies that sounds like magic when you first hear it — make a profit by trading three pairs in a loop, ending up with more than you started with, all on the same exchange. No price movement needed, no directional bet. Just exploiting the fact that exchange rate math occasionally doesn't add up perfectly. The catch? These windows last milliseconds, fees can wipe the gain entirely, and most retail traders have no idea how to even check for them. This tutorial builds a working Python scanner from scratch and shows you exactly what to look for — and what to be realistic about.

What Is Triangular Arbitrage?

Imagine you're at an airport with $1,000 in cash. You exchange dollars for euros, euros for British pounds, and pounds back to dollars. If the exchange rates are slightly misaligned, you might end up with $1,008. That's triangular arbitrage — exploiting a pricing inconsistency across three currency pairs in a loop. In crypto, the same logic applies. You start with one asset, trade through two intermediate pairs, and land back on the original asset with more than you started.

On Binance, for example, you might execute this loop: sell BTC for USDT, use that USDT to buy ETH, then sell ETH back to BTC. If the implied BTC price across those two hops is lower than the direct BTC/USDT price, you've found a discrepancy. The key word is implied — you're comparing what the market says BTC is worth directly versus what it's worth when you route through ETH.

Key Takeaway: Triangular arbitrage is market-neutral — you're not betting on price direction. You're exploiting a temporary mathematical inconsistency between three trading pairs on the same exchange.

The Math Behind the Profit Calculation

The profit ratio for a triangle is calculated by multiplying the effective exchange rates across all three legs. Start with 1 unit of your base asset. After leg 1 you hold some amount of the quote asset. After leg 2 you hold some amount of the bridge asset. After leg 3 you're back to the base asset. If the final number is greater than 1, there's a gross profit — before fees.

Example Triangle Calculation (BTC → USDT → ETH → BTC)
StepActionPriceBalance After
StartHold BTC1.00000 BTC
Leg 1Sell BTC/USDTbid = 65,00065,000 USDT
Leg 2Buy ETH/USDTask = 3,25020.0000 ETH
Leg 3Sell ETH/BTCbid = 0.0500251.00050 BTC
ResultGross profit+0.0005 BTC (0.05%)

That 0.05% looks small, but on $100,000 that's $50 per cycle — theoretically. The problem is that three Binance trades at the standard 0.1% taker fee each costs you 0.3% total. So 0.05% gross turns into -0.25% net. This is why you almost never see clean triangular arbitrage opportunities that survive fees in practice. When they do appear, they last for fractions of a second, and high-frequency trading bots on the exchange's co-located servers get there first.

Warning: Most triangular arbitrage opportunities you'll detect with a scanner are ghost opportunities — by the time your order hits the matching engine, prices have already corrected. Always account for latency and fees before going live.

Setting Up Your Python Environment

The industry-standard library for connecting to crypto exchange APIs from Python is ccxt (CryptoCurrency eXchange Trading Library). It supports over 100 exchanges including Binance, OKX, Bybit, KuCoin, and Gate.io with a unified interface — meaning the same code works across all of them with minimal changes. Install the dependencies:

pip install ccxt python-dotenv

Create a .env file in your project root to store your API credentials securely. Never hardcode keys in your source files.

# .env
BINANCE_API_KEY=your_api_key_here
BINANCE_SECRET=your_secret_here
import ccxt
import os
from dotenv import load_dotenv

load_dotenv()

# Initialize Binance connection
exchange = ccxt.binance({
    'apiKey': os.getenv('BINANCE_API_KEY'),
    'secret': os.getenv('BINANCE_SECRET'),
    'enableRateLimit': True,  # respect rate limits automatically
})

# Load all available markets
markets = exchange.load_markets()
print(f"Loaded {len(markets)} markets from Binance")

Building the Arbitrage Detection Scanner

The core of the scanner is a function that checks whether a given triangle is profitable after fees. We pull live order book data for each pair and calculate the profit ratio in real time. Using order book prices (bid/ask) rather than last-trade prices is critical — it reflects what you'd actually get if you executed right now.

import time

FEE_PCT = 0.001  # 0.1% Binance taker fee per trade

def get_best_price(symbol, side='ask'):
    """Fetch best bid or ask from live order book."""
    ob = exchange.fetch_order_book(symbol, limit=5)
    if side == 'ask':
        return ob['asks'][0][0]  # lowest ask (you buy here)
    return ob['bids'][0][0]      # highest bid (you sell here)

def check_triangle(base, quote, bridge):
    """
    Route: base -> quote -> bridge -> base
    Example: BTC -> USDT -> ETH -> BTC
    """
    pair1 = f"{base}/{quote}"    # e.g., BTC/USDT
    pair2 = f"{bridge}/{quote}" # e.g., ETH/USDT
    pair3 = f"{bridge}/{base}"  # e.g., ETH/BTC

    # Check all pairs exist on this exchange
    if not all(p in exchange.markets for p in [pair1, pair2, pair3]):
        return None

    try:
        p1 = get_best_price(pair1, 'bid')   # sell base, get quote
        p2 = get_best_price(pair2, 'ask')   # buy bridge with quote
        p3 = get_best_price(pair3, 'bid')   # sell bridge, get base

        # Profit ratio (1.0 = break even before fees)
        gross_ratio = (p1 / p2) * p3
        gross_pct   = (gross_ratio - 1) * 100

        # Subtract fees for 3 legs
        net_pct = gross_pct - (FEE_PCT * 3 * 100)

        return {
            'route':     f"{base}→{quote}→{bridge}→{base}",
            'gross_pct': round(gross_pct, 5),
            'net_pct':   round(net_pct, 5),
            'prices':    (p1, p2, p3),
        }
    except Exception as e:
        return None


def scan_all_triangles(min_net_pct=0.0):
    """Scan a predefined list of common triangles."""
    candidates = [
        ('BTC', 'USDT', 'ETH'),
        ('BTC', 'USDT', 'BNB'),
        ('BTC', 'USDT', 'SOL'),
        ('ETH', 'USDT', 'BNB'),
        ('BTC', 'ETH',  'BNB'),
        ('BTC', 'ETH',  'SOL'),
    ]

    found = []
    for triangle in candidates:
        result = check_triangle(*triangle)
        if result and result['net_pct'] > min_net_pct:
            found.append(result)

    return found


# Run scanner loop
print("Scanning Binance for triangular arbitrage...")
while True:
    opportunities = scan_all_triangles(min_net_pct=0.0)

    if opportunities:
        for opp in opportunities:
            print(f"[FOUND] {opp['route']}")
            print(f"  Gross: {opp['gross_pct']}%  |  Net after fees: {opp['net_pct']}%")
    else:
        print(f"[{time.strftime('%H:%M:%S')}] No net-positive opportunities")

    time.sleep(1)

This scanner runs continuously, checking six common triangles every second. In reality you'd want to reduce latency by using WebSocket streams rather than REST calls, and you'd want to cache the market data rather than fetching it fresh each loop. But as a learning tool, this REST-based version makes the logic crystal clear and easy to debug.

Handling Fees, Slippage, and Real-World Execution

Three things will destroy a theoretical profit in live trading: fees, slippage, and latency. You've already accounted for fees in the scanner above, but slippage is trickier. When you place a market order on Bybit or OKX, you're not guaranteed to fill at the best ask — if your order size exceeds the top level of the order book, you'll eat into the next levels at worse prices. For a $10,000 trade, even a 0.05% slippage hit per leg adds up to 0.15% across three legs.

For real-time monitoring of market conditions and spotting when volatility creates larger inefficiencies, tools like VoiceOfChain can complement your scanner. When VoiceOfChain signals a major price move on BTC or ETH, those are exactly the moments when triangular pricing can briefly go out of sync across pairs — a moving price in one pair takes a few milliseconds to propagate to correlated pairs, which is when genuine windows open.

Key Takeaway: On most days, triangular arbitrage on major pairs like BTC/USDT/ETH is essentially zero after fees. The strategy becomes more viable on less-liquid pairs, during volatile market events, or if you have VIP fee tiers. Use the scanner as a market microstructure learning tool first.

Extending the Scanner: Automated Triangle Discovery

Instead of hardcoding six triangles, you can programmatically discover all valid triangles from the exchange's market list. This is especially useful on exchanges like Gate.io and KuCoin which list hundreds of altcoin pairs — the pricing inefficiencies there are larger (though so is the spread risk).

from itertools import permutations

def find_all_triangles(markets_dict):
    """
    Auto-discover all valid triangles from exchange market list.
    A valid triangle: A/B, C/B, C/A all exist simultaneously.
    """
    # Extract unique base and quote currencies
    all_symbols = set(markets_dict.keys())
    currencies  = set()
    for sym in all_symbols:
        if '/' in sym:
            b, q = sym.split('/')
            currencies.add(b)
            currencies.add(q)

    triangles = []
    for base, quote, bridge in permutations(currencies, 3):
        p1 = f"{base}/{quote}"
        p2 = f"{bridge}/{quote}"
        p3 = f"{bridge}/{base}"
        if p1 in all_symbols and p2 in all_symbols and p3 in all_symbols:
            triangles.append((base, quote, bridge))

    return triangles

# Usage
all_triangles = find_all_triangles(exchange.markets)
print(f"Found {len(all_triangles)} valid triangles on Binance")

Running this on Binance typically reveals several thousand valid triangles. Most of them involve low-liquidity pairs where the spread alone consumes any apparent opportunity. Focus your live scanning on triangles that include at least one major stablecoin leg (USDT, USDC) and assets with genuine trading volume — the order books are tighter and execution is more predictable.

Frequently Asked Questions

Is triangular arbitrage actually profitable in crypto?
For most retail traders, no — at least not consistently. Fees eat most opportunities, and high-frequency bots on co-located servers capture the rest within milliseconds. That said, it's occasionally viable during high-volatility events on less-liquid altcoin pairs on exchanges like KuCoin or Gate.io where pricing lags more.
Do I need an API key to run the scanner?
For read-only scanning using public order book data, you don't need API keys at all — ccxt's fetch_order_book() works without authentication. You only need keys when placing actual orders. Start without keys to validate the logic before connecting any real funds.
How fast does the scanner need to be to catch real opportunities?
Realistically, sub-100ms per full cycle. The REST-based scanner in this tutorial runs in roughly 1-3 seconds per scan depending on your latency to the exchange. For live trading you'd need WebSocket streams and co-location, which is a significant engineering lift. The REST scanner is fine for learning and research.
Can I run this same code on Bybit or OKX?
Yes — ccxt abstracts the exchange differences. Change ccxt.binance() to ccxt.bybit() or ccxt.okx() and update the API credentials. You may need to adjust the market symbol format slightly since some exchanges use BTC/USDT:USDT notation for perpetuals. Stick to spot markets when starting out.
What's the minimum capital needed to start?
For testing, most exchanges allow minimum order sizes of $5-20 equivalent. The strategy itself doesn't require large capital — the challenge is that small position sizes mean tiny absolute profits even when the percentage works out. A 0.05% net gain on $100 is $0.05, which isn't worth the execution risk.
How do I prevent the bot from placing orders on ghost opportunities?
Add a confirmation delay — check the same triangle twice with 200-500ms between checks, and only execute if both scans show the same positive result. Also implement a maximum slippage tolerance in your order logic. Using limit orders instead of market orders avoids paying the spread but risks non-execution.

Triangular arbitrage in Python is one of the best ways to deeply understand crypto market microstructure — even if you never trade it live. Building this scanner forces you to think about bid/ask spreads, order book depth, fee structures, and the speed at which prices propagate across correlated pairs. That knowledge transfers directly to better decision-making in every other trading strategy you run. Start with the basic scanner, watch it output data for a few hours, and pay attention to which triangles come closest to positive most often — that market intelligence alone is worth the time investment.

◈   more on this topic
⌘ api Kraken API Documentation for Crypto Traders: Essentials and Examples ◉ basics Mastering the ccxt library documentation for crypto traders