Insights/Insights & Articles/Multi-Timeframe Backtesting: Testing Strategies Across M5, H1, and D1 Simultaneously

Multi-Timeframe Backtesting: Testing Strategies Across M5, H1, and D1 Simultaneously

We've never shipped a profitable strategy that only looked at one timeframe. Not once in twelve years. The D1 tells you where you are, the H1 tells you what's forming, and the M5 tells you when to pull the trigger. Simple as that. The hard part? Backtesting all three together without lying to yourself.

Apr 1, 2026

Multi-Timeframe Backtesting: Testing Strategies Across M5, H1, and D1 Simultaneously

Multi-Timeframe Backtesting: Testing Strategies Across M5, H1, and D1 Simultaneously

We've never shipped a profitable strategy that only looked at one timeframe. Not once in twelve years. The D1 tells you where you are, the H1 tells you what's forming, and the M5 tells you when to pull the trigger. Simple as that. The hard part? Backtesting all three together without lying to yourself. Most people screw this up — and we're going to show you exactly how to not be most people.

The Multi-Timeframe Concept

D1 (Daily)  → TREND DIRECTION
               Is the market trending up or down?
               EMA(50) above EMA(200) = bullish regime

H1 (Hourly) → SETUP IDENTIFICATION
               Has a tradeable pattern formed?
               Price pulls back to EMA(21) in the trend direction

M5 (5-min)  → ENTRY TIMING
               Where exactly to enter?
               Breakout above the H1 pullback high

Why this matters for backtesting: If you're backtesting on M5 alone, you're just measuring noise. We've seen people get excited about a 55% win rate on M5 that completely evaporates when you realize they were buying into a D1 downtrend half the time. Stack the H1 and D1 filters on top, test them together, and suddenly you're filtering out garbage trades instead of celebrating them.

MQL4: Multi-Timeframe Access in Strategy Tester

MT4's Strategy Tester is locked to one timeframe. You pick M5, it runs on M5. But MQL4 gives you iMA(), iClose(), and friends that reach across timeframes — and that's where the real power lives. We've built probably forty EAs using this pattern:

input int DailyTrendEMAFast  = 50;
input int DailyTrendEMASlow  = 200;
input int HourlySetupEMA     = 21;
input int EntryATRPeriod     = 14;

void OnTick()
{
    // === LAYER 1: DAILY TREND ===
    double d1_ema_fast = iMA(Symbol(), PERIOD_D1, DailyTrendEMAFast,
                             0, MODE_EMA, PRICE_CLOSE, 0);
    double d1_ema_slow = iMA(Symbol(), PERIOD_D1, DailyTrendEMASlow,
                             0, MODE_EMA, PRICE_CLOSE, 0);

    bool dailyBullish = d1_ema_fast > d1_ema_slow;
    bool dailyBearish = d1_ema_fast < d1_ema_slow;

    // === LAYER 2: HOURLY SETUP ===
    double h1_ema = iMA(Symbol(), PERIOD_H1, HourlySetupEMA,
                        0, MODE_EMA, PRICE_CLOSE, 0);
    double h1_close = iClose(Symbol(), PERIOD_H1, 1);  // Prior H1 bar close
    double h1_high  = iHigh(Symbol(), PERIOD_H1, 1);

    bool h1_pullback_buy  = (h1_close < h1_ema) && dailyBullish;
    bool h1_pullback_sell = (h1_close > h1_ema) && dailyBearish;

    // === LAYER 3: M5 ENTRY (current chart) ===
    if (h1_pullback_buy && Close[0] > h1_high)
    {
        // Price broke above the H1 pullback high — enter long
        double atr = iATR(Symbol(), PERIOD_M5, EntryATRPeriod, 0);
        double sl = Bid - atr * 1.5;
        double tp = Ask + atr * 3.0;  // 2:1 R:R

        OrderSend(Symbol(), OP_BUY, lots, Ask, 3, sl, tp,
                 "MTF Buy", MagicNumber, 0, clrBlue);
    }

    if (h1_pullback_sell && Close[0] < iLow(Symbol(), PERIOD_H1, 1))
    {
        double atr = iATR(Symbol(), PERIOD_M5, EntryATRPeriod, 0);
        double sl = Ask + atr * 1.5;
        double tp = Bid - atr * 3.0;

        OrderSend(Symbol(), OP_SELL, lots, Bid, 3, sl, tp,
                 "MTF Sell", MagicNumber, 0, clrRed);
    }
}

