//+------------------------------------------------------------------+
//|                                               RegressionGate.mqh |
//|                                  Copyright 2026, MetaQuotes Ltd. |
//|                                                     www.mql5.com |
//+------------------------------------------------------------------+
#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;
  };
//+------------------------------------------------------------------+
//| Class: CRegressionGate                                           |
//| Reads baseline/current profiler CSV files and writes decisions.  |
//+------------------------------------------------------------------+
class CRegressionGate
  {
private:
   SProfilerCsvRow m_baseline[];
   SProfilerCsvRow m_current[];
   SProfileDeltaRow m_deltas[];
   SRegressionGateConfig m_config;

   string m_last_error;
   string m_performance_status;
   string m_unit_test_status;
   string m_trade_assert_status;
   int m_profile_warning_count;
   int m_profile_failure_count;
   int m_profile_skipped_count;
//+------------------------------------------------------------------+
//| Make a string safe for simple CSV output                         |
//+------------------------------------------------------------------+
   string CsvSafe(const string value) const
     {
      string result = value;
      StringReplace(result, "\r", " ");
      StringReplace(result, "\n", " ");
      StringReplace(result, ",", ";");
      return result;
     }
//+------------------------------------------------------------------+
//| Calculate percentage change                                      |
//+------------------------------------------------------------------+
   double PercentChange(const double baseline, const double current) const
     {
      if(MathAbs(baseline) < 0.0000001)
        {
         //--- A rounded-zero baseline still needs to expose material current cost.
         if(current > baseline)
            return 1000000.0;
         if(current < baseline)
            return -100.0;
         return 0.0;
        }
      return ((current - baseline) / baseline) * 100.0;
     }
//+------------------------------------------------------------------+
//| Find profiler row by section name                                |
//+------------------------------------------------------------------+
   int FindProfileRow(SProfilerCsvRow &rows[], const string section) const
     {
      for(int i = 0; i < ArraySize(rows); i++)
        {
         if(rows[i].section == section)
            return i;
        }
      return -1;
     }
//+------------------------------------------------------------------+
//| Reset arrays and comparison state                                |
//+------------------------------------------------------------------+
   void ResetComparisonState(void)
     {
      //--- Unit-test and trade-assertion statuses survive until the runner sets them.
      ArrayResize(m_baseline, 0);
      ArrayResize(m_current, 0);
      ArrayResize(m_deltas, 0);
      m_last_error = "";
      m_performance_status = "PASS";
      m_profile_warning_count = 0;
      m_profile_failure_count = 0;
      m_profile_skipped_count = 0;
     }
//+------------------------------------------------------------------+
//| Append one delta row and update counters                         |
//+------------------------------------------------------------------+
   void AddDelta(const SProfileDeltaRow &row)
     {
      int size = ArraySize(m_deltas);
      ArrayResize(m_deltas, size + 1);
      m_deltas[size] = row;

      if(row.status == "FAIL")
         m_profile_failure_count++;
      else if(row.status == "WARN")
         m_profile_warning_count++;
      else if(row.status == "SKIP")
         m_profile_skipped_count++;
     }
//+------------------------------------------------------------------+
//| Append reason text                                               |
//+------------------------------------------------------------------+
   void AddReason(string &reason, const string text) const
     {
      if(reason != "")
         reason += "; ";
      reason += text;
     }
//+------------------------------------------------------------------+
//| Load a profiler CSV created by CPerfMeter::WriteCsvReport        |
//+------------------------------------------------------------------+
   bool LoadProfileCsv(const string file_name,
                                    SProfilerCsvRow &rows[],
                                    const bool common_file)
     {
      ArrayResize(rows, 0);

      int flags = FILE_READ | FILE_CSV | FILE_ANSI;
      if(common_file)
         flags |= FILE_COMMON;

      int handle = FileOpen(file_name, flags, ',');
      if(handle == INVALID_HANDLE)
        {
         m_last_error = StringFormat("cannot open profiler CSV '%s', error=%d", file_name, GetLastError());
         return false;
        }

      //--- Empty files cannot become a reliable baseline or current report.
      if(FileIsEnding(handle))
        {
         FileClose(handle);
         m_last_error = StringFormat("profiler CSV '%s' is empty", file_name);
         return false;
        }

      //--- Header produced by Part II PerfMeter:
      //--- section,calls,total_us,min_us,max_us,avg_us,slow_calls,threshold_us,status
      for(int header_col = 0; header_col < 9 && !FileIsEnding(handle); header_col++)
         FileReadString(handle);

      while(!FileIsEnding(handle))
        {
         string section = FileReadString(handle);
         if(section == "" && FileIsEnding(handle))
            break;

         string calls_text      = FileReadString(handle);
         string total_text      = FileReadString(handle);
         string min_text        = FileReadString(handle);
         string max_text        = FileReadString(handle);
         string avg_text        = FileReadString(handle);
         string slow_calls_text = FileReadString(handle);
         string threshold_text  = FileReadString(handle);
         string status_text     = FileReadString(handle);

         if(section == "")
            continue;

         //--- 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;
        }

      FileClose(handle);

      if(ArraySize(rows) == 0)
        {
         m_last_error = StringFormat("profiler CSV '%s' contains no data rows", file_name);
         return false;
        }

      return true;
     }
//+------------------------------------------------------------------+
//| Build a delta row for one matched section                        |
//+------------------------------------------------------------------+
   void BuildDeltaRow(const SProfilerCsvRow &baseline,
                                   const SProfilerCsvRow &current,
                                   SProfileDeltaRow &row)
     {
      row.section = current.section;
      row.baseline_calls = baseline.calls;
      row.current_calls = current.calls;
      row.baseline_avg_us = baseline.avg_us;
      row.current_avg_us = current.avg_us;
      row.avg_delta_us = current.avg_us - baseline.avg_us;
      row.avg_delta_pct = PercentChange(baseline.avg_us, current.avg_us);
      row.baseline_max_us = baseline.max_us;
      row.current_max_us = current.max_us;
      row.max_delta_us = current.max_us - baseline.max_us;
      row.max_delta_pct = PercentChange(baseline.max_us, current.max_us);
      row.status = "PASS";
      row.reason = "within_threshold";

      bool enough_calls = (baseline.calls >= m_config.min_calls && current.calls >= m_config.min_calls);
      bool enough_avg = (baseline.avg_us >= m_config.min_comparable_avg_us ||
                         current.avg_us >= m_config.min_comparable_avg_us);

      //--- Tiny or rarely called sections are reported, but not judged as regressions.
      if(!enough_calls || !enough_avg)
        {
         row.status = "SKIP";
         row.reason = "not_comparable";
         if(!enough_calls)
            AddReason(row.reason, "call_count_below_minimum");
         if(!enough_avg)
            AddReason(row.reason, "average_time_below_noise_floor");
         return;
        }

      string reason = "";
      bool fail = false;
      bool warn = false;

      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;
     }
//+------------------------------------------------------------------+
//| Convert status to severity                                       |
//+------------------------------------------------------------------+
   int StatusSeverity(const string status) const
     {
      if(status == "FAIL" || status == "ERROR" || status == "MISSING")
         return 2;
      if(status == "WARN" || status == "UNKNOWN" || status == "NOT_CHECKED")
         return 1;
      return 0;
     }
//+------------------------------------------------------------------+
//| Compose final status from performance and report statuses        |
//+------------------------------------------------------------------+
   string ComposeOverallStatus(void) const
     {
      int severity = 0;
      int current_severity = StatusSeverity(m_performance_status);
      if(current_severity > severity)
         severity = current_severity;

      current_severity = StatusSeverity(m_unit_test_status);
      if(current_severity > severity)
         severity = current_severity;

      current_severity = StatusSeverity(m_trade_assert_status);
      if(current_severity > severity)
         severity = current_severity;

      if(severity >= 2)
         return "FAIL";
      if(severity == 1)
         return "WARN";
      return "PASS";
     }

public:
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
   CRegressionGate(void)
     {
      ResetComparisonState();
      m_unit_test_status = "NOT_CHECKED";
      m_trade_assert_status = "NOT_CHECKED";

      m_config.warn_avg_increase_pct = 20.0;
      m_config.fail_avg_increase_pct = 35.0;
      m_config.warn_max_increase_pct = 25.0;
      m_config.fail_max_increase_pct = 50.0;
      m_config.warn_abs_avg_us = 5.0;
      m_config.fail_abs_avg_us = 10.0;
      m_config.warn_abs_max_us = 25.0;
      m_config.fail_abs_max_us = 50.0;
      m_config.min_comparable_avg_us = 2.0;
      m_config.min_calls = 20;
      m_config.warn_on_new_sections = true;
      m_config.warn_on_missing_sections = true;
     }
//+------------------------------------------------------------------+
//| Configure comparison thresholds                                  |
//+------------------------------------------------------------------+
   void Configure(const double warn_avg_pct,
                               const double fail_avg_pct,
                               const double warn_max_pct,
                               const double fail_max_pct,
                               const double warn_abs_avg_us,
                               const double fail_abs_avg_us,
                               const double warn_abs_max_us,
                               const double fail_abs_max_us,
                               const double min_comparable_avg_us,
                               const long min_calls)
     {
      m_config.warn_avg_increase_pct = warn_avg_pct;
      m_config.fail_avg_increase_pct = fail_avg_pct;
      m_config.warn_max_increase_pct = warn_max_pct;
      m_config.fail_max_increase_pct = fail_max_pct;
      m_config.warn_abs_avg_us = warn_abs_avg_us;
      m_config.fail_abs_avg_us = fail_abs_avg_us;
      m_config.warn_abs_max_us = warn_abs_max_us;
      m_config.fail_abs_max_us = fail_abs_max_us;
      m_config.min_comparable_avg_us = min_comparable_avg_us;
      m_config.min_calls = min_calls;
     }
//+------------------------------------------------------------------+
//| Configure policy for new and missing profiler sections           |
//+------------------------------------------------------------------+
   void SetNewSectionPolicy(const bool warn_on_new_sections,
                                         const bool warn_on_missing_sections)
     {
      m_config.warn_on_new_sections = warn_on_new_sections;
      m_config.warn_on_missing_sections = warn_on_missing_sections;
     }
//+------------------------------------------------------------------+
//| Compare baseline and current profiler CSV files                  |
//+------------------------------------------------------------------+
   bool CompareProfilerReports(const string baseline_file,
                               const string current_file,
                               const bool common_file = true)
     {
      ResetComparisonState();

      //--- A missing baseline is a gate failure because there is no trusted comparison.
      if(!LoadProfileCsv(baseline_file, m_baseline, common_file))
        {
         m_performance_status = "FAIL";
         m_profile_failure_count++;
         return false;
        }

      if(!LoadProfileCsv(current_file, m_current, common_file))
        {
         m_performance_status = "FAIL";
         m_profile_failure_count++;
         return false;
        }

      for(int i = 0; i < ArraySize(m_current); i++)
        {
         int baseline_index = FindProfileRow(m_baseline, m_current[i].section);
         if(baseline_index < 0)
           {
            //--- New section names change the measurement surface and need review.
            SProfileDeltaRow new_row;
            new_row.section = m_current[i].section;
            new_row.baseline_calls = 0;
            new_row.current_calls = m_current[i].calls;
            new_row.baseline_avg_us = 0.0;
            new_row.current_avg_us = m_current[i].avg_us;
            new_row.avg_delta_us = m_current[i].avg_us;
            new_row.avg_delta_pct = 0.0;
            new_row.baseline_max_us = 0.0;
            new_row.current_max_us = m_current[i].max_us;
            new_row.max_delta_us = m_current[i].max_us;
            new_row.max_delta_pct = 0.0;
            new_row.status = (m_config.warn_on_new_sections ? "WARN" : "PASS");
            new_row.reason = "new_section_no_baseline";
            AddDelta(new_row);
            continue;
           }

         m_baseline[baseline_index].matched = true;
         SProfileDeltaRow row;
         BuildDeltaRow(m_baseline[baseline_index], m_current[i], row);
         AddDelta(row);
        }

      for(int j = 0; j < ArraySize(m_baseline); j++)
        {
         if(m_baseline[j].matched)
            continue;

         //--- Missing section names may mean removed work or a renamed metric.
         SProfileDeltaRow missing_row;
         missing_row.section = m_baseline[j].section;
         missing_row.baseline_calls = m_baseline[j].calls;
         missing_row.current_calls = 0;
         missing_row.baseline_avg_us = m_baseline[j].avg_us;
         missing_row.current_avg_us = 0.0;
         missing_row.avg_delta_us = -m_baseline[j].avg_us;
         missing_row.avg_delta_pct = -100.0;
         missing_row.baseline_max_us = m_baseline[j].max_us;
         missing_row.current_max_us = 0.0;
         missing_row.max_delta_us = -m_baseline[j].max_us;
         missing_row.max_delta_pct = -100.0;
         missing_row.status = (m_config.warn_on_missing_sections ? "WARN" : "PASS");
         missing_row.reason = "baseline_section_missing_in_current";
         AddDelta(missing_row);
        }

      if(m_profile_failure_count > 0)
         m_performance_status = "FAIL";
      else if(m_profile_warning_count > 0)
         m_performance_status = "WARN";
      else
         m_performance_status = "PASS";

      return true;
     }
//+------------------------------------------------------------------+
//| Write all delta rows                                             |
//+------------------------------------------------------------------+
   bool WriteDeltaCsv(const string file_name, const bool common_file = true)
     {
      int flags = FILE_WRITE | FILE_CSV | FILE_ANSI;
      if(common_file)
         flags |= FILE_COMMON;

      int handle = FileOpen(file_name, flags, ',');
      if(handle == INVALID_HANDLE)
        {
         m_last_error = StringFormat("cannot open delta CSV '%s', error=%d", file_name, GetLastError());
         return false;
        }

      //--- The delta CSV is the detailed evidence behind the final status.
      FileWrite(handle,
                "section",
                "baseline_calls",
                "current_calls",
                "baseline_avg_us",
                "current_avg_us",
                "avg_delta_us",
                "avg_delta_pct",
                "baseline_max_us",
                "current_max_us",
                "max_delta_us",
                "max_delta_pct",
                "status",
                "reason");

      for(int i = 0; i < ArraySize(m_deltas); i++)
        {
         FileWrite(handle,
                   CsvSafe(m_deltas[i].section),
                   m_deltas[i].baseline_calls,
                   m_deltas[i].current_calls,
                   DoubleToString(m_deltas[i].baseline_avg_us, 2),
                   DoubleToString(m_deltas[i].current_avg_us, 2),
                   DoubleToString(m_deltas[i].avg_delta_us, 2),
                   DoubleToString(m_deltas[i].avg_delta_pct, 2),
                   DoubleToString(m_deltas[i].baseline_max_us, 2),
                   DoubleToString(m_deltas[i].current_max_us, 2),
                   DoubleToString(m_deltas[i].max_delta_us, 2),
                   DoubleToString(m_deltas[i].max_delta_pct, 2),
                   m_deltas[i].status,
                   CsvSafe(m_deltas[i].reason));
        }

      FileFlush(handle);
      FileClose(handle);
      return true;
     }
//+------------------------------------------------------------------+
//| Write rows that require attention                                |
//+------------------------------------------------------------------+
   bool WriteFailuresCsv(const string file_name, const bool common_file = true)
     {
      int flags = FILE_WRITE | FILE_CSV | FILE_ANSI;
      if(common_file)
         flags |= FILE_COMMON;

      int handle = FileOpen(file_name, flags, ',');
      if(handle == INVALID_HANDLE)
        {
         m_last_error = StringFormat("cannot open failures CSV '%s', error=%d", file_name, GetLastError());
         return false;
        }

      FileWrite(handle,
                "section",
                "status",
                "reason",
                "baseline_avg_us",
                "current_avg_us",
                "avg_delta_pct",
                "baseline_max_us",
                "current_max_us",
                "max_delta_pct");

      for(int i = 0; i < ArraySize(m_deltas); i++)
        {
         if(m_deltas[i].status == "PASS")
            continue;

         //--- Keep this report short by writing only rows that need review.
         FileWrite(handle,
                   CsvSafe(m_deltas[i].section),
                   m_deltas[i].status,
                   CsvSafe(m_deltas[i].reason),
                   DoubleToString(m_deltas[i].baseline_avg_us, 2),
                   DoubleToString(m_deltas[i].current_avg_us, 2),
                   DoubleToString(m_deltas[i].avg_delta_pct, 2),
                   DoubleToString(m_deltas[i].baseline_max_us, 2),
                   DoubleToString(m_deltas[i].current_max_us, 2),
                   DoubleToString(m_deltas[i].max_delta_pct, 2));
        }

      FileFlush(handle);
      FileClose(handle);
      return true;
     }
//+------------------------------------------------------------------+
//| Static trim helper for text reports                              |
//+------------------------------------------------------------------+
   static string TrimText(const string text)
     {
      int length = StringLen(text);
      if(length <= 0)
         return "";

      //--- Trim manually so report parsing does not depend on unavailable helpers.
      int first = 0;
      while(first < length)
        {
         ushort ch = StringGetCharacter(text, first);
         if(ch > 32)
            break;
         first++;
        }

      int last = length - 1;
      while(last >= first)
        {
         ushort ch = StringGetCharacter(text, last);
         if(ch > 32)
            break;
         last--;
        }

      if(last < first)
         return "";
      return StringSubstr(text, first, last - first + 1);
     }
//+------------------------------------------------------------------+
//| Read a key=value status line from a text report                  |
//+------------------------------------------------------------------+
   static bool ReadKeyValue(const string file_name,
                                  const string key,
                                  const bool common_file,
                                  string &value,
                                  string &error_message)
     {
      value = "";
      error_message = "";

      int flags = FILE_READ | FILE_TXT | FILE_ANSI;
      if(common_file)
         flags |= FILE_COMMON;

      int handle = FileOpen(file_name, flags);
      if(handle == INVALID_HANDLE)
        {
         error_message = StringFormat("cannot open '%s', error=%d", file_name, GetLastError());
         return false;
        }

      while(!FileIsEnding(handle))
        {
         string line = FileReadString(handle);
         int equals_pos = StringFind(line, "=");
         if(equals_pos <= 0)
            continue;

         //--- Match the key exactly after trimming whitespace around it.
         string found_key = CRegressionGate::TrimText(StringSubstr(line, 0, equals_pos));
         if(found_key != key)
            continue;

         value = CRegressionGate::TrimText(StringSubstr(line, equals_pos + 1));
         FileClose(handle);
         return true;
        }

      FileClose(handle);
      error_message = StringFormat("key '%s' not found in '%s'", key, file_name);
      return false;
     }
//+------------------------------------------------------------------+
//| Store unit-test status read from the Part II TestLite report     |
//+------------------------------------------------------------------+
   void SetUnitTestStatus(const string status)
     {
      m_unit_test_status = (status == "" ? "UNKNOWN" : status);
     }
//+------------------------------------------------------------------+
//| Store status from the Part III trade-assertion report            |
//+------------------------------------------------------------------+
   void SetTradeAssertionStatus(const string status)
     {
      m_trade_assert_status = (status == "" ? "UNKNOWN" : status);
     }
//+------------------------------------------------------------------+
//| Write machine-readable final gate status                         |
//+------------------------------------------------------------------+
   bool WriteStatusFile(const string file_name,
                                     const bool common_file,
                                     const string baseline_profile_file,
                                     const string current_profile_file,
                                     const string delta_report_file,
                                     const string failure_report_file,
                                     const string trade_assertion_report_file,
                                     const string unit_test_report_file)
     {
      int flags = FILE_WRITE | FILE_TXT | FILE_ANSI;
      if(common_file)
         flags |= FILE_COMMON;

      int handle = FileOpen(file_name, flags);
      if(handle == INVALID_HANDLE)
        {
         m_last_error = StringFormat("cannot open status file '%s', error=%d", file_name, GetLastError());
         return false;
        }

      //--- Keep the status file key=value so scripts can consume it easily.
      FileWriteString(handle, "diagnostic_version=3\r\n");
      FileWriteString(handle, "overall_status=" + OverallStatus() + "\r\n");
      FileWriteString(handle, "performance_status=" + m_performance_status + "\r\n");
      FileWriteString(handle, "unit_test_status=" + m_unit_test_status + "\r\n");
      FileWriteString(handle, "trade_assert_status=" + m_trade_assert_status + "\r\n");
      FileWriteString(handle, "profile_failures=" + IntegerToString(m_profile_failure_count) + "\r\n");
      FileWriteString(handle, "profile_warnings=" + IntegerToString(m_profile_warning_count) + "\r\n");
      FileWriteString(handle, "profile_skipped=" + IntegerToString(m_profile_skipped_count) + "\r\n");
      FileWriteString(handle, "effective_failures=" + IntegerToString(EffectiveFailureCount()) + "\r\n");
      FileWriteString(handle, "effective_warnings=" + IntegerToString(EffectiveWarningCount()) + "\r\n");
      FileWriteString(handle, "delta_rows=" + IntegerToString(ArraySize(m_deltas)) + "\r\n");
      FileWriteString(handle, "baseline_profile=" + baseline_profile_file + "\r\n");
      FileWriteString(handle, "current_profile=" + current_profile_file + "\r\n");
      FileWriteString(handle, "delta_report=" + delta_report_file + "\r\n");
      FileWriteString(handle, "failure_report=" + failure_report_file + "\r\n");
      FileWriteString(handle, "trade_assertion_report=" + trade_assertion_report_file + "\r\n");
      FileWriteString(handle, "unit_test_report=" + unit_test_report_file + "\r\n");
      FileWriteString(handle, "last_error=" + m_last_error + "\r\n");
      FileFlush(handle);
      FileClose(handle);
      return true;
     }
//+------------------------------------------------------------------+
//| Return last error text                                           |
//+------------------------------------------------------------------+
   string LastError(void) const
     {
      return m_last_error;
     }
//+------------------------------------------------------------------+
//| Return profiler comparison status                                |
//+------------------------------------------------------------------+
   string PerformanceStatus(void) const
     {
      return m_performance_status;
     }
//+------------------------------------------------------------------+
//| Return final status                                              |
//+------------------------------------------------------------------+
   string OverallStatus(void) const
     {
      return ComposeOverallStatus();
     }
//+------------------------------------------------------------------+
//| Return number of profile failure rows                            |
//+------------------------------------------------------------------+
   int ProfileFailureCount(void) const
     {
      return m_profile_failure_count;
     }
//+------------------------------------------------------------------+
//| Return number of profile warning rows                            |
//+------------------------------------------------------------------+
   int ProfileWarningCount(void) const
     {
      return m_profile_warning_count;
     }
//+------------------------------------------------------------------+
//| Return number of skipped profile rows                            |
//+------------------------------------------------------------------+
   int ProfileSkippedCount(void) const
     {
      return m_profile_skipped_count;
     }
//+------------------------------------------------------------------+
//| Return total delta rows                                          |
//+------------------------------------------------------------------+
   int DeltaCount(void) const
     {
      return ArraySize(m_deltas);
     }
//+------------------------------------------------------------------+
//| Count blocking failure sources                                   |
//+------------------------------------------------------------------+
   int EffectiveFailureCount(void) const
     {
      int count = m_profile_failure_count;
      if(StatusSeverity(m_unit_test_status) >= 2)
         count++;
      if(StatusSeverity(m_trade_assert_status) >= 2)
         count++;
      return count;
     }
//+------------------------------------------------------------------+
//| Count warning sources                                            |
//+------------------------------------------------------------------+
   int EffectiveWarningCount(void) const
     {
      int count = m_profile_warning_count;
      if(StatusSeverity(m_unit_test_status) == 1)
         count++;
      if(StatusSeverity(m_trade_assert_status) == 1)
         count++;
      return count;
     }
  };
