preview
Creating an HTML Dashboard for Strategy Tester and Prop Firm Challenge Analysis in MQL5

Creating an HTML Dashboard for Strategy Tester and Prop Firm Challenge Analysis in MQL5

MetaTrader 5Examples |
513 0
Roberto Danilo Riccio
Roberto Danilo Riccio

Introduction

A profitable Strategy Tester run alone does not answer whether an Expert Advisor would pass a prop‑firm style challenge. Prop challenges are rule sets — profit target, daily loss limit, overall drawdown limit, minimum trading days, and sometimes a strict time window — and a single backtest start date can be misleading.

This article presents a practical, reusable evaluation layer: a small MQL5 module that monitors balance and equity during a test, normalizes them to a configurable “virtual” challenge account, and simulates either a single attempt or a rolling series of attempts (daily/weekly/monthly starts).

The evaluator reports whether each simulated attempt would have respected the specified rules, why it failed or remained incomplete, and produces two immediate outputs: a concise terminal summary and a shareable HTML dashboard. The module is intentionally a testing aid, not a certified replica of any firm's legal rulebook.



Defining the analysis goal

The goal is to build a reusable evaluator that can be added to an Expert Advisor with minimal integration work. During the test, it simulates one or more challenge attempts and produces a readable report at the end. In this article, a "challenge" refers to the rule set, while an "attempt" refers to one simulated run under those rules.

I deliberately use the word simulate. This module neither enforces broker rules nor attempts to replicate a specific company. Prop firms can use different definitions for daily loss limits, overall drawdown limits, time limits, and target confirmation. The aim is to build a practical evaluator framework, not a legal or contractual replica of any commercial challenge.

The module works with a virtual challenge balance. The real tester account might start with 10,000 or 50,000, while the challenge is evaluated as if it were a 100,000 account.

Normalizing the tester account to a virtual challenge account

Here is how real account values are converted into virtual challenge values.

double vBal = m_initialBalance * (rBal / m_challenges[i].startRealBalance);
double vEq  = m_initialBalance * (rEq / m_challenges[i].startRealBalance);

The conversion uses proportional normalization. The real balance at the start of the challenge is used as the common conversion base for both virtual balance and virtual equity. If the real account is 2 percent above its starting balance, the virtual balance is also 2 percent above the configured challenge balance. If open positions move equity, virtual equity moves proportionally too.

With the goal defined, the next step is to express the challenge model through configurable inputs.



Setting the challenge rules

The inputs are grouped into account model, risk limits, evaluation mode, and output options. Keeping them as EA inputs makes it easy to test the same strategy under different challenge assumptions.

input group "═══ Prop Firm Evaluation ═══"
input bool                    InpPropEnable            = true;
input double                  InpPropInitialBalance    = 100000.0;
input double                  InpPropProfitTargetPct   = 8.0;
input double                  InpPropMaxDailyLossPct   = 5.0;
input double                  InpPropMaxOverallLossPct = 10.0;
input int                     InpPropDurationDays      = 30;
input int                     InpPropMinTradingDays    = 5;
input ENUM_PROP_DD_MODE       InpPropDDMode            = PROP_DD_EQUITY;
input ENUM_PROP_DAILY_LOSS_MODE InpPropDailyLossMode   = DAILY_LOSS_EQUITY;
input ENUM_PROP_EVAL_MODE     InpPropEvalMode          = EVAL_ROLLING;
input ENUM_PROP_ROLLING_FREQ  InpPropRollingFreq       = FREQ_MONTHLY;
input bool                    InpPropWriteHTML         = true;

The values shown above are only an example configuration. The user can change the initial challenge balance, profit target, daily loss limit, overall drawdown limit, and minimum trading days. Two settings are especially important: whether the overall drawdown limit is checked on equity or balance, and whether the daily loss reference is based on the starting equity or balance of the day.

The evaluator supports single mode and rolling mode. In single mode, one challenge attempt starts and the next one begins only after the current attempt ends. In rolling mode, a new attempt starts every day, week, or month, which helps measure start-date sensitivity.

Breach conditions are checked before a pass is confirmed. Drawdown breaches are evaluated until the profit target is first reached. From that point, the attempt is treated as target-reached, reflecting the practical assumption that the objective ends once the target is met.