MT4 Tester Limitation

This bit burns people. When you test on M5, MT4 synthesizes the H1 and D1 bars from those M5 bars internally. "Every Tick" model? Works fine — the higher-timeframe values update correctly. "Open Prices Only"? Now you've got a problem. The D1 EMA might jump to its new value before the daily bar actually closes. We lost two weeks debugging a strategy once because of this exact issue.

Bottom line: If your strategy touches multiple timeframes, use "Every Tick" model. Period. Yes, it's slower. We don't care. Wrong results fast are still wrong.

Python: Multi-Timeframe with Backtrader

This is where we genuinely prefer Python over MQL4. Backtrader's resampling approach is elegant — you feed it M5 data, tell it to create H1 and D1 from that, and it just works. No iMA() magic numbers, no worrying about bar indices across timeframes. The data feeds line up automatically.

import backtrader as bt
import datetime

class MTFStrategy(bt.Strategy):
    """
    Multi-timeframe strategy:
    - D1 EMA crossover for trend direction
    - H1 pullback to EMA for setup
    - M5 breakout for entry
    """
    params = dict(
        d1_fast=50,
        d1_slow=200,
        h1_ema=21,
        m5_atr=14,
        atr_sl_mult=1.5,
        risk_percent=1.0,
    )

    def __init__(self):
        # Data feeds: self.data0 = M5, self.data1 = H1, self.data2 = D1
        self.m5  = self.data0
        self.h1  = self.data1
        self.d1  = self.data2

        # D1 indicators
        self.d1_ema_fast = bt.indicators.EMA(
            self.d1.close, period=self.p.d1_fast)
        self.d1_ema_slow = bt.indicators.EMA(
            self.d1.close, period=self.p.d1_slow)

        # H1 indicators
        self.h1_ema = bt.indicators.EMA(
            self.h1.close, period=self.p.h1_ema)

        # M5 indicators
        self.m5_atr = bt.indicators.ATR(
            self.m5, period=self.p.m5_atr)

    def next(self):
        if self.position:
            return

        # Layer 1: D1 trend
        daily_bull = self.d1_ema_fast[0] > self.d1_ema_slow[0]
        daily_bear = self.d1_ema_fast[0] < self.d1_ema_slow[0]

        # Layer 2: H1 pullback
        h1_pullback_buy = (self.h1.close[-1] < self.h1_ema[-1]
                           and daily_bull)
        h1_pullback_sell = (self.h1.close[-1] > self.h1_ema[-1]
                            and daily_bear)

        # Layer 3: M5 breakout entry
        atr = self.m5_atr[0]
        sl_dist = atr * self.p.atr_sl_mult
        risk = self.broker.getvalue() * self.p.risk_percent / 100
        size = int(risk / sl_dist) if sl_dist > 0 else 0

        if size <= 0:
            return

        if h1_pullback_buy and self.m5.close[0] > self.h1.high[-1]:
            self.buy(size=size)
            self.sell(size=size, exectype=bt.Order.Stop,
                     price=self.m5.close[0] - sl_dist)
            self.sell(size=size, exectype=bt.Order.Limit,
                     price=self.m5.close[0] + sl_dist * 2)

        elif h1_pullback_sell and self.m5.close[0] < self.h1.low[-1]:
            self.sell(size=size)
            self.buy(size=size, exectype=bt.Order.Stop,
                     price=self.m5.close[0] + sl_dist)
            self.buy(size=size, exectype=bt.Order.Limit,
                     price=self.m5.close[0] - sl_dist * 2)

Setting Up Multiple Data Feeds

cerebro = bt.Cerebro()

# Primary data: M5
data_m5 = bt.feeds.GenericCSVData(
    dataname='EURUSD_M5.csv',
    dtformat='%Y.%m.%d', tmformat='%H:%M',
    datetime=0, time=1, open=2, high=3,
    low=4, close=5, volume=6, openinterest=-1,
    timeframe=bt.TimeFrame.Minutes,
    compression=5,
)
cerebro.adddata(data_m5, name='M5')

