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-8The 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
| Pair | Description |
|---|---|
| EURGBP | EUR strength vs GBP |
| GBPNZD | GBP strength vs NZD |
| GBPAUD | GBP strength vs AUD |
| GBPCHF | GBP 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
| Pair | Description |
|---|---|
| NZDUSD | NZD vs USD |
| EURUSD | EUR vs USD |
| AUDUSD | AUD vs USD |
| USDCHF | USD 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:
-
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.
-
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.
-
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.
-
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
| Risk | Mitigation |
|---|---|
| Correlation breakdown | Add a max drawdown kill switch (see Risk Management article) |
| Spread cost | Only enter during peak liquidity sessions (London/NY overlap) |
| Margin | Size lots to use no more than 50% of available margin across all 8 pairs |
| Execution | Use 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.