If a stricter rulebook requires drawdown checks to continue until the pass is formally confirmed, this condition can be adjusted accordingly.

Prop Firm Evaluation Input

Image 1: PropEvaluator's external inputs in the Strategy Tester

Once the inputs are in place, each simulated attempt needs a compact internal state.



Preparing the MQL5 data structures

Each challenge attempt needs its own state: start time, end time, status, failure reason, trading days, daily loss reference, and final result. I store this information in one structure and keep all attempts in the same array.

struct SChallenge
  {
   int                    id;                // Challenge attempt identifier
   datetime               startTime;         // Start time of the attempt
   datetime               endTime;           // End time of the attempt
   double                 startRealBalance;  // Real balance at challenge start
   ENUM_CHALLENGE_STATUS  status;            // Current status of the attempt
   ENUM_FAILURE_REASON    failReason;        // Reason for failure if failed
   ENUM_INCOMPLETE_REASON incompleteReason;  // Reason for incomplete if timed out
   int                    tradingDays;       // Number of trading days counted
   int                    lastTradingDay;    // Last counted trading day (YYYYMMDD)
   double                 dailyStartValue;   // Daily starting value for loss calc
   int                    lastProcessedDay;  // Last processed calendar day (YYYYMMDD)
   double                 maxEquity;         // Maximum virtual equity reached
   double                 minEquity;         // Minimum virtual equity reached
   double                 finalBalance;      // Final virtual balance at end
   double                 finalEquity;       // Final virtual equity at end
   bool                   isTargetReached;   // Whether profit target was hit
  };

lastTradingDay prevents the same day from being counted twice. lastProcessedDay detects a new day and updates the daily loss reference. dailyStartValue stores the virtual balance or equity from which the daily loss limit is calculated.

Status, failure reason, and incomplete reason are represented as enumerations. The module uses separate enums for failure and incomplete outcomes: a failed challenge records a drawdown breach, while an incomplete challenge records whether it expired by timeout or by end of test.

enum ENUM_CHALLENGE_STATUS       // Challenge attempt status
  {
   STATUS_ACTIVE,                // Active
   STATUS_PASSED,                // Passed
   STATUS_FAILED,                // Failed
   STATUS_INCOMPLETE             // Incomplete
  };

enum ENUM_FAILURE_REASON         // Reason for challenge failure
  {
   REASON_NONE,                  // No failure
   REASON_DAILY_DRAWDOWN,        // Daily drawdown breach
   REASON_OVERALL_DRAWDOWN       // Overall drawdown breach
  };

enum ENUM_INCOMPLETE_REASON      // Reason for incomplete status
  {
   INCOMPLETE_NONE,              // No incomplete reason
   INCOMPLETE_TIMEOUT,           // Challenge duration expired
   INCOMPLETE_END_OF_TEST        // Backtest ended before completion
  };

The structure and enums are encapsulated inside CPropEvaluator. The EA only has to initialize the evaluator, call it on every tick, and finalize it at the end of the test.

After the data structures are prepared, the evaluator must simulate the logic for each attempt.



Starting a new challenge

When a new challenge attempt starts, the module stores the real balance at that moment. That value becomes the reference point used to normalize future balance and equity into the virtual challenge account. Here is the core initialization logic.

//+------------------------------------------------------------------+
//| Starts a new simulated challenge attempt and normalizes balances |
//+------------------------------------------------------------------+
void CPropEvaluator::StartNewChallenge(datetime now, double currentBal)
  {
   int size = ::ArraySize(m_challenges);
   ::ArrayResize(m_challenges, size + 1, 100);

   m_totalSimulated++;

   MqlDateTime dt;
   ::TimeToStruct(now, dt);
   int currentDay = dt.year * 10000 + dt.mon * 100 + dt.day;

   m_challenges[size].id               = m_totalSimulated;
   m_challenges[size].startTime        = now;
   m_challenges[size].endTime          = 0;
   m_challenges[size].startRealBalance = currentBal > 0 ? currentBal : 1.0;
   m_challenges[size].status           = STATUS_ACTIVE;
   m_challenges[size].failReason       = REASON_NONE;
   m_challenges[size].incompleteReason = INCOMPLETE_NONE;
   m_challenges[size].tradingDays      = 0;
   m_challenges[size].lastTradingDay   = 0;
   m_challenges[size].dailyStartValue  = m_initialBalance;
   m_challenges[size].lastProcessedDay = currentDay;
   m_challenges[size].maxEquity        = m_initialBalance;
   m_challenges[size].minEquity        = m_initialBalance;
   m_challenges[size].finalBalance     = m_initialBalance;
   m_challenges[size].finalEquity      = m_initialBalance;
   m_challenges[size].isTargetReached  = false;
  }

