You've spent hours coding your Expert Advisor. It compiles clean. The logic looks solid. But when you run it in the Strategy Tester, it crawls through a year of data like it's wading through molasses. Or worse, it passes the backtest but starts lagging on live charts once you attach it. The culprit is almost always one or two poorly optimized functions eating up most of the CPU time. The MQL5 profiler in MetaEditor is the tool that shows you exactly where that time goes.
This guide walks you through using the profiler step by step. You'll learn how to run it, read the results, and fix the bottlenecks that matter. I assume you know your way around MetaEditor and have written at least one EA or indicator. If you haven't opened the profiler before, you're about to save hours of guesswork.
Why You Need a Profiler
Guessing which part of your code is slow almost never works. I've seen developers wrap timers around every function, print microseconds to the log, and still miss the real bottleneck. The profiler does this automatically. It instruments your code during execution, records how many times each function runs and how long it takes, and presents the data in a clean table. You don't need to add a single line of timing code.
The MQL5 profiler is built into MetaEditor. It works with Expert Advisors, custom indicators, scripts, and even libraries you include. It's available since build 2000 or so, so any recent MetaTrader 5 installation has it. MT4 users are out of luck here — the profiler is MQL5 only, and there is no equivalent for MQL4.
Prerequisites
- MetaTrader 5 build 2000 or higher (check Help > About in the terminal)
- MetaEditor 5 (comes with MT5)
- An EA, indicator, or script you want to profile — one that compiles without errors
- Some test data: a few months of history on any symbol and timeframe works
You don't need a live trading account. The profiler runs in the Strategy Tester, which works with demo data. If you want to profile an indicator that attaches to a chart, you'll need to convert it into a script or EA for profiling, or run it as part of an EA test. I've done this by wrapping the indicator's OnCalculate logic into a helper function that an EA calls on each tick.
Step 1: Open the Profiler in MetaEditor
Launch MetaEditor from MT5 (F4 or Tools > MetaQuotes Language Editor). Open the project file (.mq5) or the source code file you want to profile. The profiler is not a separate window you toggle on. Instead, you launch it through a special debug profile.
- In MetaEditor, with your .mq5 file active, click the Debug menu.
- Select Profiling from the dropdown. Alternatively, press Ctrl+Shift+P on your keyboard.
- MetaEditor opens the Strategy Tester window inside MetaEditor itself — not the terminal's tester. You'll see the familiar tester settings: symbol, period, date range, model, and inputs.
This is the exact same interface you use for regular backtesting, but with profiling instrumentation active behind the scenes. All your EA's input parameters are available. Set them as you would for a normal test.
Choosing Test Settings for Profiling
The profiler works best with a realistic but not enormous dataset. I usually pick:
- Symbol: EURUSD or whatever your EA targets
- Period: H1 or H4 — enough ticks to exercise all code paths
- Date range: 3 to 6 months. Too short and you might miss infrequent code paths; too long and the profiler run takes forever.
- Model: Every tick if your EA uses tick-level logic; 1 minute OHLC for simpler EAs. The profiler overhead is higher with every-tick mode, but the results are more accurate.
- Forward: Leave unchecked. You only need one pass for profiling.
Click Start. The tester runs normally, but you'll notice it's slower than a regular backtest. That's the instrumentation overhead. Let it finish. When the test completes, MetaEditor automatically opens the profiler report.
Step 2: Read the Profiler Report
The report appears as a table inside MetaEditor, usually docked at the bottom. It looks like this:
| Function | Calls | Time (μs) | % of Total | Average (μs) |
|---|---|---|---|---|
| OnTick | 12,450 | 4,210,000 | 58.2% | 338 |
| CalculateIndicators | 12,450 | 2,890,000 | 39.9% | 232 |
| iRSI | 37,350 | 1,950,000 | 26.9% | 52 |
| CopyBuffer | 37,350 | 1,120,000 | 15.5% | 30 |
| CheckEntryConditions | 12,450 | 180,000 | 2.5% | 14 |
Each row is a function in your code or a system function called by your code. The key columns are:
- Calls: How many times the function executed. High call counts are normal for OnTick or OnCalculate — that's expected.
- Time (μs): Total time spent in this function, in microseconds. This is your primary hunting ground.
- % of Total: The percentage of overall execution time. Focus on functions above 10%.
- Average (μs): Time per call. A high average with moderate calls suggests an expensive operation inside.
In the example above, CalculateIndicators consumes 39.9% of total time. Within it, iRSI and CopyBuffer account for most of that. This tells me the EA is calling iRSI on every tick and then copying its buffer. That's often redundant — you can cache the indicator handle and buffer once, then just update the values.
Step 3: Identify the Bottleneck
Double-click any function name in the profiler report to jump to that line in the source code. MetaEditor highlights the corresponding line. This is invaluable — you can see exactly which line is the slow one.
Common bottlenecks I see again and again:
- CopyBuffer calls inside loops: Calling CopyBuffer for every bar in a loop instead of fetching once outside the loop.
- Indicator handle creation: Creating a new iMA or iRSI handle inside OnTick instead of once in OnInit.
- Math-heavy calculations: Custom indicators that recompute the entire buffer on every tick instead of only the last bar.
- File operations: Writing to disk inside OnTick. This kills performance.
- String concatenation: Building strings with + inside loops. Use StringConcatenate or a string buffer.
How to Spot a False Positive
Sometimes a function shows high time but it's just called very often. For example, OnTick will always be near the top. The trick is to look at the Average column. If OnTick averages 338 μs but CalculateIndicators averages 232 μs, the real work is in the indicator calculation, not the event handler itself. Also, check if a function calls many sub-functions — the profiler shows both the parent and children, so you can drill down.
Step 4: Fix the Bottleneck
Let me walk you through a real fix. Suppose the profiler shows that CopyBuffer inside CalculateIndicators takes 26% of total time. The original code might look like this:
// Slow version — calls CopyBuffer every tick
void CalculateIndicators()
{
for(int i = 0; i < rates_total; i++)
{
double rsi[];
ArraySetAsSeries(rsi, true);
if(CopyBuffer(rsi_handle, 0, i, 1, rsi) < 0)
continue;
// use rsi[0]
}
}
The fix is to copy the entire buffer once per tick, then index into it:
// Fast version — copy once, index many times
double rsi_buffer[];
bool rsi_copied = false;
void CalculateIndicators()
{
if(!rsi_copied)
{
ArraySetAsSeries(rsi_buffer, true);
if(CopyBuffer(rsi_handle, 0, 0, rates_total, rsi_buffer) > 0)
rsi_copied = true;
}
for(int i = 0; i < rates_total; i++)
{
double rsi_value = rsi_buffer[i];
// use rsi_value
}
}
After this change, run the profiler again. You should see CopyBuffer drop to under 5% of total time. The CalculateIndicators row will also shrink significantly.
Another Common Fix: Indicator Handle Creation
If the profiler shows iMA or iRSI being called thousands of times, you're creating the handle inside OnTick. Move it to OnInit:
int ma_handle;
int OnInit()
{
ma_handle = iMA(_Symbol, _Period, 14, 0, MODE_SMA, PRICE_CLOSE);
if(ma_handle == INVALID_HANDLE) return INIT_FAILED;
return INIT_SUCCEEDED;
}
void OnTick()
{
// Use ma_handle, never call iMA again
}
Fixing String Concatenation Bottlenecks
String operations are surprisingly expensive in MQL5. If you're building log messages or trade comments with + inside loops, the profiler will show high time in hidden string functions. Here's a before and after:
// Slow version
string comment = "";
for(int i = 0; i < 100; i





