IC Markets

Statistical Arbitrage: Scalping the BTC/ETH Spread (MT4 Code Included)

Share, develop, and backtest custom MQL4/MQL5 Expert Advisors, Python data-scraping scripts, trading bots, and automated market alert systems.
PTScalper
Site Admin
Posts: 1114
Joined: Mon Jul 20, 2026 1:28 pm

Statistical Arbitrage: Scalping the BTC/ETH Spread (MT4 Code Included)

Post by PTScalper »

Hey traders,

Today I want to share a mean-reversion strategy that doesn't care whether the crypto market is in a bull run or a crypto winter. It relies entirely on the historical correlation between the two largest assets in the space: Bitcoin (BTC) and Ethereum (ETH).

When we treat BTC and ETH as a "pair," we can trade the divergence between them. If BTC pumps aggressively and ETH lags behind, the spread between them widens. Eventually, either ETH catches up or BTC pulls back to restore the historical balance. This is known as Statistical Arbitrage or Pairs Trading. Research shows that analyzing the short-term price difference (spread) between two co-moving assets can be highly effective, even if crypto spreads occasionally display "fat-tail" behavior rather than perfectly normal distributions.

The Logic: The Z-Score

We can't just look at the raw price difference because prices fluctuate widely over time. Instead, we use a statistical measurement called a Z-Score to normalize the data.Here is the exact math we will use in our indicator:Calculate the Spread (Ratio): Let's use BTC_Price / ETH_Price.Calculate the Moving Average (MA) of that spread over $n$ periods.Calculate the Standard Deviation ($\sigma$) of the spread over those same periods.Calculate the Z-Score:

The Z-score tells us exactly how many standard deviations the current spread has drifted away from its historical average.Explore how manipulating the Z-score threshold and moving average lookback period changes your trading frequency:

The Trading Rules (Scalping)

Timeframe: M5 or M15 (We want to scalp the intraday noise).

SHORT SIGNAL (Z-Score > +2.0):
BTC is historically overvalued relative to ETH.
Action: Sell BTCUSD and Buy ETHUSD simultaneously (using equal dollar amounts).

LONG SIGNAL (Z-Score < -2.0):
BTC is historically undervalued relative to ETH.
Action: Buy BTCUSD and Sell ETHUSD simultaneously.

EXIT (Z-Score = 0):
The spread has reverted to the mean. Close both positions for a net profit.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 1114
Joined: Mon Jul 20, 2026 1:28 pm

Re: Statistical Arbitrage: Scalping the BTC/ETH Spread (MT4 Code Included)

Post by PTScalper »

MQL4 Code: Custom Z-Score Indicator

To trade this in MetaTrader 4, you need to see the Z-score plotted in real-time. Here is an open-source custom indicator I wrote for the community. It calculates the Z-score of the BTC/ETH ratio on the fly and draws the +2, -2, and 0 standard deviation lines in a sub-window.

Code: Select all

//+------------------------------------------------------------------+
//|                                              BTC_ETH_ZScore.mq4  |
//|                                      Forum Community Open Source |
//+------------------------------------------------------------------+
#property copyright "Open Source"
#property indicator_separate_window
#property indicator_buffers 1
#property indicator_color1 clrDodgerBlue

//--- input parameters
extern string   Symbol1 = "BTCUSD";
extern string   Symbol2 = "ETHUSD";
extern int      MaPeriod = 50;

//--- indicator buffers
double         ZScoreBuffer[];
double         SpreadBuffer[];