The fallback to 1.0 is only a safety guard against division by zero in abnormal cases. On the first day, dailyStartValue is initialized from the virtual initial balance and later updated when a new day is detected.

Single challenge and rolling challenge mode

Single mode answers one direct question: did this specific attempt pass, fail, or remain incomplete? Rolling mode is more useful for research because it creates a distribution of outcomes across different starting dates. The mode selection logic from the evaluator's OnTick-handling code is shown below.

if(m_evalMode == EVAL_SINGLE)
  {
   if(!m_singleStarted)
     {
      StartNewChallenge(now, rBal);
      m_singleStarted = true;
     }
  }
else
   if(m_evalMode == EVAL_ROLLING)
     {
      bool start = false;
      if(m_rollingFreq == FREQ_DAILY)
        {
         if(currentDay != m_lastStartDayKey)
           {
            start = true;
            m_lastStartDayKey = currentDay;
           }
        }
      else
         if(m_rollingFreq == FREQ_WEEKLY)
           {
            int currentWeekKey = (int)((now + 259200) / 604800);
            if(currentWeekKey != m_lastStartWeekKey)
              {
               start = true;
               m_lastStartWeekKey = currentWeekKey;
              }
           }
         else
            if(m_rollingFreq == FREQ_MONTHLY)
              {
               int currentMonthKey = dt.year * 100 + dt.mon;
               if(currentMonthKey != m_lastStartMonthKey)
                 {
                  start = true;
                  m_lastStartMonthKey = currentMonthKey;
                 }
              }

      if(start)
        {
         StartNewChallenge(now, rBal);
        }
     }

The daily key includes year, month, and day. The monthly key includes year and month. The weekly key is based on elapsed weeks and does not reset at year boundaries. This avoids confusing a new period with an earlier one that shares the same day or month number.

For example, a monthly rolling test over fifteen years can simulate around 180 attempts. That does not guarantee future behavior, but it gives more information than one isolated start date.

Detecting trading days

Many challenges require a minimum number of trading days. In this implementation, a day is counted as a trading day when there is at least one open position or when the real balance has changed since the previous tick.

bool isTradingToday = (::PositionsTotal() > 0) || (rBal != m_lastRealBalance);
m_lastRealBalance = rBal;

if(isTradingToday && m_challenges[i].lastTradingDay != currentDay)
  {
   m_challenges[i].lastTradingDay = currentDay;
   m_challenges[i].tradingDays++;
  }

This is a simplified account-level activity proxy rather than a formal rulebook definition, so it should be treated as an approximation. PositionsTotal() observes the whole account, not a specific symbol, strategy, or magic number. For strategy-specific counting, this part should be replaced with a history-based detector that filters deals by symbol and magic number.

The approximation may differ materially from a specific prop firm's rules. A strategy that keeps positions open overnight may overcount trading days, because an open position still exists on the account when the next calendar day begins.

Handling the daily starting value

A daily loss limit is separate from the overall drawdown limit. It usually resets its reference at the beginning of each trading day, so the evaluator stores that reference in dailyStartValue.

if(currentDay != m_challenges[i].lastProcessedDay)
  {
   double vBalToday = m_initialBalance * (rBal / m_challenges[i].startRealBalance);
   double vEqToday  = m_initialBalance * (rEq / m_challenges[i].startRealBalance);

   m_challenges[i].dailyStartValue = (m_dailyLossMode == DAILY_LOSS_EQUITY) ? vEqToday : vBalToday;
   m_challenges[i].lastProcessedDay = currentDay;
  }

m_dailyLossMode decides whether the daily reference is based on virtual equity or virtual balance.

