Triangular Arbitrage Explained: How It Works in Forex and Crypto (BTC Pairs)

Triangular arbitrage enforces internal consistency across three exchange rates that cannot all move independently. When the cross rate implied by two legs disagrees with the directly quoted third leg, a cyclic conversion through all three returns more of the base currency than you started with. The idea is trivial; the entire P&L lives in three-leg latency, the bid/ask you cross on each leg, and the legging risk between fills. This piece is about the cost model and the execution machinery, not the textbook cycle.

The no-arbitrage condition

Triangular arbitrage is the multiplicative analogue of the law of one price. For a cycle A → B → C → A with conversion rates expressed as "units out per unit in":

product = r(A→B) × r(B→C) × r(C→A)

In a frictionless market product = 1. The realized edge per cycle is product − 1, but every leg crosses a spread, so you transact at the ask when buying and the bid when selling. The condition for a tradeable opportunity, net of a proportional fee f per leg, is:

product_executable × (1 - f)^3 > 1

where product_executable uses the actual marketable side of each book at your size, not the mid. Because there are two directions around the triangle, you always evaluate both — exactly one can be profitable at a time, and trading the wrong one is a guaranteed loss.

Why the product is 1 in equilibrium

Cross rates are defined, not free. If EUR/USD = 1.10 and GBP/USD = 1.25, the fair EUR/GBP is 1.10 / 1.25 = 0.88, because both routes must value the same currency identically. A quoted EUR/GBP away from 0.88 is the inconsistency the triangle removes. The deeper point: with n currencies you have n(n-1)/2 possible pairs but only n-1 independent prices; every additional quote is a redundant constraint, and triangular arbitrage is the market enforcing it.

Forex: thin gaps, fast capital

The canonical FX triangle uses EUR/USD, GBP/USD, and the EUR/GBP cross. The math is identical to the crypto case; the difference is the cost structure. On majors, top-of-book spreads are a fraction of a basis point for the fastest participants, and the implied-vs-quoted discrepancy is typically smaller than the round-trip cost of crossing three spreads. The gaps that do appear are measured in microseconds and captured by co-located bank and HFT systems with direct market access.

The practical conclusion is unsentimental: retail FX accounts do not capture triangular arbitrage. The venues that surface these discrepancies are the same venues where firms with kernel-bypass networking and exchange co-location act first. The FX example is pedagogically clean but not a live retail edge.

Crypto: larger inefficiencies, harsher frictions

Crypto is more tractable because a single venue lists many pairs against BTC, ETH, and stablecoins, and pricing is less efficient than FX. A typical single-exchange triangle:

USDT → BTC → ETH → USDT     using BTC/USDT, ETH/BTC, ETH/USDT

Single-exchange triangles are the realistic target: all three legs settle in one matching engine, so there is no inter-venue transfer and legging risk is bounded by the engine's latency rather than on-chain confirmation. Cross-exchange triangles are mostly a trap — on-chain transfers are far too slow to move inventory mid-cycle, so they require pre-funded balances on every venue and collapse into an inventory-management problem.

Worked example, executed against the book

Suppose the directly quoted ETH/USDT (3,150) is rich to the BTC route (0.0500 × 60,000 = 3,000). The cycle that monetizes it is buy ETH cheaply via BTC and sell it directly. The headline gap here is exaggerated (5%); real gaps are 0.1%–0.5% and fleeting. What matters is the per-leg fee drag:

StepActionBefore feeAfter 0.1% fee
1Start30,000 USDT30,000 USDT
2Buy BTC0.5000 BTC0.4995 BTC
3Buy ETH9.990 ETH9.980 ETH
4Sell ETH for USDT31,437 USDT31,406 USDT

Three taker fees cost ~100 USDT here. In a realistic 0.3% gross opportunity, those same three fees (≈0.3% combined) erase the entire edge. The whole discipline is: the gross cross-rate inconsistency must clear three legs of fees plus the spread you cross on each plus expected slippage at your size.

A practitioner's edge check

The minimal computation, both directions, against the marketable side of each book:

def cycle_edge(legs, fee=0.001):
    """legs: list of (rate, depth) for each conversion, rate = units_out per unit_in
    at the marketable price for your size. Returns net multiplier - 1."""
    gross = 1.0
    for rate, _depth in legs:
        gross *= rate
    net = gross * (1 - fee) ** len(legs)
    return net - 1.0

def best_direction(forward_legs, reverse_legs, fee=0.001, min_edge=0.0005):
    fwd = cycle_edge(forward_legs, fee)
    rev = cycle_edge(reverse_legs, fee)
    best = max(fwd, rev)
    if best < min_edge:                 # below threshold -> don't trade
        return None
    return ("forward" if fwd >= rev else "reverse", best)