int OnInit()
  {
   SetIndexStyle(0, DRAW_LINE);
   SetIndexBuffer(0, ZScoreBuffer);
   SetIndexLabel(0, "Z-Score");
   
   IndicatorShortName("BTC/ETH Z-Score (" + IntegerToString(MaPeriod) + ")");
   
   // Set our overbought/oversold/mean levels
   SetLevelValue(0, 2.0);
   SetLevelValue(1, -2.0);
   SetLevelValue(2, 0.0);
   SetLevelStyle(STYLE_DOT, 1, clrGray);
   
   ArrayResize(SpreadBuffer, Bars);
   return(INIT_SUCCEEDED);
  }

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   int limit = rates_total - prev_calculated;
   if(limit == 0) limit = 1;
   
   ArrayResize(SpreadBuffer, rates_total);

   for(int i = limit - 1; i >= 0; i--)
     {
      // Fetch prices for both symbols
      double close1 = iClose(Symbol1, 0, i);
      double close2 = iClose(Symbol2, 0, i);
      
      if(close2 == 0) continue; // Avoid division by zero
      
      // 1. Calculate ratio (spread)
      SpreadBuffer[i] = close1 / close2;
      
      // 2. Calculate Mean and StDev to get Z-Score
      if(i < rates_total - MaPeriod)
        {
         double sum = 0;
         for(int j = 0; j < MaPeriod; j++) sum += SpreadBuffer[i+j];
         double ma = sum / MaPeriod;
         
         double sum_dev = 0;
         for(int j = 0; j < MaPeriod; j++)
           {
            sum_dev += MathPow(SpreadBuffer[i+j] - ma, 2);
           }
         double stdev = MathSqrt(sum_dev / MaPeriod);
         
         // 3. Output Z-Score
         if(stdev != 0)
            ZScoreBuffer[i] = (SpreadBuffer[i] - ma) / stdev;
         else
            ZScoreBuffer[i] = 0;
        }
     }
     
   return(rates_total);
  }
//+------------------------------------------------------------------+
Installation Instructions:

Open your MT4 MetaEditor (F4).

Create a new Custom Indicator, paste the code above, and hit Compile.

Attach it to your BTCUSD chart. (Important: Ensure Symbol1 and Symbol2 inputs exactly match your broker's symbol names, like "BTCUSD.pro" or "ETHUSD.ecn").
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 1114
Joined: Mon Jul 20, 2026 1:28 pm

Re: Statistical Arbitrage: Scalping the BTC/ETH Spread (MT4 Code Included)

Post by PTScalper »

Moving this strategy to MetaTrader 5 (MT5) is actually a massive upgrade. The biggest limitation of MT4 for statistical arbitrage is that its strategy tester cannot backtest multiple currencies simultaneously. MT5 natively supports true multi-currency backtesting, meaning you can accurately test this exact BTC/ETH pairing over historical data.

Additionally, MQL5 handles multi-symbol data arrays a bit differently, requiring time-synchronization between the two assets to prevent errors if one symbol is missing a tick.

Here is the updated, time-aligned custom indicator written specifically for MQL5.

MQL5 Code: Custom Z-Score Indicator

Code: Select all

//+------------------------------------------------------------------+
//|                                              BTC_ETH_ZScore.mq5  |
//|                                      Forum Community Open Source |
//+------------------------------------------------------------------+
#property copyright "Open Source"
#property version   "1.00"
#property indicator_separate_window

//--- Indicator buffers and plots
#property indicator_buffers 2
#property indicator_plots   1

//--- Plot Z-Score
#property indicator_label1  "Z-Score"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrDodgerBlue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1

//--- Levels
#property indicator_level1  2.0
#property indicator_level2  -2.0
#property indicator_level3  0.0
#property indicator_levelcolor clrGray
#property indicator_levelstyle STYLE_DOT

//--- Input parameters
input string   Symbol1 = "BTCUSD"; // Primary Asset
input string   Symbol2 = "ETHUSD"; // Correlated Asset
input int      MaPeriod = 50;      // Moving Average Period

//--- Indicator buffers
double         ZScoreBuffer[];
double         SpreadBuffer[];     // Hidden buffer for calculations

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
   // Bind arrays to indicator buffers
   SetIndexBuffer(0, ZScoreBuffer, INDICATOR_DATA);
   SetIndexBuffer(1, SpreadBuffer, INDICATOR_CALCULATIONS);
   
   // Set short name
   IndicatorSetString(INDICATOR_SHORTNAME, "BTC/ETH Z-Score (" + IntegerToString(MaPeriod) + ")");
   
   // Initialize arrays to zero
   ArrayInitialize(ZScoreBuffer, 0.0);
   ArrayInitialize(SpreadBuffer, 0.0);
   
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   // Wait until we have enough bars
   if(rates_total < MaPeriod) return 0;
   
   // Define starting point to optimize calculation speed
   int start = (prev_calculated == 0) ? 0 : prev_calculated - 1;
   
   double close2[1]; // Array to hold the second symbol's price
   
   for(int i = start; i < rates_total; i++)
     {
      // 1. Fetch time-aligned price for Symbol2 using CopyClose
      // We use time[i] to ensure perfect synchronization between the two charts
      if(CopyClose(Symbol2, PERIOD_CURRENT, time[i], 1, close2) <= 0 || close2[0] == 0)
        {
         // If data is missing or fetching fails, carry forward the previous value
         SpreadBuffer[i] = (i > 0) ? SpreadBuffer[i-1] : 0.0;
         ZScoreBuffer[i] = (i > 0) ? ZScoreBuffer[i-1] : 0.0;
         continue;
        }
        
      // 2. Calculate the Spread (Ratio)
      SpreadBuffer[i] = close[i] / close2[0];
      
      // Wait until we have enough spread data to calculate MA and StDev
      if(i >= MaPeriod - 1)
        {
         // 3. Calculate Mean (Simple Moving Average)
         double sum = 0;
         for(int j = 0; j < MaPeriod; j++) 
           {
            sum += SpreadBuffer[i-j];
           }
         double ma = sum / MaPeriod;
         
         // 4. Calculate Standard Deviation
         double sum_dev = 0;
         for(int j = 0; j < MaPeriod; j++)
           {
            sum_dev += MathPow(SpreadBuffer[i-j] - ma, 2);
           }
         double stdev = MathSqrt(sum_dev / MaPeriod);
         
         // 5. Output Z-Score
         if(stdev != 0)
            ZScoreBuffer[i] = (SpreadBuffer[i] - ma) / stdev;
         else
            ZScoreBuffer[i] = 0.0;
        }
      else
        {
         ZScoreBuffer[i] = 0.0;
        }
     }
     
   return(rates_total);
  }
