Insights/Insights & Articles/Multi-Currency Arbitrage: Basket Trading Across 8 Pairs

Multi-Currency Arbitrage: Basket Trading Across 8 Pairs

Here's something that always bugs us about retail forex: everyone's glued to one chart, one pair, one position at a time. Meanwhile, the prop desks that actually make money? They've been running basket trades since before MT4 existed.

Mar 13, 2026

Multi-Currency Arbitrage: Basket Trading Across 8 Pairs

Multi-Currency Arbitrage: Basket Trading Across 8 Pairs

Here's something that always bugs us about retail forex: everyone's glued to one chart, one pair, one position at a time. Meanwhile, the prop desks that actually make money? They've been running basket trades since before MT4 existed. Eight pairs open at once. Sounds crazy until you see the equity curve. We've been building and running these systems for over a decade, and this is one of our favorite strategy architectures – ugly-simple on the surface, deceptively clever underneath. Let us walk you through a production basket arb system we've shipped to real accounts.

The Core Concept: Currency Triangle Arbitrage

Forget indicators for a second. Think pure math. EUR/GBP rises, GBP/USD sits flat – EUR/USD must rise proportionally. That's just how currency triangles work. But markets are messy. One pair overshoots. Another lags by 200 milliseconds. A third is stuck because liquidity dried up on that cross. Those tiny windows of mismatch? That's your edge.

The setup is two baskets: a GBP basket and a USD basket.

GBP BASKET (pairs where GBP is the base/major currency):

  • Buy EURGBP + Buy GBPNZD + Buy GBPAUD + Buy GBPCHF

USD BASKET (pairs where USD is the counter/major currency):

  • Buy NZDUSD + Sell EURUSD + Sell AUDUSD + Sell USDCHF

Buy all 8 at the same time and something interesting happens. Individual pair moves start canceling each other out – you've built a hedged portfolio almost by accident. What's left over, the net P&L across all 8 positions, is a small residual that tilts positive over time. Why? Because you're harvesting temporary mispricing that the market will correct. You're not predicting direction. You're betting on math fixing itself.

The Production EA: How It Works

MQL4 Implementation

Alright, enough theory. Here's what the actual EA looks like – the version running on live accounts, not a classroom exercise:

//+------------------------------------------------------------------+
//| Basket Arbitrage EA                                                |
//| Opens 8 correlated pairs simultaneously, exits on combined profit  |
//+------------------------------------------------------------------+
#property strict
 
// The 8 pairs forming the basket
input string Pair1 = "EURGBP";    // GBP basket pair 1
input string Pair2 = "GBPNZD";    // GBP basket pair 2
input string Pair3 = "GBPAUD";    // GBP basket pair 3 (or AUDGBP depending on broker)
input string Pair4 = "GBPCHF";    // GBP basket pair 4
input string Pair5 = "NZDUSD";    // USD basket pair 1
input string Pair6 = "EURUSD";    // USD basket pair 2
input string Pair7 = "AUDUSD";    // USD basket pair 3
input string Pair8 = "USDCHF";    // USD basket pair 4
input double LotSize = 0.1;       // Lot size per pair
input double ProfitTarget = 50.0; // Combined profit target ($)
 
// Direction control
enum TradeDirection { BuyAll, SellAll };
input TradeDirection Direction = BuyAll;
 
// Magic numbers for each sub-group
#define MAGIC_GROUP_1  1  // Pairs 1-2
#define MAGIC_GROUP_2  2  // Pairs 3-4
#define MAGIC_GROUP_3  3  // Pairs 5-6
#define MAGIC_GROUP_4  4  // Pairs 7-8

The Entry: 8 Orders at Once

No positions open? Fire all 8. No filtering, no waiting for the "perfect" entry. Just open the basket:

void OpenBasket()
{
    if (OrdersTotal() > 0) return;  // Only one basket at a time
 
    if (Direction == BuyAll)
    {
        // GBP basket: buy all GBP pairs
        OpenPair(Pair1, OP_BUY, MAGIC_GROUP_1);
        OpenPair(Pair2, OP_BUY, MAGIC_GROUP_1);
        OpenPair(Pair3, OP_BUY, MAGIC_GROUP_2);
        OpenPair(Pair4, OP_BUY, MAGIC_GROUP_2);
 
        // USD basket: buy USD pairs
        OpenPair(Pair5, OP_BUY, MAGIC_GROUP_3);
        OpenPair(Pair6, OP_BUY, MAGIC_GROUP_3);
        OpenPair(Pair7, OP_BUY, MAGIC_GROUP_4);
        OpenPair(Pair8, OP_BUY, MAGIC_GROUP_4);
    }
    else // SellAll
    {
        OpenPair(Pair1, OP_SELL, MAGIC_GROUP_1);
        OpenPair(Pair2, OP_SELL, MAGIC_GROUP_1);
        OpenPair(Pair3, OP_SELL, MAGIC_GROUP_2);
        OpenPair(Pair4, OP_SELL, MAGIC_GROUP_2);
        OpenPair(Pair5, OP_SELL, MAGIC_GROUP_3);
        OpenPair(Pair6, OP_SELL, MAGIC_GROUP_3);
        OpenPair(Pair7, OP_SELL, MAGIC_GROUP_4);
        OpenPair(Pair8, OP_SELL, MAGIC_GROUP_4);
    }
}
 