# USDT -> BTC -> ETH -> USDT, using marketable rates (units out per unit in)
forward = [(1/60_000, 0.4), (1/0.0500, 9.9), (3_150, 31_400)]
print(best_direction(forward, forward[::-1], fee=0.001, min_edge=0.0005))

The min_edge threshold is not optional decoration — it is the buffer that absorbs the slippage and timing risk your point-in-time rates don't capture. Set it well above your modeled cost. In production you stream order books over websockets, recompute on every book update, and fire all three orders as close to atomically as the venue allows. Pull data with CCXT, validate the pipeline in a backtest, and study market microstructure so your depth and slippage models match reality.

Legging risk is the real risk

"Risk-free" assumes the three legs fill simultaneously. They don't. You submit leg one, it fills, and in the milliseconds before legs two and three land the prices can move — leaving you holding BTC or ETH you didn't want, exposed to exactly the direction you were trying to neutralize. The defenses:

  • Size to the thinnest leg. The cycle can only move as much notional as the

shallowest book supports at your price tolerance.

  • Prefer maker legs where the edge allows, accepting a miss rate in exchange

for not crossing the spread and not being the one picked off.

  • Abort on partial fills. If any leg can't complete within a price band,

unwind the completed legs immediately rather than holding directional risk.

  • Account for queue and cancel latency, not just network round-trip — a

filled-then-stuck cycle is the worst case.

FrictionEffect on edgeMitigation
3× taker fees~3×f drag (e.g. 0.3% at 0.1%)Maker legs, fee tiers
Spread on each legCross 3 spreadsTrade only liquid triangles
Slippage at sizeWalks each bookSize to thinnest depth
Legging latencyDirectional exposureAtomic submit, abort rule
Stale quotesPhantom gapsTimestamp + sanity check feed
Cross-venue transferFatal in cryptoSingle-exchange only

Sizing to depth: the binding constraint

The cycle can only carry as much notional as its shallowest leg permits at your price tolerance. Computing the maximum tradeable size means walking each book and finding where the cumulative cross-rate still clears your threshold. A useful production routine sizes the cycle to the point where marginal slippage on the next unit would erase the remaining edge:

def max_cycle_size(books, fee=0.001, min_edge=0.0005, step=0.1):
    """books: list of marketable-side level lists [(price, size), ...] per leg.
    Increase size until the realized cross-rate edge drops below min_edge.
    Returns the largest size (in base units of leg 1) still profitable."""
    def avg_rate(levels, qty):
        filled, cost = 0.0, 0.0
        for price, size in levels:
            take = min(qty - filled, size)
            cost += take * price
            filled += take
            if filled >= qty:
                return cost / qty
        return None  # insufficient depth

    size, best = 0.0, 0.0
    while True:
        size += step
        rates = [avg_rate(b, size) for b in books]
        if any(r is None for r in rates):
            break
        gross = 1.0
        for r in rates:
            gross *= (1.0 / r)        # units out per unit in at this size
        edge = gross * (1 - fee) ** len(books) - 1.0
        if edge < min_edge:
            break
        best = size
    return best

The lesson generalizes: the headline edge is computed at the top of book, but the tradeable edge shrinks monotonically with size. Many "opportunities" are real for 0.1 BTC and gone by 1 BTC — which is exactly why this strategy rewards many small, fast cycles over occasional large ones.

Where the edge decays

Single-exchange triangular arbitrage is a latency-and-cost race. The same forces that closed FX gaps are at work in crypto: as more co-located, fee-rebated capital runs the same recompute-on-every-tick loop, the no-arbitrage band on liquid triangles (USDT/BTC/ETH on tier-1 venues) tightens toward the fastest player's marginal cost. The residual opportunity drifts to less liquid triangles — newer tokens, smaller venues — where the gaps are wider precisely because depth is thin and you are compensating for the slippage and timing risk of trading them. Honest expectation: this is a thin, infrastructure-sensitive edge, not a standing source of free money, and it is dominated by execution quality.

Conclusion

Triangular arbitrage is the multiplicative no-arbitrage condition made tradeable: when three linked rates fall out of consistency, a cyclic conversion captures the gap. In FX the discrepancies are smaller than the cost of crossing three spreads and belong to the fastest co-located capital. In crypto the inefficiencies are larger and single-exchange execution is feasible, but the gross gap must clear three legs of fees, three spreads, and slippage — and legging risk converts a "market-neutral" cycle into a directional position the instant one leg lags. Build the net-edge model and the abort logic first; the cycle math is the easy part.

For the broader family, compare classical arbitrage, statistical arbitrage, and pairs trading. Evaluate results with the Sharpe ratio and maximum drawdown, and wire up execution via building a trading bot.

#triangular arbitrage #arbitrage #forex trading #crypto arbitrage #btc pairs #algorithmic trading