# Resample to H1
data_h1 = cerebro.resampledata(
    data_m5,
    timeframe=bt.TimeFrame.Minutes,
    compression=60,
    name='H1',
)

# Resample to D1
data_d1 = cerebro.resampledata(
    data_m5,
    timeframe=bt.TimeFrame.Days,
    compression=1,
    name='D1',
)

cerebro.addstrategy(MTFStrategy)
cerebro.broker.setcash(10000)
cerebro.run()
cerebro.plot()

The beautiful part: One CSV file. Feed Backtrader your M5 data and resampledata() builds the H1 and D1 feeds by aggregating those M5 bars. No separate data downloads, no alignment headaches, no timezone nightmares. We switched our entire backtesting pipeline to this approach in 2019 and never looked back.

Common MTF Backtesting Pitfalls

These are the mistakes we see over and over — and we've made every single one of them ourselves at some point.

1. Look-Ahead Bias Across Timeframes

# WRONG: Using current D1 bar's value (it's not complete yet!)
daily_ema = self.d1_ema_fast[0]  # This D1 bar is still forming

# CORRECT: Use the COMPLETED D1 bar
daily_ema = self.d1_ema_fast[-1]  # Previous D1 bar (fully formed)

Think about it. It's 10:30 AM on your M5 chart. The daily bar? Still forming. It won't close for another six and a half hours. So when you grab [0] on D1 data, you're peeking at a value that doesn't exist yet in real trading. The fix is dead simple — use [-1] for higher timeframes. Always.

2. Timeframe Mismatch in Signals

# WRONG: Comparing M5 close to H1 indicator at the same index
if self.m5.close[0] > self.h1_ema[0]:  # H1 EMA might be stale

# CORRECT: The H1 EMA updates once per hour, M5 close updates every 5 minutes
# This is actually fine in Backtrader (H1 value holds until next H1 bar)
# But be aware: the H1 EMA changes only 12 times per day

3. Insufficient Data for Higher Timeframes

This one's embarrassingly common. You slap a 200-period EMA on the daily chart and start testing from January 1st. Guess what? That EMA needs 200 trading days — roughly ten months — before it even has a valid value. Your M5 data needs to start way before your actual test window. We always load at least a full year of warmup data.

# Ensure warmup period
data = bt.feeds.GenericCSVData(
    dataname='EURUSD_M5.csv',
    fromdate=datetime(2019, 1, 1),   # Start early for warmup
    todate=datetime(2024, 1, 1),
)
# The D1 EMA(200) won't be valid until ~Oct 2019
# Actual testing begins Nov 2019

Choosing Your Timeframe Combination

Strategy StyleTrend TFSetup TFEntry TF
Position tradingMonthlyWeeklyDaily
Swing tradingWeeklyDailyH4
Day tradingDailyH1M15
ScalpingH4H1M5
HFT scalpingH1M15M1

Our rule: Each layer should be roughly 4-6× the one below it. D1 → H4 → H1 gives you 6× and 4× ratios — that's the sweet spot. Jumping straight from D1 to M5? That's a 288× gap. You might as well be checking the weather forecast to time your microwave.

Backtest Report: Single vs. Multi-Timeframe

We ran this exact comparison on EURUSD, two years of data. The numbers speak louder than any argument we could make:

MetricM5 OnlyM5 + H1 FilterM5 + H1 + D1
Total Trades1,200480210
Win Rate42%51%58%
Profit Factor1.11.51.9
Max Drawdown-28%-15%-9%
Avg Trade Duration2 hrs5 hrs8 hrs

Look at that progression. 1,200 trades down to 210 — and the profit factor nearly doubled. Why? Because you stopped fighting the daily trend and stopped entering without a real pullback. Your ego takes fewer trades, but your account takes fewer hits. The only price you pay is longer hold times, which most people's psychology handles better anyway.

Every strategy we build starts with the D1 chart. No exceptions. If someone comes to us with an M5 scalper and no higher-TF filter, we tell them to go add one before we talk about anything else. MTF isn't optional — it's the bare minimum for a strategy worth running live. At Quantumcona, we build and stress-test MTF strategies with walk-forward validation, because a backtest that doesn't survive out-of-sample testing is just a bedtime story.

Build My MTF Strategy →

This is Article 14 of 20 in the Algo Trading Masterclass by Quantumcona.

1of8