void OpenPair(string symbol, int type, int magic)
{
    double price;
    if (type == OP_BUY)
        price = MarketInfo(symbol, MODE_ASK);
    else
        price = MarketInfo(symbol, MODE_BID);
 
    int ticket = OrderSend(symbol, type, LotSize, price, 5,
                           0, 0, NULL, magic, 0,
                           type == OP_BUY ? clrBlue : clrRed);
 
    if (ticket < 0)
        Print("Failed to open ", symbol, ". Error: ", GetLastError());
}

Notice something? No stop losses on any individual pair. We know – that makes some people physically uncomfortable. But think about it: GBPNZD might be -30 pips while EURGBP is +45 pips. Who cares about one leg? The only number that matters is the combined P&L across all 8. That's the whole philosophy.

The Exit: Combined Equity Target

void OnTick()
{
    double balance = AccountBalance();
    double equity  = AccountEquity();
 
    // No open positions? Open the basket.
    if (OrdersTotal() == 0)
        OpenBasket();
 
    // Check combined profit/loss across ALL open positions
    if (equity > balance + ProfitTarget)
    {
        CloseAllPositions();
        Print("BASKET PROFIT TARGET HIT: $", ProfitTarget);
    }
}
 
void CloseAllPositions()
{
    for (int i = OrdersTotal() - 1; i >= 0; i--)
    {
        if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
 
        if (OrderType() == OP_BUY)
            OrderClose(OrderTicket(), OrderLots(),
                      MarketInfo(OrderSymbol(), MODE_BID), 5, clrRed);
        else if (OrderType() == OP_SELL)
            OrderClose(OrderTicket(), OrderLots(),
                      MarketInfo(OrderSymbol(), MODE_ASK), 5, clrRed);
    }
}

We love how dumb this exit is. Equity exceeds balance by $50? Slam everything shut. All 8 positions, gone. No trailing stops, no per-trade micromanagement, no clever exit scaling. The basket lives and dies as one unit. People overthink exits constantly – this is the antidote.

Why These 8 Pairs?

This isn't random. You can't just throw 8 symbols in a hat and call it a basket. The pair selection is deliberate – it exploits two overlapping correlation clusters.

The GBP Cluster

PairDescription
EURGBPEUR strength vs GBP
GBPNZDGBP strength vs NZD
GBPAUDGBP strength vs AUD
GBPCHFGBP strength vs CHF

Every one of these has GBP in it. So when the Bank of England drops a rate decision and GBP rips, all four react – but not identically. EURGBP might move in 50ms, GBPCHF takes 200ms to catch up. That lag between correlated instruments? That's literally the edge you're capturing. The basket smooths it into an average GBP move.

The USD Cluster

PairDescription
NZDUSDNZD vs USD
EURUSDEUR vs USD
AUDUSDAUD vs USD
USDCHFUSD vs CHF

Same game, different currency. Four pairs, all tethered to the dollar. NFP drops, USD spikes – all four move, but with slightly different timing and magnitude. The basket averages out the noise.

The Hedge

Here's where it gets elegant. Buy both baskets at once, and they partially hedge each other. Dollar strength hammers your GBP crosses? Fine – your USD pairs are printing. GBP collapses? The GBP basket bleeds, but the USD basket picks up the slack. It's not a perfect hedge – nothing is – but it takes the edge off single-currency blow-ups.

Python Implementation

Not everyone wants to deal with MQL4's quirks (we don't blame you). Here's the same logic in Python using CCXT, which lets you run this on crypto exchanges too. Same concept, cleaner syntax, easier to extend:

import ccxt
import time
from dataclasses import dataclass
 
@dataclass
class BasketConfig:
    pairs: list
    lot_size: float
    profit_target: float
 
