Page 2 of 2
Re: Triangular Arbitrage: Why Prices Across Pairs Stay Consistent
Posted: Mon Oct 05, 2026 4:49 pm
by PTScalper
To convert this indicator into a backtestable strategy, we need to change the core function from indicator() to strategy() and introduce trade execution logic.
In a mean-reversion strategy like this, the logic dictates that when the actual price deviates too far from the mathematical synthetic price, we enter a trade betting it will snap back to zero (perfect efficiency).
Re: Triangular Arbitrage: Why Prices Across Pairs Stay Consistent
Posted: Mon Oct 05, 2026 4:49 pm
by PTScalper
Here is the complete Strategy script:
Code: Select all
//@version=5
strategy("Tri-Arb Gap Reversion Strategy", shorttitle="Uni-Arb Strat", overlay=false, initial_capital=10000, default_qty_type=strategy.percent_of_equity, default_qty_value=10, commission_type=strategy.commission.cash_per_order, commission_value=3)
// 1. Inputs for the two legs & calculation method
leg1_sym = input.symbol("FX_IDC:EURUSD", title="Leg 1 (e.g., EUR/USD)")
leg2_sym = input.symbol("FX_IDC:GBPUSD", title="Leg 2 (e.g., GBP/USD)")
calc_mode = input.string("Divide (Leg1 / Leg2)", title="Calculation Method",
options=["Divide (Leg1 / Leg2)", "Multiply (Leg1 * Leg2)"])
// 2. Trading Thresholds
entry_threshold = input.int(15, title="Entry Threshold (Points)", minval=1, tooltip="Gap required to open a trade")
exit_threshold = input.int(0, title="Exit Threshold (Points)", tooltip="Gap required to close a trade (0 = perfect reversion)")
// 3. Fetch the closing prices of the two legs
leg1_price = request.security(leg1_sym, timeframe.period, close)
leg2_price = request.security(leg2_sym, timeframe.period, close)
// 4. Calculate the synthetic rate and the pricing gap
synthetic_rate = calc_mode == "Divide (Leg1 / Leg2)" ? (leg1_price / leg2_price) : (leg1_price * leg2_price)
pricing_gap = (close - synthetic_rate) / syminfo.mintick
// 5. Plotting (Keeps the visual histogram on the bottom pane)
plot(pricing_gap, title="Pricing Gap (Points)", color=pricing_gap > 0 ? color.teal : color.maroon, style=plot.style_columns)
hline(0, title="Perfect Efficiency", color=color.gray, linestyle=hline.style_dotted)
hline(entry_threshold, title="Short Entry Line", color=color.red, linestyle=hline.style_dashed)
hline(-entry_threshold, title="Long Entry Line", color=color.green, linestyle=hline.style_dashed)
// 6. Strategy Execution Logic
// Go SHORT if the actual price is too HIGH compared to the synthetic price
shortCondition = pricing_gap >= entry_threshold
if (shortCondition)
strategy.entry("Short Cross", strategy.short)
// Go LONG if the actual price is too LOW compared to the synthetic price
longCondition = pricing_gap <= -entry_threshold
if (longCondition)
strategy.entry("Long Cross", strategy.long)
// 7. Exit Logic (Reversion to the Mean)
// Close Short when the gap drops back down to our exit threshold (usually 0)
if (strategy.position_size < 0 and pricing_gap <= exit_threshold)
strategy.close("Short Cross", comment="Reverted")
// Close Long when the gap rises back up to our exit threshold (usually 0)
if (strategy.position_size > 0 and pricing_gap >= -exit_threshold)
strategy.close("Long Cross", comment="Reverted")
Re: Triangular Arbitrage: Why Prices Across Pairs Stay Consistent
Posted: Mon Oct 05, 2026 4:49 pm
by PTScalper
How the Trading Logic Works
Going Short: If the actual chart price spikes 15 points above the synthetic mathematical price (Gap >= 15), the script sells the cross pair, expecting the actual price to fall back down to match the math.
Going Long: If the actual chart price drops 15 points below the synthetic mathematical price (Gap <= -15), the script buys the cross pair, expecting the price to rise back up to match the math.
The Exit: Once the gap touches 0 again (or whatever exit_threshold you set), the strategy closes the position to lock in the reversion profit.
Re: Triangular Arbitrage: Why Prices Across Pairs Stay Consistent
Posted: Mon Oct 05, 2026 4:50 pm
by PTScalper
Crucial Reality Checks for this Backtest
TradingView backtests operate in a simplified environment. When you review your Strategy Tester results, keep these two structural limitations in mind:
TradingView Cannot Execute 3 Legs at Once: True triangular arbitrage requires executing all three currency pairs simultaneously to lock in a risk-free profit. Because TradingView strategies can only execute orders on the chart they are applied to, this script simulates trading only the cross pair. It is technically a statistical mean-reversion strategy on the cross pair, not a true risk-free arbitrage execution.
The Spread Illusion: I added a $3 commission per order to the script settings to make it slightly more realistic, but standard backtests assume you get filled at the historical close price. In reality, the moment an arbitrage gap opens during live trading (usually during high-impact news), brokers instantly widen their spreads. A backtest might show a wildly profitable 20-point win, but in a live market, a 25-point spread would have eaten the entire move.
Re: Triangular Arbitrage: Why Prices Across Pairs Stay Consistent
Posted: Mon Oct 05, 2026 4:53 pm
by PTScalper
Here are the Expert Advisors (EAs) for both MQL4 and MQL5. They replicate the Pine Script logic by dynamically pulling the quotes of the two USD legs, calculating the mathematical gap, and executing mean-reversion trades on the cross pair.
MQL4 Expert Advisor (MetaTrader 4)
Open MetaEditor, create a new Expert Advisor named TriArb_Reversion_MT4, and paste this code:
Code: Select all
//+------------------------------------------------------------------+
//| TriArb_Reversion_MT4.mq4 |
//+------------------------------------------------------------------+
#property strict
// 1. Dropdown equivalent for Calculation Mode
enum ENUM_CALC_MODE {
DIVIDE = 0, // Divide (Leg1 / Leg2)
MULTIPLY = 1 // Multiply (Leg1 * Leg2)
};
// 2. EA Inputs
input string Leg1_Symbol = "EURUSD";
input string Leg2_Symbol = "GBPUSD";
input string Cross_Symbol = "EURGBP";
input ENUM_CALC_MODE Calc_Mode = DIVIDE;
input double Entry_Threshold_Points = 15.0;
input double Exit_Threshold_Points = 0.0;
input double LotSize = 0.1;
input int MagicNumber = 55555;
void OnTick()
{
// 3. Fetch background prices using MarketInfo
double leg1_bid = MarketInfo(Leg1_Symbol, MODE_BID);
double leg2_bid = MarketInfo(Leg2_Symbol, MODE_BID);
double cross_bid = MarketInfo(Cross_Symbol, MODE_BID);
double cross_ask = MarketInfo(Cross_Symbol, MODE_ASK);
double cross_point = MarketInfo(Cross_Symbol, MODE_POINT);
// Safety check to ensure background pairs are actively quoting
if(leg1_bid == 0 || leg2_bid == 0 || cross_point == 0) return;
// 4. Calculate Synthetic Rate
double synth_rate = 0.0;
if (Calc_Mode == DIVIDE) {
synth_rate = leg1_bid / leg2_bid;
} else {
synth_rate = leg1_bid * leg2_bid;
}
// 5. Calculate Gap in Points
double pricing_gap = (cross_bid - synth_rate) / cross_point;
// 6. Check for existing positions
bool pos_exists = false;
int pos_type = -1;
int ticket_idx = -1;
for(int i = OrdersTotal() - 1; i >= 0; i--) {
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) {
if(OrderSymbol() == Cross_Symbol && OrderMagicNumber() == MagicNumber) {
pos_exists = true;
pos_type = OrderType();
ticket_idx = OrderTicket();
break;
}
}
}
// 7. Entry & Exit Logic
if (!pos_exists) {
// Gap > Entry Threshold => Actual price is too high, SELL cross
if (pricing_gap >= Entry_Threshold_Points) {
OrderSend(Cross_Symbol, OP_SELL, LotSize, cross_bid, 3, 0, 0, "Tri-Arb Short", MagicNumber, 0, clrRed);
}
// Gap < -Entry Threshold => Actual price is too low, BUY cross
else if (pricing_gap <= -Entry_Threshold_Points) {
OrderSend(Cross_Symbol, OP_BUY, LotSize, cross_ask, 3, 0, 0, "Tri-Arb Long", MagicNumber, 0, clrGreen);
}
} else {
// Reversion Exit Logic
if (pos_type == OP_SELL && pricing_gap <= Exit_Threshold_Points) {
OrderClose(ticket_idx, LotSize, cross_ask, 3, clrRed);
}
else if (pos_type == OP_BUY && pricing_gap >= -Exit_Threshold_Points) {
OrderClose(ticket_idx, LotSize, cross_bid, 3, clrGreen);
}
}
}
Re: Triangular Arbitrage: Why Prices Across Pairs Stay Consistent
Posted: Mon Oct 05, 2026 4:53 pm
by PTScalper
MQL5 Expert Advisor (MetaTrader 5)
MT5 utilizes the object-oriented CTrade library for execution. Create a new Expert Advisor named TriArb_Reversion_MT5 and paste this code:
Code: Select all
//+------------------------------------------------------------------+
//| TriArb_Reversion_MT5.mq5 |
//+------------------------------------------------------------------+
#property strict
#include <Trade\Trade.mqh>
enum ENUM_CALC_MODE {
DIVIDE = 0, // Divide (Leg1 / Leg2)
MULTIPLY = 1 // Multiply (Leg1 * Leg2)
};
input string Leg1_Symbol = "EURUSD";
input string Leg2_Symbol = "GBPUSD";
input string Cross_Symbol = "EURGBP";
input ENUM_CALC_MODE Calc_Mode = DIVIDE;
input double Entry_Threshold_Points = 15.0;
input double Exit_Threshold_Points = 0.0;
input double LotSize = 0.1;
input ulong MagicNumber = 55555;
CTrade trade;
int OnInit()
{
trade.SetExpertMagicNumber(MagicNumber);
return(INIT_SUCCEEDED);
}
void OnTick()
{
// 3. Fetch background prices utilizing MT5's SymbolInfoDouble
double leg1_bid = SymbolInfoDouble(Leg1_Symbol, SYMBOL_BID);
double leg2_bid = SymbolInfoDouble(Leg2_Symbol, SYMBOL_BID);
double cross_bid = SymbolInfoDouble(Cross_Symbol, SYMBOL_BID);
double cross_ask = SymbolInfoDouble(Cross_Symbol, SYMBOL_ASK);
double cross_point = SymbolInfoDouble(Cross_Symbol, SYMBOL_POINT);
if(leg1_bid == 0 || leg2_bid == 0 || cross_point == 0) return;
// 4. Calculate Synthetic Rate
double synth_rate = 0.0;
if (Calc_Mode == DIVIDE) {
synth_rate = leg1_bid / leg2_bid;
} else {
synth_rate = leg1_bid * leg2_bid;
}
// 5. Calculate Gap in Points
double pricing_gap = (cross_bid - synth_rate) / cross_point;
// 6. Check for existing positions
bool pos_exists = false;
long pos_type = -1;
ulong pos_ticket = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--) {
ulong ticket = PositionGetTicket(i);
if(PositionGetString(POSITION_SYMBOL) == Cross_Symbol) {
if(PositionGetInteger(POSITION_MAGIC) == MagicNumber) {
pos_exists = true;
pos_type = PositionGetInteger(POSITION_TYPE);
pos_ticket = ticket;
break;
}
}
}
// 7. Entry & Exit Logic
if (!pos_exists) {
if (pricing_gap >= Entry_Threshold_Points) {
trade.Sell(LotSize, Cross_Symbol, cross_bid, 0, 0, "Tri-Arb Short");
}
else if (pricing_gap <= -Entry_Threshold_Points) {
trade.Buy(LotSize, Cross_Symbol, cross_ask, 0, 0, "Tri-Arb Long");
}
} else {
if (pos_type == POSITION_TYPE_SELL && pricing_gap <= Exit_Threshold_Points) {
trade.PositionClose(pos_ticket);
}
else if (pos_type == POSITION_TYPE_BUY && pricing_gap >= -Exit_Threshold_Points) {
trade.PositionClose(pos_ticket);
}
}
}
Re: Triangular Arbitrage: Why Prices Across Pairs Stay Consistent
Posted: Mon Oct 05, 2026 4:54 pm
by PTScalper
Critical MetaTrader Implementation Differences
The MT4 Backtesting Flaw: The MetaTrader 4 Strategy Tester does not support pulling historical tick data for background symbols (Leg 1 and Leg 2). It will only return 0 or the current live market price, ruining the backtest. You cannot accurately backtest this EA in MT4.
The MT5 Backtesting Solution: MetaTrader 5's Strategy Tester natively supports multi-currency testing. When you backtest the MQL5 version, the engine automatically downloads the tick history for all three legs simultaneously and reconstructs the historical synthetic gap accurately.
Market Watch Requirement: For live trading in both platforms, ensure all three pairs (e.g., EURUSD, GBPUSD, EURGBP) are active and visible in your "Market Watch" window. If a symbol is hidden, MarketInfo or SymbolInfoDouble will fail to return a quote and the EA will not execute.
Beyond Mean-Reversion (True Arbitrage): Unlike TradingView, MT4 and MT5 allow you to execute trades on all three pairs at the exact same millisecond. If you want to graduate from statistical mean-reversion (trading just the cross) to risk-free triangular arbitrage, you can replace the single trade.Buy() function with three simultaneous execution commands across the three required legs.
Re: Triangular Arbitrage: Why Prices Across Pairs Stay Consistent
Posted: Mon Oct 05, 2026 4:56 pm
by Fairman
Doing the EURGBP calculation once by hand is worth it. When the live numbers line up to the fourth decimal, the whole cross rate thing stops being abstract. Take the current EURUSD bid and divide by the GBPUSD rate and compare it to your broker's EURGBP quote. It'll almost always be within a fraction of a pip. That also explains something people find confusing, which is why EURGBP can move without either the euro or the pound having news of their own. If the dollar moves against one more than the other, the cross has to adjust. Knowing that helps when you're trying to work out what is actually driving a move.
Re: Triangular Arbitrage: Why Prices Across Pairs Stay Consistent
Posted: Mon Oct 05, 2026 5:10 pm
by Fairman
Practical use for this: if you're long EURUSD and short GBPUSD, you've roughly built a long EURGBP position. Two trades, two spreads, one idea. If that's what you meant, a single EURGBP trade is often cheaper and easier to manage. If it isn't what you meant, it's worth knowing that the two trades together will mostly move with EURGBP, which tends to have a much smaller range than either leg. People sometimes do this by accident when they trade both pairs off one dollar view and then wonder why the account barely moves. Writing out positions by currency makes it obvious before you click.