Calculating target and drawdown limits

Once virtual balance and virtual equity are known, the evaluator calculates the profit target, the overall drawdown limit, and the daily loss limit.

double targetVal    = m_initialBalance * (1.0 + m_profitTargetPct / 100.0);
double overallLimit = m_initialBalance * (1.0 - m_maxOverallLossPct / 100.0);
double dailyLimit   = m_challenges[i].dailyStartValue - m_initialBalance * (m_maxDailyLossPct / 100.0);

double currentValForDD = (m_ddMode == PROP_DD_EQUITY) ? vEq : vBal;
if(!m_challenges[i].isTargetReached)
  {
   if(currentValForDD < overallLimit)
     {
      m_challenges[i].status       = STATUS_FAILED;
      m_challenges[i].failReason   = REASON_OVERALL_DRAWDOWN;
      m_challenges[i].endTime      = now;
      m_challenges[i].finalBalance = vBal;
      m_challenges[i].finalEquity  = vEq;
      m_failedCount++;
      m_overallBreachCount++;
      continue;
     }
   if(currentValForDD < dailyLimit)
     {
      m_challenges[i].status       = STATUS_FAILED;
      m_challenges[i].failReason   = REASON_DAILY_DRAWDOWN;
      m_challenges[i].endTime      = now;
      m_challenges[i].finalBalance = vBal;
      m_challenges[i].finalEquity  = vEq;
      m_failedCount++;
      m_dailyBreachCount++;
      continue;
     }
  }

The comparison uses <, so the attempt fails when the selected value falls below the limit. If failure must occur exactly at the limit, the comparison can be changed to <=. If the overall drawdown limit should be checked only on closed balance, m_ddMode can be set to balance mode.

Passing, failing, and expiring

A challenge attempt can pass, fail because of a drawdown breach, or remain incomplete if the required conditions are not met before it expires or before the test ends. Here is the core evaluation and expiration logic.

if(vEq >= targetVal && !m_challenges[i].isTargetReached)
  {
   m_challenges[i].isTargetReached = true;
  }

if(m_challenges[i].isTargetReached && m_challenges[i].tradingDays >= m_minTradingDays)
  {
   m_challenges[i].status       = STATUS_PASSED;
   m_challenges[i].endTime      = now;
   m_challenges[i].finalBalance = vBal;
   m_challenges[i].finalEquity  = vEq;
   m_passedCount++;
   continue;
  }

Target detection is equity-based in this implementation, because the condition uses vEq. A balance-based model can use vBal instead.

Expiration is checked only if m_durationDays is greater than zero. If m_durationDays is zero, the challenge has no time limit.

if(m_durationDays > 0)
  {
   if(now - m_challenges[i].startTime >= m_durationDays * 86400)
     {
      if(m_challenges[i].isTargetReached && m_challenges[i].tradingDays >= m_minTradingDays)
        {
         m_challenges[i].status       = STATUS_PASSED;
         m_challenges[i].endTime      = now;
         m_challenges[i].finalBalance = vBal;
         m_challenges[i].finalEquity  = vEq;
         m_passedCount++;
        }
      else
        {
         m_challenges[i].status           = STATUS_INCOMPLETE;
         m_challenges[i].incompleteReason = INCOMPLETE_TIMEOUT;
         m_challenges[i].endTime          = now;
         m_challenges[i].finalBalance     = vBal;
         m_challenges[i].finalEquity      = vEq;
         m_incompleteCount++;
        }
     }
  }

When an attempt expires without meeting all pass conditions, its status is set to STATUS_INCOMPLETE and the incompleteReason is recorded as INCOMPLETE_TIMEOUT. This separates timeout from active failures, making the report more informative.

Finalizing active challenges

When the backtest ends, some attempts may still be active. OnDeinit() finalizes them using the last available tester account state. Attempts that already met the target and minimum trading days are counted as Passed; the others are counted as STATUS_INCOMPLETE with the reason set to INCOMPLETE_END_OF_TEST.

The following excerpt shows the finalization logic. The full implementation in the attached file includes the complete PrintReport() and WriteHTML() calls.

