Historical Volatility EA MQL5: Adaptive Position Sizing

Build an MQL5 Expert Advisor that uses Historical Volatility percentiles to dynamically adjust lot sizes and filter trades by volatility regime.

historical-volatility-ea-mql5-adaptive-position

Why Most EAs Die on Volatility Cliffs

I've seen dozens of otherwise solid Expert Advisors get blown up not because the strategy was wrong, but because the position sizing was static. A 0.1 lot trade during a low-volatility consolidation might be perfectly reasonable. The same 0.1 lot when volatility spikes into a news event or trend breakout can be a portfolio killer. The fix isn't complicated — it's Historical Volatility (HV).

Most traders and EA developers default to ATR for volatility measurement. While ATR is useful, it's a rolling average that reacts slowly to regime changes. Historical Volatility, calculated as the annualized standard deviation of log returns over a lookback window, gives you a cleaner signal for regime detection. Better yet, when you express HV as a percentile of its own history, you get a normalized score that tells you exactly where current volatility sits relative to the past N periods. This is pure gold for position sizing.

In this post, I'll walk you through building an Historical Volatility EA MQL5 that reads MT5's built-in volatility data, computes HV percentiles, and adjusts lot sizes dynamically. No black boxes — just clean MQL5 code you can adapt to your own strategies.

Understanding Historical Volatility for Regime Detection

Historical Volatility measures the dispersion of price returns over a defined period, annualized to make it comparable across assets. The formula is:

HV = √(252/N) * √(Σ(ln(Pi/Pi-1)²) / (N-1))

Where N is the number of periods (typically 20 or 21 trading days), and 252 annualizes daily volatility. The result is a percentage — for example, 15% means the asset is expected to move ±15% annually given recent price action.

The key insight for EAs is that HV itself isn't stationary. Markets cycle through low-volatility regimes (tight ranges, mean-reverting behavior) and high-volatility regimes (trends, news shocks, breakouts). By calculating the HV percentile — the rank of current HV relative to its values over a longer history — you get a 0–100% score. A percentile above 80% signals a high-volatility regime; below 20% signals low volatility.

Why not just use ATR percentiles? ATR is based on true range (high-low + gaps), which includes overnight gaps but is inherently less stable for annualization. HV based on log returns gives a more mathematically grounded risk metric, especially when scaling positions across different instruments or timeframes. For an EA, HV percentile is also less prone to the "cliff effect" where ATR jumps on a single large candle.

Why HV Percentile Beats Raw HV

Raw HV values are hard to interpret across different instruments. A 20% HV on EURUSD might be extreme, while the same value on a volatile crypto pair could be normal. The HV percentile solves this by normalizing everything to a 0–100 scale. This lets you define universal thresholds: "above 80% means reduce risk" works on any symbol, any timeframe. For multi-symbol EAs, this is a game changer — you don't need separate tuning per instrument.

Building the HV Percentile EA in MQL5

MQL5 provides the iVolatility function to access historical volatility data directly from the terminal. However, for percentile calculation we need to maintain our own array of HV values. Let's build a complete EA that:

  1. Computes HV over a user-defined lookback (default 20 days).
  2. Stores the last 252 HV values (one year of daily data) in a ring buffer.
  3. Calculates the percentile rank of current HV.
  4. Adjusts lot size proportionally: smaller lots in high volatility, larger in low volatility.
  5. Includes a volatility regime filter to skip trades when HV is extreme.

EA Input Parameters

Parameter Type Default Description
HV_Lookback int 20 Days used to compute current HV.
HV_History int 252 Number of past HV values for percentile calculation.
BaseLotSize double 0.1 Maximum lot size used at median volatility.
MinLotPct double 0.25 Fraction of BaseLotSize at highest volatility percentile.
MaxLotPct double 2.0 Fraction of BaseLotSize at lowest volatility percentile.
HighRegimeThreshold double 80.0 HV percentile above which we enter high-volatility mode.
LowRegimeThreshold double 20.0 HV percentile below which we enter low-volatility mode.

Core MQL5 Code: HV Calculation and Percentile

Here's the OnTick function with HV logic. We use daily data for HV calculation — this is critical because HV is a daily metric. You can adapt it to any timeframe, but daily gives you the cleanest signal for position sizing. Note that we use CopyRates with PERIOD_D1 to ensure we're working with daily closes, which avoids intraday noise.

