Custom Debugging and Profiling Tools for MQL5 Development (Part III): Regression Gates for Performance and Trading Rules
Contents are listed below.
- Introduction
- What carries over from Part II
- Organizing the code base and report flow
- Building the regression gate
- Adding MQL5-specific assertions without rewriting TestLite
- The gate runner script
- Running the workflow and interpreting results
- Conclusion
Introduction
Part II produced two useful artifacts: a profiler CSV and a deterministic unit-test report. That already moves the project beyond a quiet Experts tab, but a single report still describes only one run. Part III adds the missing decision layer while preserving the Part II contracts: the profiler writes the same CSV, the unit-test runner writes the same TestLite report, and the trading math helper remains the owner of the pure functions. The regression gate compares the current evidence with an accepted baseline and turns that comparison into a decision.
The practical problem is familiar: an Expert Advisor can compile and the Strategy Tester can finish, while a regression remains hidden behind a harmless-looking log. Typical examples are listed below.
- A buffer-copy path becomes more expensive after an indicator change.
- A section that used to run on new bars starts running on every tick.
- A lot-size rule passes on EURUSD but fails on a symbol with a different volume step.
- A signal accidentally reads the current forming bar after a refactor.
The gate answers three practical questions.
- Did the selected profiled sections move enough to deserve PASS, WARN, SKIP, or FAIL?
- Do the pure trading-math tests from Part II continue to pass?
- Do the symbol-specific assumptions used by the example EA remain valid?
This is not a replacement for MetaEditor profiling. MetaEditor already has a built-in profiler that can collect execution statistics for functions and code lines, and it can run on a chart or with historical data in the Strategy Tester. That tool remains the right choice for broad discovery. The embedded gate solves the next maintenance problem: after the relevant sections are known, did those sections materially regress compared with a trusted baseline? The output is intentionally plain: status, delta, and failure files, plus trade-assertion and symbol-assumption reports. The workflow stays simple: run unit tests, run the profiled EA, promote a clean profile to a baseline, and compare the current run against it before that run becomes the new reference point.
What carries over from Part II
Part III treats the existing files as contracts, not as drafts to rewrite. Part II already provides the profiler, test helper, pure trading math module, deterministic test script, and non-trading EA that writes profiler evidence. RegressionGate.mqh and TradeAssertions.mqh build around those roles.
The carried-forward contracts are listed below.
- Profiler CSV: CPerfMeter::WriteCsvReport() writes the stable fields section, calls, total_us, min_us, max_us, avg_us, slow_calls, threshold_us, and status. The gate reads that shape without asking the profiler to expose private arrays or grow new methods.
- Test report: CTestLite::WriteReport() writes TradeMathCore_TestReport.txt with a key-value status line. The gate reads the top-level status instead of re-parsing every old failure row.
- Trading math: CTradeMathCore remains the owner of pip/point conversion, volume normalization, stop-distance validation, moving average crossover classification, and signal-to-string conversion.
- Profiled EA: ProfilerExampleEA.mq5 creates moving average handles, copies two closed bars, classifies the crossover through CTradeMathCore, and writes ProfilerExampleEA_Profile.csv.
The following excerpt anchors the EA identity that remains in place: the includes, the CPerfMeter instance, the CTradeMathCore signal enumeration, and the original report file name. The gate uses these names as part of the contract.
#property strict #include <Debugging_Profiling_Part3/PerfMeter.mqh> #include <Debugging_Profiling_Part3/TradeMathCore.mqh> input bool EnableProfiling = true; input int ProfileEveryNTicks = 10; input int SlowCallThresholdMicroseconds = 250; input int FastMAPeriod = 10; input int SlowMAPeriod = 30; CPerfMeter g_profiler; int g_fast_ma_handle = INVALID_HANDLE; int g_slow_ma_handle = INVALID_HANDLE; ulong g_ticks_seen = 0; TradeMathSignal g_last_signal = TM_SIGNAL_NONE; string g_report_name = "ProfilerExampleEA_Profile.csv";
The initialization block keeps indicator creation visible as a named profiler section. That makes startup cost part of the same evidence trail as the later tick sections.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { if(EnableProfiling) { //--- Measure initialization as two nested sections: total and handle creation. g_profiler.Reset(); g_profiler.SetSlowCallThreshold(SlowCallThresholdMicroseconds); g_profiler.Start("OnInit.total"); g_profiler.Start("OnInit.create_indicators"); } g_fast_ma_handle = iMA(_Symbol, _Period, FastMAPeriod, 0, MODE_EMA, PRICE_CLOSE); g_slow_ma_handle = iMA(_Symbol, _Period, SlowMAPeriod, 0, MODE_EMA, PRICE_CLOSE); if(EnableProfiling) { g_profiler.Stop("OnInit.create_indicators"); g_profiler.Stop("OnInit.total"); } if(g_fast_ma_handle == INVALID_HANDLE || g_slow_ma_handle == INVALID_HANDLE) { PrintFormat("ProfilerExampleEA: failed to create MA handles, error=%d", GetLastError()); return INIT_FAILED; } PrintFormat("ProfilerExampleEA initialized on %s %s, fast=%d, slow=%d, profiling=%s", _Symbol, EnumToString(_Period), FastMAPeriod, SlowMAPeriod, (EnableProfiling ? "true" : "false")); return INIT_SUCCEEDED; }
The report-writing path is unchanged. ProfilerExampleEA writes through the Part II CPerfMeter instance and stores the CSV in FILE_COMMON, while UnitTestRunner.mq5 writes TradeMathCore_TestReport.txt in the normal MQL5 Files folder. The runner therefore has separate inputs for profile report location, unit-test report location, and gate output location.
//+------------------------------------------------------------------+ //| Write profiler CSV report | //+------------------------------------------------------------------+ void WriteProfilerReport(const string origin) { if(!EnableProfiling) return; bool report_written = g_profiler.WriteCsvReport(g_report_name, true); if(report_written) PrintFormat("ProfilerExampleEA: %s profile report saved: %s in FILE_COMMON. %s", origin, g_report_name, g_profiler.Summary()); else PrintFormat("ProfilerExampleEA: %s profile report could not be written: %s. %s", origin, g_report_name, g_profiler.Summary()); }
PerfMeter.mqh and TestLite.mqh remain in their original roles. The attachment includes the Part II support files because the Part III workflow directly runs them, includes them, or consumes their reports; the files now live under the project subfolders required for a clean package. The Part I logger is not included unless the actual logger files are present in the target project. The new runner keeps one small LogDiagnosticGateResult() function that uses PrintFormat() in the attached package and can be replaced by the Part I logger call in a real project.
Organizing the code base and report flow
The attachment mirrors the MetaTrader 5 data-folder layout. After extraction into the terminal data folder, all source files are grouped under the Debugging_Profiling_Part3 project folder inside MQL5\Include, MQL5\Scripts, and MQL5\Experts. That structure prevents include-path confusion, avoids cluttering the standard folders, and keeps runnable files where MetaEditor and the Navigator expect them.
The package has two file categories.
- Source files: these include the shared includes, scripts, and EA required for compilation and execution.
- Generated evidence: these are reports produced locally after running the script or EA. The package discusses these reports but does not ship stale output as source material.
Keeping source files separate from generated reports avoids stale sample output in the attachment and makes the archive easier to inspect.
After extraction, the important folders are easy to inspect.
- MQL5\Include\Debugging_Profiling_Part3 contains shared helpers and include files.
- MQL5\Experts\Debugging_Profiling_Part3 contains the profiled EA used in the Strategy Tester.
- MQL5\Scripts\Debugging_Profiling_Part3 contains the unit-test runner, baseline promotion script, and diagnostic gate runner.
The report flow is intentionally sequential.
- Run UnitTestRunner.mq5 to write TradeMathCore_TestReport.txt.
- Run ProfilerExampleEA.mq5 in the Strategy Tester to write ProfilerExampleEA_Profile.csv in FILE_COMMON.
- When the profile belongs to an accepted version, run PromoteProfilerBaseline.mq5 to create ProfilerExampleEA_Baseline.csv.
- After a code change, run the profiled EA again and then run DiagnosticGateRunner.mq5.
That order matters. A baseline is a report from a version the developer is willing to treat as acceptable; if it came from a broken run, the gate only preserves that broken behavior. The promotion script stays small because its job is small: copy the current profile report to the baseline file using the platform file functions, with defaults that match the existing Part II EA report name.
#property strict #property script_show_inputs input bool UseCommonProfileFiles = true; input string CurrentProfileFile = "ProfilerExampleEA_Profile.csv"; input string BaselineProfileFile = "ProfilerExampleEA_Baseline.csv"; //+------------------------------------------------------------------+ //| Copy the latest profiler report to the accepted baseline file | //+------------------------------------------------------------------+ bool PromoteProfilerBaseline(const string current_file, const string baseline_file, const bool common_file) { int source_location = (common_file ? FILE_COMMON : 0); int target_mode = FILE_REWRITE; if(common_file) target_mode |= FILE_COMMON; //--- FileCopy keeps promotion explicit: current profile becomes accepted baseline. ResetLastError(); bool copied = FileCopy(current_file, source_location, baseline_file, target_mode); if(!copied) { PrintFormat("PromoteProfilerBaseline: cannot copy '%s' to '%s', error=%d", current_file, baseline_file, GetLastError()); return false; } PrintFormat("PromoteProfilerBaseline: baseline updated from '%s' to '%s', common=%s", current_file, baseline_file, (common_file ? "true" : "false")); return true; }
The output folder policy is explicit.
- Profile reports default to FILE_COMMON because the profiled EA already writes there, and Strategy Tester output is easier to retrieve from the common folder.
- The Part II unit-test report defaults to the normal files folder because UnitTestRunner.mq5 writes it there.
- Gate reports default to FILE_COMMON so the final status, deltas, and failures are easy to find after the runner executes.
Generated reports:
| Report | Produced by | Purpose |
|---|---|---|
| TradeMathCore_TestReport.txt | UnitTestRunner.mq5 | Part II deterministic test status and failure rows. |
| ProfilerExampleEA_Profile.csv | ProfilerExampleEA.mq5 | Current profiler evidence from the existing profiled EA. |
| ProfilerExampleEA_Baseline.csv | PromoteProfilerBaseline.mq5 | Accepted profiler evidence used as the comparison baseline. |
| DiagnosticGate_Deltas.csv | DiagnosticGateRunner.mq5 | All matched, new, missing, skipped, warning, and failing profiler rows. |
| DiagnosticGate_Failures.csv | DiagnosticGateRunner.mq5 | Only rows that need attention: WARN, FAIL, or SKIP. |
| TradeAssertions_Report.txt | DiagnosticGateRunner.mq5 | Symbol-aware assertion report using the Part II TestLite format. |
| SymbolAssumptions.txt | DiagnosticGateRunner.mq5 | Symbol properties used by the assertion runner. |
| DiagnosticGate_Status.txt | DiagnosticGateRunner.mq5 | Final machine-readable PASS, WARN, or FAIL decision. |
With this split, each component has one job: Part II files produce evidence, and Part III files decide whether the current evidence remains acceptable.
Building the regression gate
RegressionGate.mqh is the central new include file. It does not time code; that remains the profiler's responsibility. It loads two profiler reports, matches rows by section name, calculates the difference, classifies the movement, and writes a status that can be consumed by a human or a script.
The gate design has three practical rules.
- Section names are keys. Names such as OnTick.copy_buffers and OnTick.signal_calculation are useful because they identify the work being measured. A name such as Work is too vague, and a name that changes every version breaks comparison history.
- Structures stay plain. SProfilerCsvRow mirrors the Part II CSV, SProfileDeltaRow stores comparison output, and SRegressionGateConfig stores threshold policy.
- Timing fields stay visible. The fields avg_us, max_us, baseline_calls, and current_calls remain in the delta report so a scheduling change is not mistaken for a slower function.
The delta report is intentionally more detailed than a one-word result. A developer reviewing a WARN or FAIL row should read the columns together.
- Call counts: baseline_calls and current_calls show whether the code path ran a comparable number of times.
- Average time: baseline_avg_us and current_avg_us show whether ordinary work became more expensive.
- Maximum time: baseline_max_us and current_max_us show whether the worst observed call changed.
- Relative movement: avg_delta_pct and max_delta_pct show movement relative to the accepted baseline.
- Reason: the reason column explains why the row was marked WARN, FAIL, or SKIP.
#property strict //+------------------------------------------------------------------+ //| RegressionGate.mqh | //| Compares Part II profiler reports and writes gate outputs. | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Structure: SProfilerCsvRow | //| One row loaded from the Part II CPerfMeter CSV report. | //+------------------------------------------------------------------+ struct SProfilerCsvRow { string section; long calls; double total_us; double min_us; double max_us; double avg_us; long slow_calls; long threshold_us; string status; bool matched; }; //+------------------------------------------------------------------+ //| Structure: SProfileDeltaRow | //| One comparison row written by the regression gate. | //+------------------------------------------------------------------+ struct SProfileDeltaRow { string section; long baseline_calls; long current_calls; double baseline_avg_us; double current_avg_us; double avg_delta_us; double avg_delta_pct; double baseline_max_us; double current_max_us; double max_delta_us; double max_delta_pct; string status; string reason; }; //+------------------------------------------------------------------+ //| Structure: SRegressionGateConfig | //| Thresholds used while comparing profiler reports. | //+------------------------------------------------------------------+ struct SRegressionGateConfig { double warn_avg_increase_pct; double fail_avg_increase_pct; double warn_max_increase_pct; double fail_max_increase_pct; double warn_abs_avg_us; double fail_abs_avg_us; double warn_abs_max_us; double fail_abs_max_us; double min_comparable_avg_us; long min_calls; bool warn_on_new_sections; bool warn_on_missing_sections; };
The loader reads the exact CSV shape written by CPerfMeter. It skips the header, then reads nine fields per row. The gate is coupled to the report contract, not to the profiler internals, so the profiler can change its private storage as long as the CSV contract remains stable.
//--- Store the row exactly in the shape used by the delta builder. int size = ArraySize(rows); ArrayResize(rows, size + 1); rows[size].section = section; rows[size].calls = (long)StringToInteger(calls_text); rows[size].total_us = StringToDouble(total_text); rows[size].min_us = StringToDouble(min_text); rows[size].max_us = StringToDouble(max_text); rows[size].avg_us = StringToDouble(avg_text); rows[size].slow_calls = (long)StringToInteger(slow_calls_text); rows[size].threshold_us = (long)StringToInteger(threshold_text); rows[size].status = status_text; rows[size].matched = false;
The classification rule uses both relative and absolute thresholds because microsecond measurements are noisy around tiny values. A change from 0.02 microseconds to 0.04 microseconds is a 100 percent increase, but it is not a useful reason to reject a build. When a baseline value is zero or rounded to zero in the CSV, the gate treats a positive current value as a large percentage movement, while the absolute threshold still decides whether it matters.
The default policy uses these thresholds.
- warn when average time rises by at least 20 percent and at least 5 microseconds;
- fail when average time rises by at least 35 percent and at least 10 microseconds;
- use separate maximum-time thresholds because occasional spikes should not be judged like ordinary work;
- mark rows as SKIP when they have too few calls or too small an average time for a fair comparison.
The absolute thresholds are as important as the percentages. Without them, tiny values can create dramatic percentages that are not meaningful in practice. Without the percentages, a large absolute movement on an already slow section can look less serious than it is. Combining both conditions makes the gate stricter where it matters and quieter where the measurement is below the useful noise floor.
if(row.avg_delta_pct >= m_config.fail_avg_increase_pct && row.avg_delta_us >= m_config.fail_abs_avg_us) { //--- Average time is the main signal for routine section cost. fail = true; AddReason(reason, "avg_us_failed"); } else if(row.avg_delta_pct >= m_config.warn_avg_increase_pct && row.avg_delta_us >= m_config.warn_abs_avg_us) { warn = true; AddReason(reason, "avg_us_warned"); } if(row.max_delta_pct >= m_config.fail_max_increase_pct && row.max_delta_us >= m_config.fail_abs_max_us) { //--- Maximum time catches spikes that average time can hide. fail = true; AddReason(reason, "max_us_failed"); } else if(row.max_delta_pct >= m_config.warn_max_increase_pct && row.max_delta_us >= m_config.warn_abs_max_us) { warn = true; AddReason(reason, "max_us_warned"); } if(fail) row.status = "FAIL"; else if(warn) row.status = "WARN"; else row.status = "PASS"; if(reason != "") row.reason = reason;
New and missing sections are warnings by default because they change the measurement surface. A new section might be a deliberate risk filter, or it might hide work that used to be inside a measured block. A missing section might be a cleanup, or it might mean a section name changed and the historical comparison was lost. The warning policy keeps those changes visible without blocking every refactor by default.
The final status is the highest severity among the performance comparison, the Part II unit-test status, and the Part III trade-assertion status. A unit-test failure fails the gate even when the profiler comparison looks clean, because optimizing incorrect logic is wasted effort. The gate remains file-based and stays inside the ordinary MetaTrader environment; it needs no DLL, database, or separate desktop runner.
Adding MQL5-specific assertions without rewriting TestLite
Generic assertions are useful, but a trading project also needs symbol-aware checks. TradeAssertions.mqh includes TestLite.mqh and TradeMathCore.mqh, then exposes static helper methods that receive a CTestLite reference. The same report format, status line, and assertion counters are reused; the new include only adds MQL5-aware questions.
The assertion groups cover the following checks.
- Symbol volume settings: SYMBOL_VOLUME_MIN, SYMBOL_VOLUME_MAX, and SYMBOL_VOLUME_STEP must allow the requested example volume.
- Shared normalization: CTradeMathCore::NormalizeVolume() must agree with the current symbol constraints.
- Stop distance: SYMBOL_POINT and SYMBOL_TRADE_STOPS_LEVEL are passed to CTradeMathCore::IsStopDistanceValid().
- Closed-bar access: signal code should copy from closed bars rather than the current forming bar.
These checks stay close to the real risks in the example EA. They do not try to model every broker rule, order type, or execution condition. Instead, they test the assumptions that are easy to break during development and expensive to discover late: volume alignment, stop distance, and the difference between a closed bar and a forming bar.
//+------------------------------------------------------------------+ //| Assert that a requested volume is aligned with symbol settings | //+------------------------------------------------------------------+ static bool AssertValidVolumeStep(CTestLite &test, const string symbol, const double requested_volume, const string message = "requested volume must align with broker limits") { double min_volume; double max_volume; double volume_step; string details; if(!LoadVolumeSettings(symbol, min_volume, max_volume, volume_step, details)) return test.AssertTrue(false, message + ": invalid symbol volume settings: " + details); //--- Reuse the production normalization helper instead of duplicating volume logic. double normalized = CTradeMathCore::NormalizeVolume(requested_volume, min_volume, max_volume, volume_step); bool in_range = (requested_volume + 0.0000001 >= min_volume && requested_volume <= max_volume + 0.0000001); bool aligned = (MathAbs(normalized - requested_volume) <= MathMax(volume_step * 0.0001, 0.00000001)); string actual = StringFormat("requested=%.8f normalized=%.8f %s", requested_volume, normalized, details); return test.AssertTrue(in_range && aligned, message + ": " + actual); }
AssertValidVolumeStep() does not invent a second normalization algorithm. It calls CTradeMathCore::NormalizeVolume(), then checks whether the normalized value equals the requested value within a small tolerance. If a future change breaks the existing normalization function, this assertion can surface the problem while still using the same report system from Part II. Stop-distance validation follows the same boundary: the runner reads SYMBOL_POINT and SYMBOL_TRADE_STOPS_LEVEL, then passes the point size and stops level to CTradeMathCore::IsStopDistanceValid().
The closed-bar assertions protect a different kind of error. The Part II EA copies indicator buffers from shift 1 for two bars. According to the MQL5 CopyBuffer contract, start position 0 means the current bar. A strategy that claims to work on closed bars should therefore avoid shift 0 for signal decisions. The assertion cannot inspect every future CopyBuffer call automatically; it records the intended contract where the runner calls it. That value comes from making the assumption explicit.
//+------------------------------------------------------------------+ //| Assert that CopyBuffer is not reading the forming bar | //+------------------------------------------------------------------+ static bool AssertNoCurrentBarLookahead(CTestLite &test, const int copy_start_pos, const string message = "closed-bar logic must not copy from bar zero") { //--- CopyBuffer start position 0 is the forming bar; closed-bar logic starts at 1. bool ok = (copy_start_pos >= 1); string details = StringFormat("copy_start_pos=%d", copy_start_pos); return test.AssertTrue(ok, message + ": " + details); } //+------------------------------------------------------------------+ //| Assert that enough closed bars are copied for a crossover rule | //+------------------------------------------------------------------+ static bool AssertClosedBarWindow(CTestLite &test, const int copy_start_pos, const int copy_count, const string message = "closed-bar crossover logic needs two closed bars") { bool ok = (copy_start_pos >= 1 && copy_count >= 2); string details = StringFormat("copy_start_pos=%d copy_count=%d", copy_start_pos, copy_count); return test.AssertTrue(ok, message + ": " + details); }
Do not overload these assertions. They check the assumptions that the example EA and diagnostic workflow depend on: usable symbol volume settings, valid example volumes, stop distance that respects the symbol rule, and closed-bar buffer access. A real EA can add helpers later for spread filters, freeze levels, margin mode, session rules, and allowed order types.
The generated assertion evidence has two layers.
- TradeAssertions_Report.txt uses TestLite's key-value status line, giving the gate a uniform way to reason about deterministic and symbol-aware reports.
- SymbolAssumptions.txt records the symbol, point size, digits, volume limits, SYMBOL_TRADE_STOPS_LEVEL value, and freeze level observed by the runner.
That second file matters because a failed assertion is not always a code defect. It may reveal that the script was run on a symbol with different contract settings than expected. The assumption report gives the first facts to inspect before changing a helper function or weakening a test.
The gate runner script
DiagnosticGateRunner.mq5 pulls the pieces together. It does not trade, optimize, or run the Strategy Tester; it reads files produced by earlier steps and writes a final diagnostic decision. The inputs expose file locations and thresholds while keeping the Part II names: ProfilerExampleEA_Profile.csv, ProfilerExampleEA_Baseline.csv, and TradeMathCore_TestReport.txt.
#property strict #property script_show_inputs #include <Debugging_Profiling_Part3/RegressionGate.mqh> #include <Debugging_Profiling_Part3/TradeAssertions.mqh> input bool UseCommonProfileFiles = true; input bool UseCommonUnitTestReport = false; input bool UseCommonGateReports = true; input string BaselineProfileFile = "ProfilerExampleEA_Baseline.csv"; input string CurrentProfileFile = "ProfilerExampleEA_Profile.csv"; input string UnitTestReportFile = "TradeMathCore_TestReport.txt"; input bool RequireUnitTestReport = true; input string DeltaReportFile = "DiagnosticGate_Deltas.csv"; input string FailureReportFile = "DiagnosticGate_Failures.csv"; input string StatusReportFile = "DiagnosticGate_Status.txt"; input string TradeAssertionReportFile = "TradeAssertions_Report.txt"; input string SymbolAssumptionReportFile = "SymbolAssumptions.txt"; input double WarnAvgIncreasePct = 20.0; input double FailAvgIncreasePct = 35.0; input double WarnMaxIncreasePct = 25.0; input double FailMaxIncreasePct = 50.0; input double WarnAbsAvgMicroseconds = 5.0; input double FailAbsAvgMicroseconds = 10.0; input double WarnAbsMaxMicroseconds = 25.0; input double FailAbsMaxMicroseconds = 50.0; input double MinComparableAvgMicroseconds = 2.0; input int MinComparableCalls = 20; input bool WarnOnNewProfilerSections = true; input bool WarnOnMissingProfilerSections = true;
The runner uses three folder switches.
- UseCommonProfileFiles defaults to true because the profiled EA writes its report through FILE_COMMON.
- UseCommonUnitTestReport defaults to false because UnitTestRunner.mq5 writes TradeMathCore_TestReport.txt in the normal files folder.
- UseCommonGateReports defaults to true because the final status and delta files are easier to find when they are shared.
The script flow is direct.
- Configure gate thresholds.
- Compare baseline and current profile reports.
- Write delta and failure CSV files.
- Read the Part II unit-test status.
- Run trade assertions and write their reports.
- Write the final status file and print one stable summary line.
This order is useful during failures because every stage leaves evidence. When the profiler baseline is missing, the status file reports that directly. A missing unit-test report remains visible too. Failed trade assertions produce their report and the symbol-assumption file. The result is a single run that reports the diagnostic state instead of stopping at the first missing file.
CRegressionGate gate; gate.Configure(WarnAvgIncreasePct, FailAvgIncreasePct, WarnMaxIncreasePct, FailMaxIncreasePct, WarnAbsAvgMicroseconds, FailAbsAvgMicroseconds, WarnAbsMaxMicroseconds, FailAbsMaxMicroseconds, MinComparableAvgMicroseconds, MinComparableCalls); gate.SetNewSectionPolicy(WarnOnNewProfilerSections, WarnOnMissingProfilerSections); bool compared = gate.CompareProfilerReports(BaselineProfileFile, CurrentProfileFile, UseCommonProfileFiles); if(!compared) PrintFormat("DiagnosticGateRunner: profiler comparison problem: %s", gate.LastError()); //--- Write comparison files even when the comparison status is not clean. gate.WriteDeltaCsv(DeltaReportFile, UseCommonGateReports); gate.WriteFailuresCsv(FailureReportFile, UseCommonGateReports); string unit_test_status = ReadUnitTestStatus(UnitTestReportFile, UseCommonUnitTestReport, RequireUnitTestReport); gate.SetUnitTestStatus(unit_test_status); CTestLite trade_assertions("MQL5 symbol and closed-bar assertions"); RunTradeAssertions(trade_assertions, _Symbol); trade_assertions.WriteReport(TradeAssertionReportFile, UseCommonGateReports); //--- Keep symbol properties beside the assertion report for faster triage. CTradeAssertions::WriteSymbolAssumptionReport(_Symbol, SymbolAssumptionReportFile, UseCommonGateReports); gate.SetTradeAssertionStatus(trade_assertions.Status()); gate.WriteStatusFile(StatusReportFile, UseCommonGateReports, BaselineProfileFile, CurrentProfileFile, DeltaReportFile, FailureReportFile, TradeAssertionReportFile, UnitTestReportFile); LogDiagnosticGateResult(gate.OverallStatus(), gate.EffectiveFailureCount(), gate.EffectiveWarningCount());
The logger connection is intentionally small. If the Part I logger exists in a project, replace the PrintFormat() call inside LogDiagnosticGateResult() with the real logger's Info() call; otherwise, the script remains standalone. The runner also avoids hard early exits after profiler comparison failure. It writes available reports, runs trade assertions, and records the full diagnostic state rather than only the first problem encountered.
The final status file is deliberately key-value text. A script can read overall_status=FAIL without understanding the delta CSV, and a developer can see whether the problem came from performance, unit tests, or symbol assertions.
For manual review, open files in this order.
- DiagnosticGate_Status.txt for the final PASS, WARN, or FAIL decision.
- DiagnosticGate_Failures.csv when performance_status is WARN or FAIL.
- TradeMathCore_TestReport.txt when unit_test_status is not PASS.
- TradeAssertions_Report.txt and SymbolAssumptions.txt when trade_assert_status is not PASS.
- DiagnosticGate_Deltas.csv when the short failure report is not enough to explain the movement.
Status fields:
| Field | Meaning |
|---|---|
| overall_status | Final PASS, WARN, or FAIL decision composed from all diagnostic sources. |
| performance_status | Result of comparing current profiler rows against the accepted baseline. |
| unit_test_status | Status read from the Part II TradeMathCore_TestReport.txt file. |
| trade_assert_status | Status from the Part III symbol-aware TestLite report. |
| profile_failures | Number of profiler rows classified as FAIL. |
| profile_warnings | Number of profiler rows classified as WARN. |
| profile_skipped | Rows skipped because they were not comparable. |
| delta_rows | Total number of profiler comparison rows written to DiagnosticGate_Deltas.csv. |
Use the status file as a triage map.
- overall_status=FAIL: do not move forward with performance work until the failure source is clear.
- unit_test_status=FAIL: open TradeMathCore_TestReport.txt first.
- trade_assert_status=FAIL: open TradeAssertions_Report.txt and SymbolAssumptions.txt.
- performance_status=FAIL: open DiagnosticGate_Failures.csv before the full delta report.
Running the workflow and interpreting results
The run sequence is short and works best when followed consistently. Compile the runnable files first: UnitTestRunner.mq5, ProfilerExampleEA.mq5, PromoteProfilerBaseline.mq5, and DiagnosticGateRunner.mq5. Includes do not run by themselves, but they must compile when included by the scripts and EA.
Then run the workflow in this order.
- Run UnitTestRunner.mq5 from the Scripts folder. It checks the pure rules in TradeMathCore.mqh and writes TradeMathCore_TestReport.txt.
- If the unit-test report fails, stop there and fix the trading math first. A performance comparison of incorrect logic is noise.
- Run ProfilerExampleEA.mq5 in the Strategy Tester with the same symbol, timeframe, date range, model, and inputs used for the accepted baseline.
- When the profile belongs to a version you accept, run PromoteProfilerBaseline.mq5 to create ProfilerExampleEA_Baseline.csv from ProfilerExampleEA_Profile.csv.
- After a code change, run the profiled EA again so ProfilerExampleEA_Profile.csv represents the current version.
- Run DiagnosticGateRunner.mq5 and open DiagnosticGate_Status.txt first.
The built-in profiler remains useful for discovering unknown bottlenecks, while this embedded profiler measures named sections selected for regression checks. The comparison is only meaningful when the test environment is controlled, and a profile should be promoted only when the version is a real baseline.
The sample evidence below used a controlled setup. Keeping these details stable makes the baseline and current profile comparable.
- Symbol and period: EURUSD, M15.
- Strategy Tester range: 2026.05.20 to 2026.05.22.
- Model: 1-minute OHLC.
- Profiler inputs: ProfileEveryNTicks=10 and SlowCallThresholdMicroseconds=250.
Sample diagnostic results:
| Check | Result | Interpretation |
|---|---|---|
| TradeMathCore_TestReport.txt | 21 passed, 0 failed, status=PASS. | The pure trading math checks passed before the profiler comparison was accepted. |
| Profiler baseline promotion | ProfilerExampleEA_Baseline.csv created from the accepted profile. | The first controlled Strategy Tester run became the reference for the next comparison. |
| TradeAssertions_Report.txt | 5 passed, 0 failed, status=PASS. | The selected symbol exposed usable volume, stop-distance, and closed-bar assumptions. |
| DiagnosticGate_Status.txt | overall_status=PASS. | Performance, unit-test, and trade-assertion evidence all cleared the gate. |
The status file should read like a checklist rather than a log dump. In the sample run, the important fields are grouped by role.
- Final decision: overall_status=PASS.
- Source decisions: performance_status=PASS; unit_test_status=PASS; trade_assert_status=PASS.
- Profiler counts: profile_failures=0; profile_warnings=0; profile_skipped=8; delta_rows=8.
- Effective counts: effective_failures=0; effective_warnings=0.
The delta report also includes skipped rows. They show sections that were present but not comparable under the configured call-count and noise-floor rules.
Sample profiler delta rows:
| Section | Baseline avg | Current avg | Baseline max | Current max | Gate status |
|---|---|---|---|---|---|
| OnTick.total_sampled | 2.78 microseconds | 0.74 microseconds | 2217 microseconds | 24 microseconds | PASS |
| OnTick.copy_buffers | 0.26 microseconds | 0.26 microseconds | 4 microseconds | 5 microseconds | SKIP |
| OnTick.signal_calculation | 1.98 microseconds | 0.05 microseconds | 2215 microseconds | 1 microsecond | SKIP |
| OnTick.decision_block | 0.05 microseconds | 0.08 microseconds | 1 microsecond | 23 microseconds | SKIP |
This sample shows two expected outcomes: the main sampled tick path is comparable and passes, while smaller sections are skipped because their average time is below the configured noise floor. The gate should reject meaningful drift, but it should not treat a fraction of a microsecond as a reliable performance signal.
Read the final status narrowly.
- PASS means the selected diagnostic evidence did not violate the selected thresholds.
- WARN means the evidence changed enough to deserve review, but not enough to reject automatically.
- FAIL means at least one blocking condition exists: a profiler row failed the threshold, the Part II unit tests failed or were missing when required, or the Part III trading assertions failed.
A PASS does not mean the strategy is profitable. It only means this diagnostic gate did not reject the implementation evidence it checked. SKIP also needs careful interpretation: it is not good or bad by itself, only a sign that the row had too few calls or too small an average time for a fair comparison.
When a profiler row fails, read the reason field before changing code; each reason points to a different kind of fix.
- Average-time failure: avg_us_failed means typical work became more expensive.
- Maximum-time failure: max_us_failed means the worst observed call grew.
- New section: new_section_no_baseline means a new measured section exists but has no accepted history.
- Missing section: baseline_section_missing_in_current means a section disappeared.
The main source of false confidence is context drift, not the comparison code. A clean PASS is only useful when the profile was produced under the same kind of market, tester, and input conditions as the baseline. Before changing code or promoting a new baseline, check these points first.
- Environment: Confirm symbol, period, date range, model, inputs, and terminal load before treating the result as meaningful.
- Correctness: Fix unit-test or trade-assertion failures before investigating profiler movement; fast incorrect logic is still incorrect.
- Reason field: Inspect avg_us_failed, max_us_failed, new_section_no_baseline, or baseline_section_missing_in_current before choosing a fix.
- Measured surface: Profile enough named sections. If the only section is OnTick.total_sampled, the gate may show that the tick path became slower without showing why.
- Symbol assumptions: Read SymbolAssumptions.txt before weakening a rule. A failed assertion may reveal a symbol the strategy was never designed to support.
- Keep baseline notes outside the CSV. Record the symbol, timeframe, modeling mode, date range, relevant inputs, sampling interval, terminal build if it matters, and slow-call threshold.
- Change thresholds slowly. Raise a limit only when the team understands why the old value is no longer suitable; use section-specific rules instead of weakening the whole gate.
- Promote baselines deliberately. A profile promoted after a signal-refactor test should not quietly become the reference for a later multi-symbol scanner unless that is intentional.
Do not overload the gate with profitability questions. It protects implementation assumptions and selected performance behavior; profitability, robustness, parameter sensitivity, and execution quality need their own testing stages. The strongest use is modest and consistent: keep one accepted baseline per scenario, keep section names stable, run the same checks after meaningful changes, and explain WARN or FAIL rows before promoting a new baseline.
Final file overview
The attachment contains only files used by the Part III workflow. Files first introduced in Part II remain in the package only where Part III directly runs them, includes them, or consumes their generated reports.
| File | Brief purpose in Part III |
|---|---|
| MQL5\Include\Debugging_Profiling_Part3\PerfMeter.mqh | profiler helper used by ProfilerExampleEA.mq5 to create the CSV compared by the gate. |
| MQL5\Include\Debugging_Profiling_Part3\TestLite.mqh | assertion/report helper used by UnitTestRunner.mq5 and TradeAssertions.mqh. |
| MQL5\Include\Debugging_Profiling_Part3\TradeMathCore.mqh | shared trading calculations used by the unit tests, EA, and symbol-aware assertions. |
| MQL5\Include\Debugging_Profiling_Part3\RegressionGate.mqh | compares baseline and current profiler CSV files and writes gate status data. |
| MQL5\Include\Debugging_Profiling_Part3\TradeAssertions.mqh | adds symbol volume, stop-distance, and closed-bar assertions for the gate runner. |
| MQL5\Experts\Debugging_Profiling_Part3\ProfilerExampleEA.mq5 | Strategy Tester EA that produces ProfilerExampleEA_Profile.csv for baseline/current comparison. |
| MQL5\Scripts\Debugging_Profiling_Part3\UnitTestRunner.mq5 | creates TradeMathCore_TestReport.txt, which the diagnostic gate reads before accepting a run. |
| MQL5\Scripts\Debugging_Profiling_Part3\PromoteProfilerBaseline.mq5 | copies an accepted profiler CSV to ProfilerExampleEA_Baseline.csv. |
| MQL5\Scripts\Debugging_Profiling_Part3\DiagnosticGateRunner.mq5 | runs the final gate by combining profiler deltas, unit-test status, and trade assertions. |
Conclusion
Part III turns the reports from Part II into a decision. PerfMeter.mqh measures named sections, TestLite checks pure trading math, and TradeMathCore owns reusable calculations. The new gate reads those outputs, adds symbol-aware assertions, and produces a final status that can be reviewed without guessing. The package adds a decision layer without forking the earlier profiler or test helper.
The useful contribution is the project-local regression gate: baseline profile, current profile, unit-test status, symbol assertions, and a machine-readable final decision. Use it with discipline: do not promote baselines casually, do not compare different test environments as if the results were precise, and do not let WARN rows become normal background noise. Most good diagnostic runs should be uneventful; their value is that an unexpected run is harder to ignore.
The next natural extension is automation. A script can read DiagnosticGate_Status.txt and block packaging or publication when overall_status=FAIL without changing the profiler or the test helper. That is the progression of this series: observe events, measure cost, test rules, and reject unacceptable drift before it becomes expensive. The same pattern can support larger Expert Advisor projects, especially when several developers share the same baseline evidence across repeated releases.
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Neural Networks in Trading: Skill Hierarchy for Adaptive Agent Behavior (Final Part)
Quantum Neural Network in MQL5 (Part I): Creating the Include File
Features of Experts Advisors
MQL5 Wizard Techniques you should know (Part 94): Using Reservoir Sampling and Linear Regression in a Custom Trailing Stop Class
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use