//+------------------------------------------------------------------+
//| Finalizes incomplete challenges and outputs evaluation reports   |
//+------------------------------------------------------------------+
void CPropEvaluator::OnDeinit(void)
  {
   if(!m_enable)
      return;

   datetime now  = ::TimeCurrent();
   double   rBal = ::AccountInfoDouble(ACCOUNT_BALANCE);
   double   rEq  = ::AccountInfoDouble(ACCOUNT_EQUITY);

//--- Finalize any still active challenges
   int totalChallenges = ::ArraySize(m_challenges);
   for(int i = 0; i < totalChallenges; i++)
     {
      if(m_challenges[i].status == STATUS_ACTIVE)
        {
         double vBal = m_initialBalance * (rBal / m_challenges[i].startRealBalance);
         double vEq  = m_initialBalance * (rEq / m_challenges[i].startRealBalance);

//--- Check if it actually met the conditions at the very end
         if(m_challenges[i].isTargetReached && m_challenges[i].tradingDays >= m_minTradingDays)
           {
            m_challenges[i].status       = STATUS_PASSED;
            m_challenges[i].endTime      = now;
            m_challenges[i].finalBalance = vBal;
            m_challenges[i].finalEquity  = vEq;
            m_passedCount++;
           }
         else
           {
            m_challenges[i].status           = STATUS_INCOMPLETE;
            m_challenges[i].incompleteReason = INCOMPLETE_END_OF_TEST;
            m_challenges[i].endTime          = now;
            m_challenges[i].finalBalance     = vBal;
            m_challenges[i].finalEquity      = vEq;
            m_incompleteCount++;
           }
        }
     }

//--- Print Report
   PrintReport();

//--- Write HTML Report
   if(m_writeHTML)
     {
      WriteHTML();
     }
  }

This prevents late rolling attempts from disappearing just because the tested period ended before they had enough time to reach a final result.



Including the evaluator in an Expert Advisor

The module is written as an .mqh file, so the same logic can be reused in different Expert Advisors.

#include "PropEvaluator.mqh"

I create one global evaluator instance at the EA level, because the module observes the whole account during the test.

CPropEvaluator g_propEvaluator;

The evaluator is configured and initialized in OnInit(), after the normal EA setup. As per Object-Oriented Programming (OOP) best practices, the module is completely self-contained and does not read the EA's global input variables directly. Instead, the EA passes the input parameters to the evaluator instance using public setters before calling Init(). The following excerpts show only the integration calls; the full EA code would include its own initialization, strategy logic, and cleanup.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- normal EA initialization here

   g_propEvaluator.SetEnable(InpPropEnable);
   g_propEvaluator.SetInitialBalance(InpPropInitialBalance);
   g_propEvaluator.SetProfitTargetPct(InpPropProfitTargetPct);
   g_propEvaluator.SetMaxDailyLossPct(InpPropMaxDailyLossPct);
   g_propEvaluator.SetMaxOverallLossPct(InpPropMaxOverallLossPct);
   g_propEvaluator.SetDurationDays(InpPropDurationDays);
   g_propEvaluator.SetMinTradingDays(InpPropMinTradingDays);
   g_propEvaluator.SetDDMode(InpPropDDMode);
   g_propEvaluator.SetDailyLossMode(InpPropDailyLossMode);
   g_propEvaluator.SetEvalMode(InpPropEvalMode);
   g_propEvaluator.SetRollingFreq(InpPropRollingFreq);
   g_propEvaluator.SetWriteHTML(InpPropWriteHTML);

   g_propEvaluator.Init();

   ::Print("EA initialized successfully.");
   return INIT_SUCCEEDED;
  }

During the backtest, it must be called on every tick. In a portfolio EA, I first let the strategies process the tick and then call the evaluator, so it reads the updated account state.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   for(int i = 0; i < 4; i++)
     {
      strategies[i].ProcessTick();
     }

   g_propEvaluator.OnTick();
  }

At the end of the test, OnDeinit() finalizes active attempts, prints the terminal report, and writes the HTML dashboard if enabled.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   for(int i = 0; i < 4; i++)
      strategies[i].Deinit();

   ::EventKillTimer();
   CleanDashboard();

   g_propEvaluator.OnDeinit();
  }