class BasketArbitrage:
    """
    Multi-currency basket arbitrage strategy.
    Opens positions across 8 correlated pairs simultaneously.
    Exits when combined P&L hits target.
    """
 
    GBP_BASKET = ['EUR/GBP', 'GBP/NZD', 'GBP/AUD', 'GBP/CHF']
    USD_BASKET = ['NZD/USD', 'EUR/USD', 'AUD/USD', 'USD/CHF']
 
    def __init__(self, exchange, config: BasketConfig):
        self.exchange = exchange
        self.config = config
        self.positions = {}
        self.entry_balance = None
 
    def open_basket(self, direction='buy'):
        """Open all 8 pairs simultaneously."""
        all_pairs = self.GBP_BASKET + self.USD_BASKET
        self.entry_balance = self.get_balance()
 
        for pair in all_pairs:
            try:
                if direction == 'buy':
                    order = self.exchange.create_market_buy_order(
                        pair, self.config.lot_size)
                else:
                    order = self.exchange.create_market_sell_order(
                        pair, self.config.lot_size)
 
                self.positions[pair] = order
                print(f"Opened {direction} {pair} @ {order['price']}")
            except Exception as e:
                print(f"Failed to open {pair}: {e}")
 
    def check_combined_pnl(self):
        """Calculate total P&L across all open positions."""
        total_pnl = 0
        for pair, entry_order in self.positions.items():
            ticker = self.exchange.fetch_ticker(pair)
            entry_price = entry_order['price']
            current_price = ticker['last']
 
            if entry_order['side'] == 'buy':
                pnl = (current_price - entry_price) * self.config.lot_size
            else:
                pnl = (entry_price - current_price) * self.config.lot_size
 
            total_pnl += pnl
        return total_pnl
 
    def close_basket(self):
        """Close all positions."""
        for pair in list(self.positions.keys()):
            try:
                entry = self.positions[pair]
                if entry['side'] == 'buy':
                    self.exchange.create_market_sell_order(
                        pair, self.config.lot_size)
                else:
                    self.exchange.create_market_buy_order(
                        pair, self.config.lot_size)
                del self.positions[pair]
                print(f"Closed {pair}")
            except Exception as e:
                print(f"Failed to close {pair}: {e}")
 
    def run(self, direction='buy', check_interval=1):
        """Main loop: open basket, monitor P&L, close on target."""
        self.open_basket(direction)
 
        while self.positions:
            pnl = self.check_combined_pnl()
            print(f"Combined P&L: ${pnl:.2f}")
 
            if pnl >= self.config.profit_target:
                print(f"TARGET HIT: ${pnl:.2f}")
                self.close_basket()
                break
 
            time.sleep(check_interval)

Risk Considerations

What Can Go Wrong

We'd be lying if we told you this was risk-free. It's not. Here's what's bitten us (and clients) over the years:

  1. Correlation breakdown – March 2020. COVID hits. Every correlation model on the planet broke simultaneously. "Hedged" portfolios started moving in the same direction. Your basket's supposed safety net? Gone. This will happen again.

  2. Spread cost × 8 – Do the math. Eight pairs, 2-pip average spread each, 0.1 lots… you're looking at ~$160 in spread costs before your P&L even starts. That's a real headwind, and most people forget to account for it in backtests.

  3. Margin requirement – Eight positions at 0.1 lots = 0.8 total lots. On a $1,000 account, you're practically maxed out on leverage. One bad night and you're getting a margin call.

  4. Execution risk – Here's one that kills the strategy on bad brokers. You need 8 orders filled near-simultaneously. By the time order #8 executes, order #1's price has already moved. During NFP or BOE announcements? That slippage compounds fast.

Risk Mitigation

RiskMitigation
Correlation breakdownAdd a max drawdown kill switch (see Risk Management article)
Spread costOnly enter during peak liquidity sessions (London/NY overlap)
MarginSize lots to use no more than 50% of available margin across all 8 pairs
ExecutionUse a VPS close to broker's server; consider market makers with guaranteed fills

Variants: Buy vs. Sell Direction

The EA ships with a direction toggle, and which one you pick matters more than people think:

  • BuyAll – Every pair goes long. You're betting the GBP/USD universe collectively drifts upward. Works well in risk-on environments.
  • SellAll – Everything short. Profits when the basket drops. Our go-to during risk-off regimes and dollar-strength cycles.

The versions we run on our own accounts go further – they flip direction based on regime detection. Trending market? Go with it. Mean-reverting range? Fade it. The really interesting variant buys the GBP basket while selling the USD basket, which gets you closer to a pure statistical arb. That's a whole separate article though.

Need a Custom Arbitrage System?

Look – basket arb isn't plug-and-play. Your broker's pair availability, spread structure, execution speed, and account size all change the math. We've built dozens of these systems, and no two clients get the same configuration. We pick the right pairs for your broker, size lots so you're not one bad tick from a margin call, and optimize execution to minimize the slippage problem we mentioned above.

Discuss Your Arbitrage Strategy →

Algorithmic Trading Insights by Quantumcona.

3of4