//+------------------------------------------------------------------+
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 1114
Joined: Mon Jul 20, 2026 1:28 pm

Re: Statistical Arbitrage: Scalping the BTC/ETH Spread (MT4 Code Included)

Post by PTScalper »

Key Differences from the MT4 Version:

Time Alignment (CopyClose): MQL5 is much stricter about how data is handled. Instead of simply pulling iClose() by bar index (which can result in misaligned data if one crypto asset prints a bar and the other doesn't), this code queries the exact datetime of the current bar (time) to guarantee the BTC and ETH prices matched up perfectly at that exact moment.

Buffer Management: MQL5 requires strict separation of buffers that are drawn on the screen (INDICATOR_DATA) and buffers used purely for math (INDICATOR_CALCULATIONS).

Array Indexing: By default, MQL5 OnCalculate arrays go from oldest to newest (index 0 is the oldest data). The loop iterates forwards, which is standard practice in MQL5 and speeds up calculation.

Installation Instructions:

Open your MT5 MetaEditor (F4).

Right-click the Indicators folder -> New -> Custom Indicator.

Name it BTC_ETH_ZScore, paste the code above over the default template, and hit Compile (F7).

Attach it to a BTCUSD chart. Ensure both assets exist in your MT5 Market Watch window so the indicator can pull the secondary symbol's data.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 1114
Joined: Mon Jul 20, 2026 1:28 pm

Re: Statistical Arbitrage: Scalping the BTC/ETH Spread (MT4 Code Included)

Post by PTScalper »

Moving this to cTrader is an excellent choice. cTrader’s native C# environment (cAlgo) is arguably much cleaner for multi-asset trading than MQL because it handles time synchronization and external data series natively and safely.

Instead of manually coding the math loops like we did in MQL4/MQL5, cTrader allows us to store the BTC/ETH spread in a custom IndicatorDataSeries and pass that directly into its built-in Moving Average and Standard Deviation functions. This makes the code significantly shorter and faster.

Here is the fully synchronized cTrader (cAlgo) Custom Indicator.

cAlgo C# Code: Custom Z-Score Indicator

Code: Select all

using System;
using cAlgo.API;
using cAlgo.API.Internals;
using cAlgo.API.Indicators;
using cAlgo.Indicators;

namespace cAlgo
{
    [Indicator(IsOverlay = false, TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class BTCETHZScore : Indicator
    {
        [Parameter("Secondary Symbol", DefaultValue = "ETHUSD")]
        public string Symbol2Name { get; set; }

        [Parameter("MA Period", DefaultValue = 50)]
        public int MaPeriod { get; set; }

        [Output("Z-Score", LineColor = "DodgerBlue", Thickness = 2)]
        public IndicatorDataSeries ZScore { get; set; }

        [Output("Upper Level (+2)", LineColor = "Gray", LineStyle = LineStyle.Lines)]
        public IndicatorDataSeries UpperLevel { get; set; }

        [Output("Mean (0)", LineColor = "Gray", LineStyle = LineStyle.Lines)]
        public IndicatorDataSeries ZeroLine { get; set; }

        [Output("Lower Level (-2)", LineColor = "Gray", LineStyle = LineStyle.Lines)]
        public IndicatorDataSeries LowerLevel { get; set; }

        private Bars _symbol2Bars;
        private IndicatorDataSeries _spread;
        private SimpleMovingAverage _ma;
        private StandardDeviation _stdDev;

        protected override void Initialize()
        {
            // Retrieve the secondary symbol's data using the current chart's timeframe
            _symbol2Bars = MarketData.GetBars(TimeFrame, Symbol2Name);
            
            // Create a custom data series to hold the BTC/ETH ratio
            _spread = CreateDataSeries();

            // Pass our custom spread series directly into cTrader's built-in math indicators
            _ma = Indicators.SimpleMovingAverage(_spread, MaPeriod);
            _stdDev = Indicators.StandardDeviation(_spread, MaPeriod, MovingAverageType.Simple);
        }

        public override void Calculate(int index)
        {
            // Draw static threshold lines
            UpperLevel[index] = 2.0;
            ZeroLine[index] = 0.0;
            LowerLevel[index] = -2.0;

            // 1. Time Synchronization
            // Get the time of the current chart bar, then find the matching bar on the secondary asset
            var time = Bars.OpenTimes[index];
            var index2 = _symbol2Bars.OpenTimes.GetIndexByTime(time);

            if (index2 == -1)
            {
                // If no matching bar exists (data gap), carry forward the previous spread value
                _spread[index] = index > 0 ? _spread[index - 1] : 0;
            }
            else
            {
                // Both bars exist, calculate the exact ratio
                double close1 = Bars.ClosePrices[index];
                double close2 = _symbol2Bars.ClosePrices[index2];

                if (close2 > 0)
                    _spread[index] = close1 / close2;
                else
                    _spread[index] = index > 0 ? _spread[index - 1] : 0;
            }

            // 2. Calculate Z-Score
            if (index >= MaPeriod)
            {
                double currentMa = _ma.Result[index];
                double currentStdDev = _stdDev.Result[index];

                if (currentStdDev > 0 && !double.IsNaN(currentStdDev))
                {
                    ZScore[index] = (_spread[index] - currentMa) / currentStdDev;
                }
                else
                {
                    ZScore[index] = 0.0;
                }
            }
            else
            {
                ZScore[index] = 0.0;
            }
        }
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 1114
Joined: Mon Jul 20, 2026 1:28 pm

Re: Statistical Arbitrage: Scalping the BTC/ETH Spread (MT4 Code Included)

Post by PTScalper »

Key Differences in cTrader:

GetIndexByTime(time): This is cTrader's secret weapon for pairs trading. It perfectly aligns the timestamp of ETH with the timestamp of BTC. If one asset is missing a tick or has a data gap, it safely references the last known price.

Built-in Indicator Nesting: Notice how we declare _ma = Indicators.SimpleMovingAverage(_spread, MaPeriod). In MQL, we had to write a manual for loop to calculate the math. In cTrader, we simply feed our custom _spread buffer into the native StandardDeviation algorithm, saving processing power.

Flat Lines: cTrader allows you to output horizontal threshold lines natively via the [Output] tags, meaning the +2, 0, and -2 levels will draw automatically without requiring manual UI configuration.

Installation Instructions:

Open cTrader and go to the Automate tab on the left menu.

Click the + icon next to "Indicators" and select New Indicator.

Name it BTCETHZScore and replace the default template with the code above.

Click the Build icon (or press Ctrl+B) at the top.

Go to your BTCUSD chart, open the Indicators list -> Custom, and add your new Z-Score tool.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 1114
Joined: Mon Jul 20, 2026 1:28 pm

Re: Statistical Arbitrage: Scalping the BTC/ETH Spread (MT4 Code Included)

Post by PTScalper »

Here is the complete cTrader cBot in C#. It takes the math from our custom indicator and wraps it in a fully automated execution engine.

The most critical part of this bot is the Delta-Neutral Position Sizing algorithm. If you buy 1 BTC, you cannot just sell 1 ETH—the dollar values would be wildly mismatched, exposing you to directional market risk. The bot dynamically calculates exactly how much ETH to trade based on the current BTC/ETH price ratio so that both "legs" of the trade have the exact same fiat value.

C# cBot: Delta-Neutral Z-Score Arbitrage

Code: Select all

using System;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Indicators;
using cAlgo.API.Internals;
using cAlgo.Indicators;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class BTCETHZScoreArbitrage : Robot
    {
        [Parameter("Secondary Symbol", DefaultValue = "ETHUSD")]
        public string Symbol2Name { get; set; }

        [Parameter("Z-Score Threshold", DefaultValue = 2.0)]
        public double ZScoreThreshold { get; set; }

        [Parameter("MA Period", DefaultValue = 50)]
        public int MaPeriod { get; set; }

        [Parameter("Base Volume (BTC)", DefaultValue = 0.1)]
        public double BaseVolume { get; set; }

        private Symbol _symbol2;
        private Bars _symbol2Bars;
        private IndicatorDataSeries _spread;
        private SimpleMovingAverage _ma;
        private StandardDeviation _stdDev;
        
        private readonly string _botLabel = "ZScoreArb";
        private bool _isSpreadLong = false;
        private bool _isSpreadShort = false;

        protected override void OnStart()
        {
            // 1. Initialize secondary symbol
            _symbol2 = Symbols.GetSymbol(Symbol2Name);
            _symbol2Bars = MarketData.GetBars(TimeFrame, Symbol2Name);

            // 2. Setup Indicator Math
            _spread = CreateDataSeries();
            _ma = Indicators.SimpleMovingAverage(_spread, MaPeriod);
            _stdDev = Indicators.StandardDeviation(_spread, MaPeriod, MovingAverageType.Simple);
        }

        protected override void OnBar()
        {
            int index = Bars.Count - 1;
            int index2 = _symbol2Bars.OpenTimes.GetIndexByTime(Bars.OpenTimes[index]);

            // Ensure we have synchronized data for both assets
            if (index2 == -1) return;

            double close1 = Bars.ClosePrices[index];
            double close2 = _symbol2Bars.ClosePrices[index2];

            if (close2 <= 0) return;

            // 3. Calculate Spread and Z-Score
            _spread[index] = close1 / close2;

            if (index < MaPeriod) return; // Wait for enough data

            double currentMa = _ma.Result[index];
            double currentStdDev = _stdDev.Result[index];

            if (currentStdDev <= 0 || double.IsNaN(currentStdDev)) return;

            double zScore = (_spread[index] - currentMa) / currentStdDev;
            
            // 4. Update Position State
            UpdatePositionState();

            // 5. Trading Logic
            if (!_isSpreadLong && !_isSpreadShort)
            {
                // NO OPEN POSITIONS: Look for entries
                
                if (zScore > ZScoreThreshold)
                {
                    // BTC is overvalued vs ETH. Sell BTC, Buy ETH.
                    ExecuteSpreadTrade(TradeType.Sell, TradeType.Buy);
                }
                else if (zScore < -ZScoreThreshold)
                {
                    // BTC is undervalued vs ETH. Buy BTC, Sell ETH.
                    ExecuteSpreadTrade(TradeType.Buy, TradeType.Sell);
                }
            }
            else
            {
                // POSITIONS OPEN: Look for mean reversion (Z-Score crosses 0)
                
                if (_isSpreadShort && zScore <= 0)
                {
                    CloseAllSpreadPositions();
                }
                else if (_isSpreadLong && zScore >= 0)
                {
                    CloseAllSpreadPositions();
                }
            }
        }

        private void ExecuteSpreadTrade(TradeType primaryAction, TradeType secondaryAction)
        {
            // Normalize the primary volume for BTC
            double volume1 = Symbol.NormalizeVolumeInUnits(BaseVolume, RoundingMode.ToNearest);

            // Calculate delta-neutral volume for ETH
            // Formula: Vol2 = Vol1 * (Price1 / Price2)
            double priceRatio = Symbol.Ask / _symbol2.Ask;
            double rawVolume2 = volume1 * priceRatio;
            
            // Normalize the secondary volume according to broker step settings
            double volume2 = _symbol2.NormalizeVolumeInUnits(rawVolume2, RoundingMode.ToNearest);

            // Execute simultaneously
            ExecuteMarketOrderAsync(primaryAction, Symbol.Name, volume1, _botLabel);
            ExecuteMarketOrderAsync(secondaryAction, _symbol2.Name, volume2, _botLabel);
        }

        private void UpdatePositionState()
        {
            var openPositions = Positions.FindAll(_botLabel);
            
            if (openPositions.Length == 0)
            {
                _isSpreadLong = false;
                _isSpreadShort = false;
                return;
            }

            // Determine spread direction based on the primary symbol's trade type
            var primaryPosition = openPositions.FirstOrDefault(p => p.SymbolName == Symbol.Name);
            
            if (primaryPosition != null)
            {
                _isSpreadLong = primaryPosition.TradeType == TradeType.Buy;
                _isSpreadShort = primaryPosition.TradeType == TradeType.Sell;
            }
        }

        private void CloseAllSpreadPositions()
        {
            var openPositions = Positions.FindAll(_botLabel);
            foreach (var position in openPositions)
            {
                ClosePositionAsync(position);
            }
            
            _isSpreadLong = false;
            _isSpreadShort = false;
        }
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 1114
Joined: Mon Jul 20, 2026 1:28 pm

Re: Statistical Arbitrage: Scalping the BTC/ETH Spread (MT4 Code Included)

Post by PTScalper »

How the Mechanics Work:
OnBar Execution: The logic calculates strictly at the close of the candlestick (OnBar) rather than on every tick. Tick data in crypto can be highly fragmented, causing the Z-score to wildly spike for a millisecond and trigger false entries. Bar closes smooth this out.

Delta-Neutral Sizing: The ExecuteSpreadTrade method looks at your requested BTC volume, multiplies it by the current BTC/ETH price ratio, and calculates exactly how many ETH units equal that same dollar value. It then runs both sizes through NormalizeVolumeInUnits to ensure the sizes comply with your broker's lot-stepping rules.

Asynchronous Execution: It uses ExecuteMarketOrderAsync and ClosePositionAsync. By not waiting for the server to confirm the first trade before sending the second, both orders hit the matching engine at practically the same millisecond, virtually eliminating execution "leg" delays.

Label Tracking: All orders are tagged with ZScoreArb. The bot manages the trades purely by scanning open positions for this label. This means you can still manual-trade BTC or run other bots on the same account without them interfering with one another.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 1114
Joined: Mon Jul 20, 2026 1:28 pm

Re: Statistical Arbitrage: Scalping the BTC/ETH Spread (MT4 Code Included)

Post by PTScalper »

Here is the complete MetaTrader 4 Expert Advisor (EA) in MQL4.

This EA is fully self-contained (it calculates the Z-Score directly without needing external indicators) and features dynamic delta-neutral position sizing, new-bar execution to prevent tick-whipsaws, and synchronized multi-asset order management.

MQL4 Code: Delta-Neutral Z-Score Scalping EA

Code: Select all

//+------------------------------------------------------------------+
//|                                           BTC_ETH_ZScore_EA.mq4  |
//|                                      Forum Community Open Source |
//+------------------------------------------------------------------+
#property copyright "Open Source"
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict

//--- Inputs
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;       // Lookback Period for Z-Score
input double   BaseLots1         = 0.10;     // Base Lot Size for Symbol 1
input int      Slippage          = 10;       // Slippage in points
input int      MagicNumber       = 778899;   // Unique Magic Number

//--- Global Variables
datetime lastBarTime = 0;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   // Check if both symbols are available in Market Watch
   if(!MarketInfo(Symbol1, MODE_BID) || !MarketInfo(Symbol2, MODE_BID))
     {
      Print("Error: Make sure both ", Symbol1, " and ", Symbol2, " are in the Market Watch window.");
      return(INIT_FAILED);
     }
     
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   // 1. Execute only on the open of a new bar (prevents intraday noise/spikes)
   datetime currentBarTime = iTime(Symbol1, 0, 0);
   if(currentBarTime == lastBarTime) return;
   
   // 2. Calculate current Z-Score on completed bars
   double zScore = CalculateZScore();
   if(zScore == 0.0) return; // Wait until enough data is collected
   
   // 3. Determine current position state
   int activeSpreadState = GetSpreadState(); // 0 = None, 1 = Long Spread, -1 = Short Spread
   
   // 4. Trade Execution Logic
   if(activeSpreadState == 0)
     {
      // NO OPEN POSITIONS -> Look for entry
      if(zScore > ZScoreThreshold)
        {
         // BTC is overvalued vs ETH -> Short Spread (Sell BTC, Buy ETH)
         Print("Z-Score > ", ZScoreThreshold, " (", zScore, ") -> Opening SHORT SPREAD");
         OpenSpread(OP_SELL, OP_BUY);
         lastBarTime = currentBarTime;
        }
      else if(zScore < -ZScoreThreshold)
        {
         // BTC is undervalued vs ETH -> Long Spread (Buy BTC, Sell ETH)
         Print("Z-Score < -", ZScoreThreshold, " (", zScore, ") -> Opening LONG SPREAD");
         OpenSpread(OP_BUY, OP_SELL);
         lastBarTime = currentBarTime;
        }
     }
   else
     {
      // OPEN POSITIONS EXIST -> Look for mean reversion (crossing 0)
      if(activeSpreadState == -1 && zScore <= 0.0)
        {
         Print("Z-Score returned to 0 (", zScore, ") -> Closing SHORT SPREAD");
         CloseAllSpreadPositions();
         lastBarTime = currentBarTime;
        }
      else if(activeSpreadState == 1 && zScore >= 0.0)
        {
         Print("Z-Score returned to 0 (", zScore, ") -> Closing LONG SPREAD");
         CloseAllSpreadPositions();
         lastBarTime = currentBarTime;
        }
     }
  }

//+------------------------------------------------------------------+
//| Calculate Z-Score of the Symbol1 / Symbol2 Ratio                 |
//+------------------------------------------------------------------+
double CalculateZScore()
  {
   if(iBars(Symbol1, 0) < MaPeriod + 1 || iBars(Symbol2, 0) < MaPeriod + 1) return 0.0;
   
   double spreads[];
   ArrayResize(spreads, MaPeriod);
   
   double sum = 0.0;
   for(int i = 1; i <= MaPeriod; i++)
     {
      double c1 = iClose(Symbol1, 0, i);
      double c2 = iClose(Symbol2, 0, i);
      
      if(c2 == 0) return 0.0;
      
      spreads[i-1] = c1 / c2;
      sum += spreads[i-1];
     }
     
   double mean = sum / MaPeriod;
   
   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;
   
   // Current completed ratio (bar 1)
   double currentRatio = iClose(Symbol1, 0, 1) / iClose(Symbol2, 0, 1);
   return (currentRatio - mean) / stDev;
  }

//+------------------------------------------------------------------+
//| Calculate Delta-Neutral Lot Size for Symbol 2                    |
//+------------------------------------------------------------------+
double CalculateHedgeLots(double baseLots)
  {
   double price1 = MarketInfo(Symbol1, MODE_ASK);
   double contract1 = MarketInfo(Symbol1, MODE_LOTSIZE);
   
   double price2 = MarketInfo(Symbol2, MODE_ASK);
   double contract2 = MarketInfo(Symbol2, MODE_LOTSIZE);
   
   if(price2 <= 0 || contract2 <= 0) return MarketInfo(Symbol2, MODE_MINLOT);
   
   // Target dollar exposure: Lots1 * ContractSize1 * Price1
   double notionalValue1 = baseLots * contract1 * price1;
   
   // Required Lots2 = NotionalValue1 / (ContractSize2 * Price2)
   double rawLots2 = notionalValue1 / (contract2 * price2);
   
   // Normalize according to broker lot-stepping constraints
   double lotStep = MarketInfo(Symbol2, MODE_LOTSTEP);
   double minLot  = MarketInfo(Symbol2, MODE_MINLOT);
   double maxLot  = MarketInfo(Symbol2, MODE_MAXLOT);
   
   double normalizedLots2 = MathFloor(rawLots2 / lotStep) * lotStep;
   
   if(normalizedLots2 < minLot) normalizedLots2 = minLot;
   if(normalizedLots2 > maxLot) normalizedLots2 = maxLot;
   
   return normalizedLots2;
  }

//+------------------------------------------------------------------+
//| Open both legs simultaneously                                    |
//+------------------------------------------------------------------+
void OpenSpread(int type1, int type2)
  {
   double lots1 = BaseLots1;
   double lots2 = CalculateHedgeLots(lots1);
   
   double price1 = (type1 == OP_BUY) ? MarketInfo(Symbol1, MODE_ASK) : MarketInfo(Symbol1, MODE_BID);
   double price2 = (type2 == OP_BUY) ? MarketInfo(Symbol2, MODE_ASK) : MarketInfo(Symbol2, MODE_BID);
   
   // Execute Leg 1
   int ticket1 = OrderSend(Symbol1, type1, lots1, price1, Slippage, 0, 0, "ZScore_Leg1", MagicNumber, 0, (type1 == OP_BUY ? clrGreen : clrRed));
   if(ticket1 < 0)
     {
      Print("Error opening Leg 1 (", Symbol1, "): ", GetLastError());
      return;
     }
     
   // Execute Leg 2
   int ticket2 = OrderSend(Symbol2, type2, lots2, price2, Slippage, 0, 0, "ZScore_Leg2", MagicNumber, 0, (type2 == OP_BUY ? clrGreen : clrRed));
   if(ticket2 < 0)
     {
      Print("Error opening Leg 2 (", Symbol2, "): ", GetLastError());
      // Emergency: If Leg 2 fails, you may want to close Leg 1 to avoid unhedged directional exposure
     }
  }

//+------------------------------------------------------------------+
//| Check the current spread position status                         |
//+------------------------------------------------------------------+
int GetSpreadState()
  {
   for(int i = OrdersTotal() - 1; i >= 0; i--)
     {
      if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
        {
         if(OrderMagicNumber() == MagicNumber && OrderSymbol() == Symbol1)
           {
            if(OrderType() == OP_BUY)  return 1;  // Long Spread active
            if(OrderType() == OP_SELL) return -1; // Short Spread active
           }
        }
     }
   return 0; // No positions active
  }

//+------------------------------------------------------------------+
//| Close all open orders associated with this Magic Number          |
//+------------------------------------------------------------------+
void CloseAllSpreadPositions()
  {
   for(int i = OrdersTotal() - 1; i >= 0; i--)
     {
      if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
        {
         if(OrderMagicNumber() == MagicNumber)
           {
            double closePrice = 0.0;
            if(OrderType() == OP_BUY)
               closePrice = MarketInfo(OrderSymbol(), MODE_BID);
            else if(OrderType() == OP_SELL)
               closePrice = MarketInfo(OrderSymbol(), MODE_ASK);
               
            if(closePrice > 0)
              {
               bool closed = OrderClose(OrderTicket(), OrderLots(), closePrice, Slippage, clrYellow);
               if(!closed)
                  Print("Failed to close ticket #", OrderTicket(), " Error: ", GetLastError());
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 1114
Joined: Mon Jul 20, 2026 1:28 pm

Re: Statistical Arbitrage: Scalping the BTC/ETH Spread (MT4 Code Included)

Post by PTScalper »

Key Architectural Details

Delta-Neutral Sizing (CalculateHedgeLots): Calculates dollar notional exposure ($Lots \times ContractSize \times Price$) on BTC and matches it on ETH. It then rounds down to the broker's specific MODE_LOTSTEP and checks MODE_MINLOT / MODE_MAXLOT.Bar-Close Protection (OnTick): Calculates the Z-Score on completed bars (iClose(..., 1)). This avoids entering and exiting repeatedly within the same 5-minute bar due to tick noise.Magic Number Isolation: Every trade is tagged with MagicNumber = 778899. You can run this EA alongside other strategies without interference.MT4 Backtesting Caveat: The MT4 Strategy Tester cannot natively send orders to a second symbol (Symbol2) during backtests. To test this EA, run it live on a demo account or use an MT5 instance where multi-currency backtesting is natively supported.Installation StepsOpen MetaTrader 4 and press F4 to launch MetaEditor.Go to File $\rightarrow$ New $\rightarrow$ Expert Advisor (template) and name it BTC_ETH_ZScore_EA.Paste the code above, overwriting all default template code.Press F7 to Compile.In MT4, open a BTCUSD M5 or M15 chart, drag the EA onto the chart, and ensure "Allow Live Trading" is checked in the Common tab.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Post Reply