The evaluator reads balance and equity with AccountInfoDouble(), so it does not need to know why trades were opened or how exits were managed.

double rBal = ::AccountInfoDouble(ACCOUNT_BALANCE);
double rEq  = ::AccountInfoDouble(ACCOUNT_EQUITY);

The accuracy of this model still depends on tester quality, tick modeling, commissions, swaps, spreads, and account state updates.

In summary, the minimal integration requires five steps:

  1. Include PropEvaluator.mqh in the EA source file.
  2. Create a global CPropEvaluator instance.
  3. Call its Init() method inside the EA's OnInit() function.
  4. Call its OnTick() method inside the EA's OnTick() function, after your strategy logic.
  5. Call its OnDeinit() method inside the EA's OnDeinit() function.



Printing a terminal report

Before generating the HTML dashboard, the evaluator prints a text summary in the Experts journal. This is useful during development because the main result can be checked without opening the browser report.

The snippet below shows the main counts and rates. The full module also prints diagnostic statistics such as average trading days, time to pass, maximum and minimum virtual equity, average final equity, and failure reason counts.

SDiagnostics d = ComputeDiagnostics();

::Print("Total Challenges Simulated: ", ::IntegerToString(m_totalSimulated));
::Print("Passed: ", ::IntegerToString(m_passedCount));
::Print("Failed: ", ::IntegerToString(m_failedCount));
::Print("Incomplete: ", ::IntegerToString(m_incompleteCount));
::Print("Pass Rate: ", ::DoubleToString(d.passRate, 2), "%");
::Print("Failure Rate: ", ::DoubleToString(d.failRate, 2), "%");
::Print("Incomplete Rate: ", ::DoubleToString(d.incRate, 2), "%");

A typical terminal summary is shown below, as it appears in the Experts journal after a rolling evaluation run.

Prop Firm Evaluation Tester

Image 2: Terminal summary in the Experts journal



Generating the HTML report

The terminal report is useful for diagnostics, but the HTML dashboard is easier to read and share. At the end of the test, the module opens a local file and writes a complete HTML page with summary cards, diagnostic statistics, and a challenge history table.

string filename = "PropFirm_Dashboard_" + ::MQLInfoString(MQL_PROGRAM_NAME) + ".html";
int fileHandle = ::FileOpen(filename, FILE_WRITE | FILE_TXT | FILE_ANSI);
if(fileHandle == INVALID_HANDLE)
  {
   ::Print("PropFirm: Failed to open file for writing HTML: ", filename);
   return;
  }

MQL5 file functions are sandboxed. The file is created in the terminal's Files directory (or in the tester agent's files directory in the Strategy Tester). The printed path is only a convenience message. With this filename, each new run of the same EA overwrites the previous report. If unique reports are needed, append the date or test parameters to the filename.

The page is assembled line by line using FileWrite(). Here is how the summary cards are generated. For clarity, only the card block is shown here; the full implementation in the attached file contains the complete CSS, header, diagnostics grid, chart section, and challenge history table.

::FileWrite(fileHandle, "<div class='grid'>");
::FileWrite(fileHandle, "  <div class='card'><h3>Simulated</h3><div class='value'>" + ::IntegerToString(m_totalSimulated) + "</div><div class='subtext'>Total Challenges</div></div>");
::FileWrite(fileHandle, "  <div class='card'><h3>Passed</h3><div class='value text-green'>" + ::IntegerToString(m_passedCount) + "</div><div class='subtext'>" + ::DoubleToString(d.passRate, 2) + "% Pass Rate</div></div>");
::FileWrite(fileHandle, "  <div class='card'><h3>Failed</h3><div class='value text-red'>" + ::IntegerToString(m_failedCount) + "</div><div class='subtext'>" + ::DoubleToString(d.failRate, 2) + "% Failure Rate</div></div>");
::FileWrite(fileHandle, "  <div class='card'><h3>Incomplete</h3><div class='value text-blue'>" + ::IntegerToString(m_incompleteCount) + "</div><div class='subtext'>" + ::DoubleToString(d.incRate, 2) + "% Incomplete</div></div>");
::FileWrite(fileHandle, "</div>");