//+------------------------------------------------------------------+
//| Calculate Historical Volatility percentile and adjust lot size   |
//+------------------------------------------------------------------+
double GetHVPctile(int hvLookback, int hvHistory, double &hvBuffer[]) {
    // Use daily data for HV calculation
    MqlRates rates[];
    ArraySetAsSeries(rates, true);
    if(CopyRates(_Symbol, PERIOD_D1, 0, hvLookback + hvHistory + 1, rates) < hvLookback + hvHistory + 1)
        return -1.0;
    
    // Compute current HV over hvLookback days
    double logReturns[];
    ArrayResize(logReturns, hvLookback);
    double sumSq = 0.0;
    for(int i = 0; i < hvLookback; i++) {
        if(rates[i].close > 0 && rates[i+1].close > 0) {
            logReturns[i] = MathLog(rates[i].close / rates[i+1].close);
            sumSq += logReturns[i] * logReturns[i];
        }
    }
    double hvCurrent = MathSqrt(252.0 / hvLookback * sumSq / (hvLookback - 1));
    
    // Build history buffer of past HV values
    ArrayResize(hvBuffer, hvHistory);
    for(int j = 0; j < hvHistory; j++) {
        double sumSqHist = 0.0;
        for(int k = 0; k < hvLookback; k++) {
            int idx = j + k + 1; // shift by 1 to avoid current
            if(rates[idx].close > 0 && rates[idx+1].close > 0) {
                double lr = MathLog(rates[idx].close / rates[idx+1].close);
                sumSqHist += lr * lr;
            }
        }
        hvBuffer[j] = MathSqrt(252.0 / hvLookback * sumSqHist / (hvLookback - 1));
    }
    
    // Calculate percentile rank of hvCurrent within hvBuffer
    int countBelow = 0;
    for(int i = 0; i < hvHistory; i++) {
        if(hvBuffer[i] < hvCurrent) countBelow++;
    }
    return (double)countBelow / hvHistory * 100.0;
}

Adaptive Lot Size Logic

Once we have the HV percentile, we map it linearly to a lot size multiplier. The mapping is straightforward — at 0% volatility (lowest historical), we use the maximum multiplier; at 100%, we use the minimum. This inverse relationship ensures we reduce exposure when risk is highest.

//+------------------------------------------------------------------+
//| Map HV percentile to lot size multiplier                         |
//+------------------------------------------------------------------+
double GetAdaptiveLotSize(double hvPctile, double baseLot, double minPct, double maxPct) {
    // Clamp percentile to [0,100]
    hvPctile = fmax(0.0, fmin(100.0, hvPctile));
    
    // Linear interpolation: at 0% volatility -> maxPct, at 100% -> minPct
    double multiplier = maxPct - (maxPct - minPct) * (hvPctile / 100.0);
    
    double lot = baseLot * multiplier;
    
    // Ensure lot respects broker minimum and step
    double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
    double stepLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
    lot = MathRound(lot / stepLot) * stepLot;
    lot = fmax(minLot, lot);
    
    return lot;
}

The philosophy here is simple: when volatility is high (percentile near 100), we reduce risk by cutting lot size. When volatility is low, we can afford to increase exposure. The MinLotPct and MaxLotPct parameters let you control the aggressiveness of the adjustment. For example, with MinLotPct=0.25 and MaxLotPct=2.0, at 100% HV percentile you trade 0.025 lots (0.1 * 0.25), at 0% you trade 0.2 lots (0.1 * 2.0).

Volatility Regime Filter

I also add a regime filter that prevents trading when HV percentile exceeds HighRegimeThreshold or drops below LowRegimeThreshold. This is optional but useful for strategies that perform poorly in extreme volatility environments (e.g., mean-reversion systems hate high volatility, while trend-followers might hate extremely low volatility).

//+------------------------------------------------------------------+
//| Check if current regime is tradable                              |
//+------------------------------------------------------------------+
bool IsTradableRegime(double hvPctile, double highThresh, double lowThresh) {
    if(hvPctile > highThresh) {
        Print("High volatility regime: HV percentile ", hvPctile, "% > ", highThresh, "%");
        return false;
    }
    if(hvPctile < lowThresh) {
        Print("Low

Want to build your own version?

Recreate similar entry logic, risk rules, and filters in TradingBotMaker—no MQL coding. Start free.

Community

Clap for the article and open comments only when you want to read them.

0 claps0 comments

Related articles