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

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

Post by PTScalper »

Here is the complete MetaTrader 5 (MQL5) Expert Advisor (EA).

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);
           }
        }
     }
  }
//+------------------------------------------------------------------+
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 »

Highlights & MT5 Advantages

Native Multi-Currency Backtesting: Unlike MT4, MT5's Strategy Tester will properly simulate trades on both BTCUSD and ETHUSD at the exact same historical timestamps.Modern Execution Class (CTrade): Order sending and position closure use CTrade, handling filling modes (FOK, IOC, RETURN) automatically per symbol.Exact Time-Synchronized Math: Uses CopyTime and CopyClose by datetime to ensure the BTC and ETH price bars evaluated in the Z-score correspond to the exact same moment in time.Installation & SetupOpen MetaTrader 5 and press F4 to launch MetaEditor.

Click File $\rightarrow$ New $\rightarrow$ Expert Advisor (template) and name it BTC_ETH_ZScore_EA.Replace the entire code template with the script above and press F7 to compile.Attach the EA to a BTCUSD M5 or M15 chart and enable "Allow Algo Trading" in the top toolbar.
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 TradingView Pine Script (v5) implementation.

Because TradingView is built for both visual charting and strategy backtesting, this script functions as a dual-purpose tool: it plots the normalized Z-Score oscillator in a separate sub-window with color-coded alert zones, while simultaneously calculating and simulating the statistical arbitrage trades in the Strategy Tester.

Pine Script v5: BTC/ETH Z-Score Statistical Arbitrage Strategy & Indicator

Code: Select all

//@version=5
strategy(title="BTC/ETH Z-Score Mean Reversion Scalper", 
         shorttitle="BTC/ETH Z-Score", 
         overlay=false, 
         precision=2,
         initial_capital=10000, 
         default_qty_type=strategy.percent_of_equity, 
         default_qty_value=100, 
         commission_type=strategy.commission.percent, 
         commission_value=0.06)

// --- Inputs ---
var string G_PAIR   = "Asset Settings"
symbol2Input        = input.symbol("BINANCE:ETHUSDT", title="Hedge Asset (ETH)", group=G_PAIR)

var string G_PARAMS = "Z-Score Parameters"
maPeriod            = input.int(50, title="MA Lookback Period", minval=5, group=G_PARAMS)
zThreshold          = input.float(2.0, title="Z-Score Entry Threshold", minval=0.5, step=0.1, group=G_PARAMS)
exitThreshold       = input.float(0.0, title="Mean Reversion Exit Level", step=0.1, group=G_PARAMS)

// --- Data Fetching (Non-Repainting) ---
// Fetch secondary symbol's close price aligned to the current chart timeframe
close2 = request.security(symbol2Input, timeframe.period, close, gaps=barmerge.gaps_off, lookahead=barmerge.lookahead_off)

// --- Spread & Statistical Calculations ---
// 1. Calculate the Spread Ratio: Primary / Secondary
spreadRatio = close / close2

// 2. Rolling Moving Average & Standard Deviation
spreadMA    = ta.sma(spreadRatio, maPeriod)
spreadStd   = ta.stdev(spreadRatio, maPeriod)

// 3. Compute Z-Score
zScore      = spreadStd > 0 ? (spreadRatio - spreadMA) / spreadStd : 0.0

// --- Plotting & Visual Styling ---
// Dynamic color based on overbought / oversold conditions
zColor = zScore > zThreshold ? color.red : zScore < -zThreshold ? color.green : color.dodgerblue

plot(zScore, title="Z-Score", color=zColor, linewidth=2)

// Horizontal Reference Levels
hUpper = hline(zThreshold,  "Overbought (+Threshold)", color=color.new(color.red, 30), linestyle=hline.style_dashed)
hZero  = hline(exitThreshold, "Mean (0.0)",              color=color.new(color.gray, 50), linestyle=hline.style_solid)
hLower = hline(-zThreshold, "Oversold (-Threshold)",   color=color.new(color.green, 30), linestyle=hline.style_dashed)

// Highlight extreme deviation zones
fill(hUpper, hline(3.5, display=display.none), color=color.new(color.red, 90), title="Extreme Upper Zone")
fill(hLower, hline(-3.5, display=display.none), color=color.new(color.green, 90), title="Extreme Lower Zone")

// --- Trading Logic ---
// Long Spread: BTC is undervalued relative to ETH -> Buy BTC (primary leg)
enterLongSpread  = ta.crossunder(zScore, -zThreshold)
exitLongSpread   = ta.crossover(zScore, exitThreshold)

// Short Spread: BTC is overvalued relative to ETH -> Sell BTC (primary leg)
enterShortSpread = ta.crossover(zScore, zThreshold)
exitShortSpread  = ta.crossunder(zScore, exitThreshold)

// --- Strategy Execution ---
if (enterLongSpread)
    strategy.entry("Long Spread (Buy BTC)", strategy.long, comment="Entry: Oversold Spread")

if (exitLongSpread)
    strategy.close("Long Spread (Buy BTC)", comment="Exit: Mean Reverted")

if (enterShortSpread)
    strategy.entry("Short Spread (Sell BTC)", strategy.short, comment="Entry: Overbought Spread")

if (exitShortSpread)
    strategy.close("Short Spread (Sell BTC)", comment="Exit: Mean Reverted")

// --- Real-Time Alert Conditions ---
alertcondition(enterLongSpread,  title="Alert: Long Spread Signal",  message="Z-Score crossed below -{{threshold}}: BTC is undervalued vs ETH.")
alertcondition(enterShortSpread, title="Alert: Short Spread Signal", message="Z-Score crossed above +{{threshold}}: BTC is overvalued vs ETH.")
alertcondition(exitLongSpread or exitShortSpread, title="Alert: Spread Exit Signal", message="Z-Score returned to mean (0.0). Close open spread positions.")
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 to Use This on TradingView
1. Applying to standard BTC/USDT Charts
Open a BTCUSDT chart on a lower timeframe (5m or 15m work best for scalping).

Open the Pine Editor at the bottom of TradingView, paste the script, and click Add to Chart.

In the script settings gear icon, ensure the Hedge Asset matches your broker or exchange ticker for Ethereum (e.g., BINANCE:ETHUSDT, COINBASE:ETHUSD, or BYBIT:ETHUSDT.P).

2. Trading the True Synthetic Ratio Directly
If you want to view and trade the exact synthetic spread chart directly in TradingView:

In the TradingView symbol search bar, type: BINANCE:BTCUSDT / BINANCE:ETHUSDT

Add this script to the ratio chart.

Buying the ratio will automatically simulate longing the numerator (BTC) and shorting the denominator (ETH).
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 Pine Script FeaturesNo Repainting: Uses lookahead=barmerge.lookahead_off and standard closed-bar calculations to ensure backtest metrics match live forward results.Built-in Alerts: Includes three pre-configured alertcondition triggers so you can send webhooks directly to automated execution bots (e.g., 3Commas, PineConnector, or custom webhook servers).Dynamic Visualization: The line automatically shifts red when the spread is overextended to the upside ($Z > +2.0$) and green when overextended to the downside ($Z < -2.0$).
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Post Reply