The full implementation can contain a larger CSS block for the light dashboard style, a diagnostics grid with average trading days and time-to-pass statistics, and a detailed challenge history table where each row displays the attempt ID, dates, status, failure or incomplete reason, trading days, and equity extremes.

The resulting dashboard is shown below.

Prop firm report

Image 3: HTML dashboard with summary cards and key statistics

Optional visual charts

The working version also includes two client-side charts: one for the distribution of Passed, Failed, and Incomplete attempts, and one for pass speed distribution by calendar days.

If the dashboard loads Chart.js from a CDN, the HTML file is not fully self-contained. The report is still generated locally by MQL5, but the chart library is external. For a fully offline report, remove the CDN dependency and keep cards, tables, or CSS-based visuals only.

//--- Scripts for Charting : this part is optional. Remove it if the report must remain fully offline.
::FileWrite(fileHandle, "<script src='https://cdn.jsdelivr.net/npm/chart.js'></script>");
::FileWrite(fileHandle, "<script>");
::FileWrite(fileHandle, "const ctx1 = document.getElementById('statusChart').getContext('2d');");
::FileWrite(fileHandle, "new Chart(ctx1, {");
::FileWrite(fileHandle, "  type: 'doughnut',");
::FileWrite(fileHandle, "  data: {");
::FileWrite(fileHandle, "    labels: ['Passed', 'Failed', 'Incomplete'],");
::FileWrite(fileHandle, "    datasets: [{");
::FileWrite(fileHandle, "      data: [" + ::IntegerToString(m_passedCount) + ", " + ::IntegerToString(m_failedCount) + ", " + ::IntegerToString(m_incompleteCount) + "],");
::FileWrite(fileHandle, "      backgroundColor: ['#10b981', '#ef4444', '#3b82f6'],");
::FileWrite(fileHandle, "      borderColor: '#ffffff',");
::FileWrite(fileHandle, "      borderWidth: 2");
::FileWrite(fileHandle, "    }]");
::FileWrite(fileHandle, "  }");
::FileWrite(fileHandle, "});");
::FileWrite(fileHandle, "</script>");

The chart section is shown in the following image.

Prop Firm Evaluation Visual Analysis

Image 4: Optional visual charts powered by Chart.js



Interpreting the dashboard

The dashboard should change how the test is read. A normal Strategy Tester result may look positive, but the evaluator can show that only a small percentage of monthly attempts passed, or that most failures came from daily loss limit breaches rather than overall drawdown limit breaches.

  • If failures are mostly caused by overall drawdown limit breaches, exposure or stop logic may need work.
  • If attempts remain incomplete because of trading days, the issue may be trade frequency or challenge duration.
  • If results depend heavily on the starting month, rolling evaluation exposes that dependence.

It turns a vague impression into a concrete diagnostic.

A simple example

In one illustrative monthly rolling test across a long historical sample, the standard backtest ends in profit, with reasonable drawdown and a profit factor above 1. The evaluator is enabled with a 100,000 initial challenge balance, 8 percent target, 5 percent daily loss limit, 10 percent overall drawdown limit, unlimited duration, and 5 minimum trading days.

In the example report, the dashboard shows 180 total attempts: 149 Passed, 15 Failed, and 17 remained Incomplete. That result is useful, but it still has to be read together with failure reasons, average time to pass, and minimum equity reached during the attempts.

The strategy may be profitable over time, but the dashboard shows whether its return path is compatible with the selected challenge model. The challenge history table for this example run is shown below.

Prop Firm Evaluation Loss Reason

Image 5: Challenge history table showing each attempt and its result



Limitations

No evaluator should be treated as absolute truth. This implementation is a generic evaluator framework, not a certified match to any specific prop firm rulebook. Its precision depends on backtest quality, broker conditions, tick data, commissions, swaps, spreads, and execution assumptions.

The daily loss limit is another important limitation. The module updates the daily starting value when a new date is detected from TimeCurrent(), but a real rulebook may require a specific server cut-off time, time zone, or end-of-day equity reference.

Intraday equity excursions are only as reliable as the tester modeling quality. Rolling evaluation also remains historical simulation: a system that passed many historical attempts can still fail in the future.



Possible improvements

