Triangular Arbitrage — Complete Guide
A 4th arbitrage strategy currently in prototype/validation — theory, math and how to read results.
1. What Triangular Arbitrage actually is
Simple Arbitrage, Market Maker and Spread Sniper all compare the same asset across two different exchanges. Triangular Arbitrage compares three related markets instead — and, crucially, all three can live on the same exchange:
base/Q1— e.g. ETH/USDTbase/Q2— e.g. ETH/BTCQ2/Q1, the bridge — e.g. BTC/USDT
In theory, the price of base/Q2 should always be consistent with base/Q1 divided by Q2/Q1 — the implied cross-rate. When the real, quoted price drifts from that implied rate, there's a potential opportunity: trading through all three markets in sequence closes a loop that, if the drift is real and tradeable, returns more capital than you put in.
Structural advantage over the other 3 strategies
Run on a single exchange, Triangular Arbitrage needs zero transfers between platforms — everything happens inside one account. That completely sidesteps the "which network, which exchange has that asset available for withdrawal right now" problem that the other three strategies still have to deal with.
The trade-off: the prototype has also been extended to run across 2 exchanges simultaneously. In that cross-exchange mode you lose the "zero transfers" advantage — you need capital pre-positioned in all three assets (Q1, base, Q2) on both exchanges, exactly like Simple/Market Maker Arbitrage already require, and inter-exchange latency risk comes back into the picture.
2. The math
2.1 Two possible cycles
Given a bridge Q2/Q1, there are always two directions to close the triangle (3 taker/taker/taker trades):
- Cycle 1 (clockwise):
Q1 → base → Q2 → Q1 - Cycle 2 (counter-clockwise):
Q1 → Q2 → base → Q1
The system calculates both for every candidate base asset and picks whichever returns more — it's never obvious in advance which direction is favorable, it depends on exactly where the mispricing sits.
2.2 Solving a leg
Each leg in the chain is either a buy (acquire the target asset at the ask price) or a sell (dispose of what you hold at the bid price):
sell: to_amount = from_amount × price, constrained by from_amount ≤ qty_limit (bid quantity, in the asset you're selling)
buy: to_amount = from_amount / price, constrained by to_amount ≤ qty_limit (ask quantity, in the asset you're buying)
2.3 Solving the full 3-leg cycle
A single function walks all three legs in one pass and returns two numbers: the final multiplicative factor (how much you get back per unit of Q1 invested — from which deviation_pct = (factor − 1) × 100), and the maximum tradeable volume in Q1, propagating the liquidity constraint of all three legs at once — the same principle already used for maxTradeableVolume in Simple Arbitrage, extended from 2 conversions to 3:
function ta_solve_cycle(array $legs): array {
$factor = 1.0; $maxInput = INF;
foreach ($legs as $leg) {
if ($leg['price'] <= 0 || $factor <= 0) return [0.0, 0.0];
if ($leg['type'] === 'sell') {
$maxInput = min($maxInput, $leg['qty_limit'] / $factor);
$factor *= $leg['price'];
} else { // buy
$maxInput = min($maxInput, ($leg['qty_limit'] * $leg['price']) / $factor);
$factor /= $leg['price'];
}
}
return [$factor, $maxInput];
}
2.4 Bridge direction
The bridge between Q1 and Q2 can be listed either way depending on the exchange: as Q2/Q1 (e.g. BTC/USDT — the common case) or as Q1/Q2 (e.g. USDT/BTC — rarer, but handled). The system checks both directions and adapts which side of the book (bid/ask) to use for each cycle accordingly.
Worked example (illustrative)
Base = ETH, Q1 = USDT, Q2 = BTC.
ETH/USDT = 3,000 · ETH/BTC = 0.0452 · BTC/USDT = 66,500
Implied ETH/BTC = 3,000 / 66,500 ≈ 0.04511
Quoted ETH/BTC = 0.0452 → a ≈ 0.2% deviation between the implied and quoted cross-rate — small, but potentially real if liquidity on all three legs supports it.
3. Intra-exchange vs. cross-exchange: how the search actually runs
In cross-exchange mode, for each of the three leg roles (Q1→base, base→Q2, bridge Q1↔Q2) the system collects one candidate per selected exchange that offers that pair, and picks the best one — a greedy, per-leg selection, not a global optimization across all three legs together (the same simplification already used implicitly in Simple Arbitrage when picking the best bid/ask across exchanges). A base asset becomes a candidate if base/Q1 exists on at least one of the two exchanges and base/Q2 exists on at least one (not necessarily the same one), and the bridge exists on at least one of the two.
Every opportunity exposes a full legs[] breakdown — action, exchange, symbol, price, a direct link to the market, data age, and 24h volume — so in cross-exchange mode you always know exactly where to place each of the three orders.
4. How to read the prototype's results
5. What real testing has already taught us
These are hard-won lessons from actual test sessions on the prototype — worth knowing before you draw conclusions from what you see.
"Fresh" data doesn't mean "correct" data. In cross-exchange tests, one exchange consistently showed up as the cheap leg for a wide variety of unrelated assets. The cause wasn't stale data (the age field was consistently low and comparable across legs) — it was dead markets: pairs technically listed with a 24h volume of literally zero. An order can sit untouched for hours while the bot's polling still refreshes the "updated_at" timestamp every cycle — a fresh timestamp only means "our bot wrote something recently," not that the underlying price reflects a real, current market.
BTC-quoted altcoin pairs are structurally thin almost everywhere. Even on solid exchanges, altcoin/BTC books are often tiny (think $6–20k of daily volume) — the industry has largely moved to USDT-quoted pairs. This is a structural limitation of the Q1=USDT/Q2=BTC combination itself, not a quirk of any one exchange.
Practical heuristic: small deviations (0.1–0.6%) with healthy volume on both sides of the loop are the most credible real arbitrage candidates — consistent with the intuition that rebalancing bots keep true mispricings small. Large deviations (>2–3%) are almost always a symptom of bad data, not opportunity — verify manually before trusting them.
6. What the prototype does not do yet
| Missing | Current state |
|---|---|
| Fees (3 taker trades) | Not included — figures are gross. With 3 legs, real minimum-deviation thresholds need to be higher than for 2-leg strategies. |
| Blocking freshness filter | Age is shown as a diagnostic field only, not used to hide results — in testing so far, staleness was never the actual cause of the anomalies observed. |
| Translations / i18n | None — interface strings are hardcoded, not wired into the lang/{code}.php system yet. |
| Navigation integration | None — standalone page reachable only via direct URL. |
| Redis persistence of results | None — everything is computed on demand each time you click "Calculate." |
| Global multi-leg optimization (cross-exchange) | Not implemented — per-leg greedy selection only (see section 3). |
7. Risks specific to this strategy
Three uncertain legs instead of two. Even in taker/taker/taker mode, three sequential fills mean more moments where price can move against you between the first and last leg.
Data-quality risk is unusually high here. Because the strategy is sensitive to small cross-rate discrepancies, bad or stale data produces phantom opportunities more easily than in the other three strategies — always verify manually before trusting a large deviation.
No fee model yet means the deviation % you see today overstates real net profitability — treat every number in the current prototype as gross, not net.
8. Where this is headed
The near-term roadmap for this module: adding a real fee model to recalculate genuinely profitable thresholds, testing quote-asset combinations beyond USDT+BTC (e.g. USDT+ETH, USDT+a liquid major) to work around the structural thinness of BTC-quoted altcoin books, and — once results keep validating positively — porting the core calculation logic (ta_solve_cycle, best-leg selection, bridge lookup) from the PHP prototype into a proper Node.js analyzer module, following the same architectural pattern as the other three strategies, with translations, nav integration and Telegram notifications added only after that.
1. Cos'è davvero l'Arbitraggio Triangolare
Simple Arbitrage, Market Maker e Spread Sniper confrontano tutti lo stesso asset su due exchange diversi. L'Arbitraggio Triangolare confronta invece tre mercati correlati — e, punto cruciale, tutti e tre possono stare sullo stesso exchange:
base/Q1— es. ETH/USDTbase/Q2— es. ETH/BTCQ2/Q1, il ponte — es. BTC/USDT
In teoria, il prezzo di base/Q2 dovrebbe essere sempre coerente con base/Q1 diviso Q2/Q1 — il tasso di cambio implicito. Quando il prezzo reale quotato si scosta da quel tasso implicito, c'è potenzialmente un'opportunità: tradando in sequenza sui tre mercati si chiude un ciclo che, se lo scostamento è reale e tradeable, restituisce più capitale di quanto se ne sia investito.
Vantaggio strutturale rispetto alle altre 3 strategie
Eseguito su un singolo exchange, l'Arbitraggio Triangolare non richiede alcun trasferimento tra piattaforme — tutto avviene all'interno di un solo account. Questo elimina completamente il problema "quale rete, quale exchange ha quell'asset disponibile per il prelievo in questo momento" che le altre tre strategie devono ancora affrontare.
Il compromesso: il prototipo è stato esteso anche per funzionare su 2 exchange contemporaneamente. In quella modalità cross-exchange si perde il vantaggio "zero trasferimenti" — serve capitale pre-posizionato in tutti e tre gli asset (Q1, base, Q2) su entrambi gli exchange, esattamente come richiesto già da Simple/Market Maker Arbitrage, e il rischio di latenza inter-exchange torna in gioco.
2. La matematica
2.1 Due cicli possibili
Dato un ponte Q2/Q1, ci sono sempre due direzioni per chiudere il triangolo (3 trade taker/taker/taker):
- Ciclo 1 (orario):
Q1 → base → Q2 → Q1 - Ciclo 2 (antiorario):
Q1 → Q2 → base → Q1
Il sistema calcola entrambi per ogni asset base candidato e sceglie quello che rende di più — non è mai ovvio in anticipo quale direzione sia favorevole, dipende da dove si trova esattamente lo scostamento.
2.2 Risolvere una gamba (leg)
Ogni gamba della catena è o un acquisto (ottieni l'asset target al prezzo ask) o una vendita (cedi ciò che possiedi al prezzo bid):
sell: to_amount = from_amount × price, vincolo from_amount ≤ qty_limit (quantità in bid, nell'asset che vendi)
buy: to_amount = from_amount / price, vincolo to_amount ≤ qty_limit (quantità in ask, nell'asset che compri)
2.3 Risolvere l'intero ciclo a 3 gambe
Un'unica funzione percorre tutte e tre le gambe in un solo passaggio e restituisce due numeri: il fattore moltiplicativo finale (quanto si ottiene per unità di Q1 investita — da cui deviation_pct = (factor − 1) × 100) e il volume massimo tradeable in unità di Q1, propagando il vincolo di liquidità di tutte e tre le gambe insieme — lo stesso principio già usato per maxTradeableVolume in Simple Arbitrage, esteso da 2 conversioni a 3:
function ta_solve_cycle(array $legs): array {
$factor = 1.0; $maxInput = INF;
foreach ($legs as $leg) {
if ($leg['price'] <= 0 || $factor <= 0) return [0.0, 0.0];
if ($leg['type'] === 'sell') {
$maxInput = min($maxInput, $leg['qty_limit'] / $factor);
$factor *= $leg['price'];
} else { // buy
$maxInput = min($maxInput, ($leg['qty_limit'] * $leg['price']) / $factor);
$factor /= $leg['price'];
}
}
return [$factor, $maxInput];
}
2.4 Direzione del ponte
Il ponte tra Q1 e Q2 può essere listato in due modi diversi a seconda dell'exchange: come Q2/Q1 (es. BTC/USDT — il caso comune) o come Q1/Q2 (es. USDT/BTC — raro, ma gestito). Il sistema verifica entrambe le direzioni e adatta di conseguenza quale lato del book (bid/ask) usare per ciascun ciclo.
Esempio svolto (illustrativo)
Base = ETH, Q1 = USDT, Q2 = BTC.
ETH/USDT = 3.000 · ETH/BTC = 0,0452 · BTC/USDT = 66.500
ETH/BTC implicito = 3.000 / 66.500 ≈ 0,04511
ETH/BTC quotato = 0,0452 → uno scostamento ≈ 0,2% tra tasso implicito e quotato — piccolo, ma potenzialmente reale se la liquidità su tutte e tre le gambe lo supporta.
3. Intra-exchange vs cross-exchange: come funziona davvero la ricerca
In modalità cross-exchange, per ciascuno dei tre ruoli di gamba (Q1→base, base→Q2, ponte Q1↔Q2) il sistema raccoglie un candidato per ogni exchange selezionato che offre quel pair, e sceglie il migliore — una selezione greedy per singola gamba, non un'ottimizzazione globale sulla combinazione delle tre gambe insieme (la stessa semplificazione già usata implicitamente in Simple Arbitrage quando si sceglie il miglior bid/ask tra exchange). Una base diventa candidata se base/Q1 esiste su almeno uno dei due exchange e base/Q2 esiste su almeno uno (non necessariamente lo stesso), e il ponte esiste su almeno uno dei due.
Ogni opportunità espone un dettaglio completo legs[] — azione, exchange, simbolo, prezzo, link diretto al mercato, età del dato e volume 24h — così in modalità cross-exchange sai sempre esattamente dove piazzare ciascuno dei tre ordini.
4. Come leggere i risultati del prototipo
5. Cosa ci ha già insegnato il testing reale
Sono lezioni imparate a caro prezzo durante le sessioni di test effettive sul prototipo — utili da conoscere prima di trarre conclusioni da ciò che vedi.
Dato "fresco" non vuol dire dato "corretto". Nei test cross-exchange, un exchange appariva sistematicamente come la gamba di acquisto per una grande varietà di asset non correlati tra loro. La causa non era dato vecchio (il campo età era sempre basso e comparabile tra le gambe) — era mercati morti: pair tecnicamente listati con volume 24h letteralmente pari a zero. Un ordine può restare fermo per ore mentre il polling del bot continua comunque ad aggiornare il timestamp "updated_at" ad ogni ciclo — un timestamp fresco significa solo "il nostro bot ha scritto qualcosa di recente", non che il prezzo sottostante rifletta un mercato reale e attuale.
Le coppie altcoin quotate in BTC sono strutturalmente sottili quasi ovunque. Anche su exchange solidi, i book altcoin/BTC sono spesso minuscoli (dell'ordine di 6-20mila $ di volume giornaliero) — il settore si è spostato in gran parte su pair quotati in USDT. Questo è un limite strutturale della combinazione Q1=USDT/Q2=BTC in sé, non una particolarità di un singolo exchange.
Euristica pratica: scostamenti piccoli (0,1-0,6%) con volumi sani su entrambi i lati del ciclo sono i candidati più credibili come arbitraggio reale — coerente con l'intuizione che i bot di ribilanciamento mantengono piccoli gli scostamenti veri. Scostamenti grandi (>2-3%) sono quasi sempre sintomo di dato cattivo, non di opportunità — verifica sempre manualmente prima di fidartene.
6. Cosa non fa ancora il prototipo
| Cosa manca | Stato attuale |
|---|---|
| Fee (3 trade taker) | Non incluse — i numeri sono lordi. Con 3 gambe, le soglie minime di scostamento reali devono essere più alte rispetto alle strategie a 2 gambe. |
| Filtro di freschezza bloccante | L'età è mostrata solo come campo diagnostico, non usata per nascondere risultati — nei test finora la staleness non è mai stata la causa reale delle anomalie osservate. |
| Traduzioni / i18n | Nessuna — stringhe hardcoded, non ancora collegate al sistema lang/{code}.php. |
| Integrazione navigazione | Nessuna — pagina standalone raggiungibile solo via URL diretto. |
| Persistenza Redis dei risultati | Nessuna — tutto calcolato on-demand ad ogni click su "Calcola". |
| Ottimizzazione globale multi-gamba (cross-exchange) | Non implementata — solo selezione greedy per singola gamba (vedi sezione 3). |
7. Rischi specifici di questa strategia
Tre gambe incerte invece di due. Anche in modalità taker/taker/taker, tre esecuzioni sequenziali significano più momenti in cui il prezzo può muoversi contro di te tra la prima e l'ultima gamba.
Il rischio di qualità del dato è insolitamente alto qui. Poiché la strategia è sensibile a piccoli scostamenti del tasso di cambio incrociato, dati cattivi o vecchi producono opportunità fantasma più facilmente che nelle altre tre strategie — verifica sempre manualmente prima di fidarti di uno scostamento grande.
Non c'è ancora un modello di fee, quindi lo scostamento % che vedi oggi sovrastima la profittabilità netta reale — tratta ogni numero nel prototipo attuale come lordo, non netto.
8. Verso dove sta andando
La roadmap a breve termine per questo modulo: aggiungere un vero modello di fee per ricalcolare soglie realmente profittevoli, testare combinazioni di quote asset oltre a USDT+BTC (es. USDT+ETH, USDT+una major liquida) per aggirare la sottigliezza strutturale dei book altcoin quotati in BTC, e — una volta che i risultati continuano a essere validati positivamente — portare la logica di calcolo principale (ta_solve_cycle, selezione miglior gamba, ricerca del ponte) dal prototipo PHP a un vero modulo analyzer Node.js, seguendo lo stesso pattern architetturale delle altre tre strategie, con traduzioni, integrazione nella navigazione e notifiche Telegram aggiunte solo dopo.