Because MT5 has a modern event-driven trading engine and natively supports multi-currency backtesting, this EA uses the standard CTrade library for execution, synchronizes bar data across both pairs, and dynamically calculates delta-neutral lot sizing.
MQL5 Code: Delta-Neutral Z-Score Arbitrage EA
Code: Select all
//+------------------------------------------------------------------+
//| BTC_ETH_ZScore_EA.mq5 |
//| Forum Community Open Source |
//+------------------------------------------------------------------+
#property copyright "Open Source"
#property link "https://www.mql5.com"
#property version "1.00"
#include <Trade\Trade.mqh>
#include <Trade\SymbolInfo.mqh>
//--- Input parameters
input group "=== Pair Settings ==="
input string Symbol1 = "BTCUSD"; // Primary Asset (Base)
input string Symbol2 = "ETHUSD"; // Secondary Asset (Hedge)
input double ZScoreThreshold = 2.0; // Entry Threshold (+/-)
input int MaPeriod = 50; // Moving Average Lookback Period
input group "=== Position & Risk Settings ==="
input double BaseLots1 = 0.10; // Base Lot Size for Symbol 1
input ulong Deviation = 10; // Max Slippage (points)
input ulong MagicNumber = 778899; // Unique Magic Number
//--- Global Objects & Variables
CTrade trade;
datetime lastBarTime = 0;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
// Set magic number and deviation for trade object
trade.SetExpertMagicNumber(MagicNumber);
trade.SetDeviationInPoints(Deviation);
trade.SetTypeFillingBySymbol(Symbol1);
// Ensure both symbols are available in Market Watch
if(!SymbolSelect(Symbol1, true) || !SymbolSelect(Symbol2, true))
{
Print("Error: Make sure both ", Symbol1, " and ", Symbol2, " are available in Market Watch.");
return(INIT_FAILED);
}
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// 1. Execute only on the opening of a new candle (prevents intraday noise)
datetime currentBarTime = iTime(Symbol1, PERIOD_CURRENT, 0);
if(currentBarTime == lastBarTime) return;
// 2. Calculate current Z-Score on completed bars
double zScore = CalculateZScore();
if(zScore == 0.0) return; // Wait until sufficient history is available
// 3. Check active position state
int activeSpreadState = GetSpreadState(); // 0 = Flat, 1 = Long Spread, -1 = Short Spread
// 4. Trade Execution Logic
if(activeSpreadState == 0)
{
// NO ACTIVE POSITIONS -> Look for entry triggers
if(zScore > ZScoreThreshold)
{
// BTC is overvalued vs ETH -> Short Spread (Sell BTC, Buy ETH)
PrintFormat("Z-Score (%.2f) > %.2f -> Opening SHORT SPREAD", zScore, ZScoreThreshold);
OpenSpread(ORDER_TYPE_SELL, ORDER_TYPE_BUY);
lastBarTime = currentBarTime;
}
else if(zScore < -ZScoreThreshold)
{
// BTC is undervalued vs ETH -> Long Spread (Buy BTC, Sell ETH)
PrintFormat("Z-Score (%.2f) < -%.2f -> Opening LONG SPREAD", zScore, ZScoreThreshold);
OpenSpread(ORDER_TYPE_BUY, ORDER_TYPE_SELL);
lastBarTime = currentBarTime;
}
}
else
{
// ACTIVE POSITIONS EXIST -> Look for mean-reversion exit (crossing 0)
if(activeSpreadState == -1 && zScore <= 0.0)
{
PrintFormat("Z-Score (%.2f) reverted to 0 -> Closing SHORT SPREAD", zScore);
CloseAllSpreadPositions();
lastBarTime = currentBarTime;
}
else if(activeSpreadState == 1 && zScore >= 0.0)
{
PrintFormat("Z-Score (%.2f) reverted to 0 -> Closing LONG SPREAD", zScore);
CloseAllSpreadPositions();
lastBarTime = currentBarTime;
}
}
}
//+------------------------------------------------------------------+
//| Calculate Z-Score of the Symbol1 / Symbol2 Ratio |
//+------------------------------------------------------------------+
double CalculateZScore()
{
datetime timeArr[];
ArraySetAsSeries(timeArr, true);
if(CopyTime(Symbol1, PERIOD_CURRENT, 1, MaPeriod, timeArr) < MaPeriod)
return 0.0;
double spreads[];
ArrayResize(spreads, MaPeriod);
double sum = 0.0;
for(int i = 0; i < MaPeriod; i++)
{
double close1[1], close2[1];
// Time-synchronize prices between Symbol1 and Symbol2
if(CopyClose(Symbol1, PERIOD_CURRENT, timeArr[i], 1, close1) <= 0 ||
CopyClose(Symbol2, PERIOD_CURRENT, timeArr[i], 1, close2) <= 0 ||
close2[0] == 0)
{
return 0.0;
}
spreads[i] = close1[0] / close2[0];
sum += spreads[i];
}
double mean = sum / MaPeriod;
// Standard Deviation
double sumDev = 0.0;
for(int i = 0; i < MaPeriod; i++)
{
sumDev += MathPow(spreads[i] - mean, 2);
}
double stDev = MathSqrt(sumDev / MaPeriod);
if(stDev == 0.0) return 0.0;
// Calculate Z-Score of the most recently closed bar (index 0 of timeArr)
return (spreads[0] - mean) / stDev;
}
//+------------------------------------------------------------------+
//| Calculate Delta-Neutral Lot Size for Symbol 2 |
//+------------------------------------------------------------------+
double CalculateHedgeLots(double baseLots)
{
double price1 = SymbolInfoDouble(Symbol1, SYMBOL_ASK);
double contract1 = SymbolInfoDouble(Symbol1, SYMBOL_TRADE_CONTRACT_SIZE);
double price2 = SymbolInfoDouble(Symbol2, SYMBOL_ASK);
double contract2 = SymbolInfoDouble(Symbol2, SYMBOL_TRADE_CONTRACT_SIZE);
if(price2 <= 0 || contract2 <= 0)
return SymbolInfoDouble(Symbol2, SYMBOL_VOLUME_MIN);
// Target fiat notional exposure: Lots1 * ContractSize1 * Price1
double notionalValue1 = baseLots * contract1 * price1;
// Required Lots2 = NotionalValue1 / (ContractSize2 * Price2)
double rawLots2 = notionalValue1 / (contract2 * price2);
// Normalize to broker volume step constraints
double lotStep = SymbolInfoDouble(Symbol2, SYMBOL_VOLUME_STEP);
double minLot = SymbolInfoDouble(Symbol2, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(Symbol2, SYMBOL_VOLUME_MAX);
double normalizedLots2 = MathFloor(rawLots2 / lotStep) * lotStep;
if(normalizedLots2 < minLot) normalizedLots2 = minLot;
if(normalizedLots2 > maxLot) normalizedLots2 = maxLot;
return normalizedLots2;
}
//+------------------------------------------------------------------+
//| Open both legs simultaneously |
//+------------------------------------------------------------------+
void OpenSpread(ENUM_ORDER_TYPE type1, ENUM_ORDER_TYPE type2)
{
double lots1 = BaseLots1;
double lots2 = CalculateHedgeLots(lots1);
// Set filling type dynamically per symbol
trade.SetTypeFillingBySymbol(Symbol1);
if(type1 == ORDER_TYPE_BUY)
trade.Buy(lots1, Symbol1, 0, 0, 0, "ZScore_Leg1");
else
trade.Sell(lots1, Symbol1, 0, 0, 0, "ZScore_Leg1");
trade.SetTypeFillingBySymbol(Symbol2);
if(type2 == ORDER_TYPE_BUY)
trade.Buy(lots2, Symbol2, 0, 0, 0, "ZScore_Leg2");
else
trade.Sell(lots2, Symbol2, 0, 0, 0, "ZScore_Leg2");
}
//+------------------------------------------------------------------+
//| Determine current spread status |
//+------------------------------------------------------------------+
int GetSpreadState()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket > 0)
{
if(PositionGetInteger(POSITION_MAGIC) == MagicNumber &&
PositionGetString(POSITION_SYMBOL) == Symbol1)
{
ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
if(posType == POSITION_TYPE_BUY) return 1; // Long Spread
if(posType == POSITION_TYPE_SELL) return -1; // Short Spread
}
}
}
return 0; // Flat
}
//+------------------------------------------------------------------+
//| Close all positions belonging to this Magic Number |
//+------------------------------------------------------------------+
void CloseAllSpreadPositions()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket > 0)
{
if(PositionGetInteger(POSITION_MAGIC) == MagicNumber)
{
trade.PositionClose(ticket);
}
}
}
}
//+------------------------------------------------------------------+