The current version is intentionally compact, but several extensions would make it more flexible:

  1. For filtering, the evaluator could support magic number, symbol, or strategy group selection.
  2. For export, CSV or JSON output would make deeper analysis easier in Python, Excel, or another tool.
  3. For reporting, a pure CSS equity curve could keep the dashboard fully offline.
  4. For automation, the module could send a report notification or the saved report path to Telegram, or save a copy in the common files folder for multi-terminal workflows.



Conclusion

The PropEvaluator complements the Strategy Tester by converting an overall backtest into a rule‑based assessment of challenge fitness. By normalizing real tester balances to a virtual challenge size, tracking per‑attempt state (start/end, trading days, daily reference, max/min equity), and supporting single and rolling modes, the module makes it straightforward to answer: “Would this strategy have met these exact constraints — and if not, why?”

Integration is minimal (include the .mqh, create an instance, and call Init → OnTick → OnDeinit), and outputs include a terminal summary plus an HTML dashboard with per‑attempt history, failure reasons (daily vs. overall drawdown), and basic diagnostics.

Important assumptions and limitations are explicit: the evaluator is a practical simulator (not a legal replica), equity/ balance reference choices and the default behavior of suspending drawdown checks after a target is first reached are configurable, and accuracy depends on backtest data quality and account modeling. Used in rolling mode, the evaluator reveals start‑date sensitivity and provides a more actionable diagnostic than a single backtest curve.

Attached files |
PropEvaluator.mqh (44.38 KB)
Automating Classic Market Methods in MQL5 (Part 2): Wyckoff Cause and Effect—Point and Figure Price Targets Automating Classic Market Methods in MQL5 (Part 2): Wyckoff Cause and Effect—Point and Figure Price Targets
This article builds a self-contained MQL5 Expert Advisor that completes the Wyckoff cycle: it detects accumulation/distribution with a finite state machine, enters at the last point of support/supply, and calculates exit point-and-figure counts under Wyckoff's Cause and Effect. We detail the box size from range ATR, a 1-box reversal, target validation, and a 2R fallback. Readers get runnable code without external dependencies.
Lazy-Loading Indicator Handles in MQL5: A Resource Manager Pattern for Multi-Timeframe EAs Lazy-Loading Indicator Handles in MQL5: A Resource Manager Pattern for Multi-Timeframe EAs
Multi‑timeframe EAs that initialize every indicator handle in OnInit() pay a fixed startup cost even when most handles are never used. CIndicatorCache applies lazy loading with composite‑key lookup, reference‑counted Acquire/Release, and a deterministic FlushAll() for cleanup. Handles are created on first request and reused across ticks, reducing startup latency, avoiding repeated heap allocation, and preventing terminal resource leaks through centralized ownership.
MQL5 Wizard Techniques you should know (Part 99): Using a KD-Tree and an Echo State Network in a Custom Money Management Class MQL5 Wizard Techniques you should know (Part 99): Using a KD-Tree and an Echo State Network in a Custom Money Management Class
This article lays out 'CMoneyKDTreeESN' custom money management class usable with the MQL5 Wizard, that combines the KD-Tree algorithm and the Echo State Network. We use the KD-Tree on log returns and ATR to give us a risk score, while the ESN tracks recent flow to give us a bounded lot size multiplier. Our class is usable in a variety of Wizard assembled Expert Advisors as shown here with the Envelopes and RSI signals, with a broad objective of modulating exposure in high-volatility and tail-risk environments.
MetaTrader 5 Machine Learning Blueprint (Part 18): Sequential Bootstrap, Corrected — Clone, Class Erasure, and the Comparison Toolkit MetaTrader 5 Machine Learning Blueprint (Part 18): Sequential Bootstrap, Corrected — Clone, Class Erasure, and the Comparison Toolkit
The article diagnoses two defects that neutralize sequential bootstrap during cross‑validation: type erasure of SequentiallyBootstrappedBaggingClassifier and a fold‑level shape mismatch from cloning full samples info sets. It retains the classifier's identity, adds find seq bagging to re‑inject fold‑sliced t1 in CalibratorCV.fit, and resets state per split. A new bootstrap_comparison module reports OOF and OOB metrics and memory, letting you verify that sequential sampling is applied correctly and quantify its impact.