preview
Automated Trade Statement Exporter to Excel-Compatible XLSX in MQL5

Automated Trade Statement Exporter to Excel-Compatible XLSX in MQL5

MetaTrader 5Statistics and analysis |
259 0
Ushana Kevin Iorkumbul
Ushana Kevin Iorkumbul

Introduction

After a backtest run or a live trading session, the terminal's History tab holds a complete record of every closed trade: entry and exit prices, profit, duration, stop-loss, take-profit. The data is all there, but getting it into Excel for further analysis is not straightforward. The terminal's built-in export produces an HTML file. Opening that file in Excel triggers a format-conversion dialog, column types default to text, and dates require manual reformatting before any formula can reference them. Copy-pasting from the terminal loses structure entirely and does not scale beyond a handful of trades.

This article builds an MQL5 script that solves the problem directly. The script reads closed trade history, reconstructs each trade from its constituent deal pair using the two-pass SL/TP lookup pattern, and writes a fully structured XLSX file to MQL5/Files/. The output file opens directly in Excel or Google Sheets with no conversion step, no dialog box, and no manual reformatting. Prices are stored as numbers, dates are stored as Excel date serial numbers with a date format applied, and the header row is bold. The implementation separates trade reconstruction, XML generation, and file assembly into four distinct modules with clearly bounded responsibilities.



Why XLSX Over CSV or HTML

CSV is the simplest export format, but it carries a fundamental limitation for trade data. Every field in a CSV file is a string. When Excel opens a CSV, it attempts to infer column types, and the inference fails in predictable ways: prices with five decimal places are sometimes parsed as dates, date strings in non-standard formats are left as text, and profit figures with a negative sign are occasionally treated as formulas. The analyst must manually reformat every numeric column before any formula can operate on the data correctly.

XLSX avoids all of these problems. A SpreadsheetML worksheet stores each cell with an explicit type declaration. A price cell is stored as a number. A date cell is stored as a serial number with a named date format attached. When Excel opens the file, it reads those type declarations directly and renders each column correctly without inference. The file also opens natively in Google Sheets, LibreOffice Calc, and any other OOXML-compatible spreadsheet application. For a trade statement that an analyst will open repeatedly and run formulas against, XLSX is the only format that requires zero preparation after export.

The generated trade_statement.xlsx file open in Excel

The generated trade_statement.xlsx file open in Excel

The same trade_statement.xlsx file open in Google Sheets

The same trade_statement.xlsx file open in Google Sheets after uploading, confirming that date columns are recognized as dates, numeric columns are formatted as numbers, and no conversion dialog was required.


The SpreadsheetML Format

An XLSX file is a ZIP archive. Inside that archive is a set of XML files organized into a specific folder structure. The format is defined by the Office Open XML standard maintained by ECMA International as ECMA-376. No Excel installation is required to produce a valid XLSX file. Any tool that can write XML and produce a ZIP archive can generate an XLSX file that Excel, Google Sheets, and LibreOffice Calc will open.

The minimum set of files needed for a valid, openable XLSX file is seven. Each plays a specific role.

  • [Content_Types].xml lives at the archive root and maps every file path inside the ZIP to its MIME type. Excel reads this file first to discover what the archive contains. Without it, Excel refuses to open the file.
  • _rels/.rels is the root relationships file. It declares the relationship between the archive root and the workbook, identifying xl/workbook.xml as the workbook part using a fixed relationship type URI from the OOXML specification.
  • xl/workbook.xml defines the workbook itself. It lists the worksheets in the workbook, giving each a name and an r:id attribute that links it to a relationship entry.
  • xl/_rels/workbook.xml.rels maps the r:id values in xl/workbook.xml to their actual file paths inside the archive, pointing at xl/worksheets/sheet1.xml, xl/sharedStrings.xml, and xl/styles.xml.
  • xl/worksheets/sheet1.xml is the worksheet containing all cell data organized in rows and columns.
  • xl/sharedStrings.xml holds a table of string values referenced by index from worksheet cells. Column header labels are stored here to avoid repeating identical string literals inside the worksheet XML.
  • xl/styles.xml defines the cell styles and number formats. It is required for date formatting and for the bold header row style.

MQL5 writes each of these XML files individually to MQL5/Files/ using standard file I/O functions. Once all files are in the correct relative structure inside a staging folder, a single call to ShellExecuteW() invokes PowerShell's .NET ZipFile.CreateFromDirectory to package them into a valid ZIP archive with the .xlsx extension.


Cell Types, Dates, and Styles

SpreadsheetML defines cell types through the t attribute on the <c> element. This implementation uses three.

  • An inline string cell carries t="inlineStr" and places its value inside an <is><t> child element. The value lives directly in the cell element rather than in the shared strings table. The direction and symbol columns use this type.
  • A number cell omits the t attribute entirely. The value is placed inside a <v> child element as a plain decimal number. Price, profit, volume, and R-multiple columns use this type.
  • A shared string cell carries t="s". Its <v> element contains an integer index into the shared strings table. All header row cells use this type.

Dates require special handling. SpreadsheetML has no native date type. Dates are stored as number cells containing an Excel date serial number: the integer count of days elapsed since January 1, 1900. A cell containing a date serial number looks identical to any other number cell in the XML. What distinguishes it as a date is a s attribute on the <c> element referencing a style index in xl/styles.xml. That style index must point to an <xf> element whose numFmtId references a date number format. If the style index is wrong or the applyNumberFormat="1" attribute is omitted, Excel displays the serial number as a plain integer.

The following snippet shows a header cell, a date cell, and a number cell in context:

<!-- Header cell: shared string index 0, bold centered style (s="1") -->
<c r="A1" t="s" s="1">
  <v>0</v>
</c>

<!-- Date cell: serial number, date style (s="2") -->
<c r="A2" s="2">
  <v>46179</v>
</c>

<!-- Price cell: number, no style needed -->
<c r="F2">
  <v>1.08523</v>
</c>

The xl/styles.xml file declares three <xf> entries in its <cellXfs> section. Index 0 is the default style. Index 1 applies bold font and centered alignment for header cells. Index 2 applies numFmtId="14", which is Excel's built-in short date format. The <fills> section must declare two placeholder entries before any real fills, and the <cellStyleXfs> section must contain at least one base format record. Omitting any of these structural requirements causes Excel to display a repair prompt when opening the file.


Trade History Reconstruction

MetaTrader 5 records trading activity as individual deal records rather than as complete trades. A trade that opens and closes is represented by at least two deals: one with DEAL_ENTRY_IN marking the position opening, and one with DEAL_ENTRY_OUT marking the position closing. All deals belonging to the same trade share the same DEAL_POSITION_ID value.

Reconstructing a complete trade requires selecting the full deal history for the requested date range using HistorySelect(), then iterating every deal returned by HistoryDealGetTicket(). For each deal, HistoryDealGetInteger() with DEAL_ENTRY identifies whether the deal opens or closes a position. Entry deals are stored in parallel arrays keyed by DEAL_POSITION_ID. When an exit deal is encountered, its position ID is used to locate the matching entry, and the two are combined into a CTradeRecord.

Stop-loss and take-profit values present a challenge. HistoryDealGetDouble() exposes DEAL_SL and DEAL_TP fields on the deal record, but these fields are often zero for deals executed in live trading or during backtests that do not persist SL/TP to the deal record. The two-pass lookup handles this case. The first pass reads DEAL_SL and DEAL_TP directly from the deal record. If either value is zero, the second pass retrieves the order ticket from the deal using DEAL_ORDER, then calls HistoryOrderGetDouble() with ORDER_SL and ORDER_TP to retrieve the values from the originating order record.


ZIP Packaging with ShellExecuteW

Once the individual XML component files are written to a staging folder in the correct relative structure, they must be packaged into a single ZIP archive with an .xlsx extension. Excel opens XLSX files by extracting the ZIP and reading the XML files inside.

MQL5 provides access to external Windows functions through #import. This implementation imports ShellExecuteW from shell32.dll to invoke PowerShell. Rather than using the Compress-Archive PowerShell cmdlet, which depends on the Microsoft.PowerShell.Archive module and can fail silently in non-interactive sessions, the implementation uses .NET's System.IO.Compression.ZipFile class directly. This class is part of the .NET Framework itself and is always available without module auto-loading.

The PowerShell command takes this form:

Add-Type -AssemblyName System.IO.Compression.FileSystem;
[System.IO.Compression.ZipFile]::CreateFromDirectory(
    'C:\path\to\staging\',
    'C:\path\to\output.xlsx',
    [System.IO.Compression.CompressionLevel]::Optimal,
    $false
)

The fourth argument, $false, sets includeBaseDirectory to false. This ensures the XML files land at the ZIP root rather than inside a subfolder named after the staging directory, which is required for Excel to recognize the file structure correctly.

The entire command is wrapped in a PowerShell try/catch block with $ErrorActionPreference='Stop'. On success, the script writes a small done marker file into MQL5/Files/. On failure, it writes the exception message into an error marker file. MQL5 polls for either marker file every 400 milliseconds, up to a 15-second timeout. This means the script reports the exact .NET exception text if anything goes wrong, rather than waiting silently and timing out. Once the done marker appears, the script reads it, cleans up the staging files, and completes in under one second for typical trade statement sizes.

On Linux or Wine environments, ShellExecuteW and PowerShell are not available. An alternative approach would call the system zip command through a different import. That adaptation is outside the scope of this article, but the staging folder structure and XML files are identical regardless of the ZIP tool.


Implementation — TradeRecord.mqh

CTradeRecord is a plain data struct that holds all fields describing one completed trade. It carries the entry ticket, entry and exit times and prices, stop-loss, take-profit, profit, volume, symbol, direction, magic number, and comment. Three computed methods are declared on the struct: RMultiple(), which expresses profit in units of initial risk; DurationSeconds(), which returns the trade's holding period in seconds; and PipsProfit(), which converts the price movement to pips. Both CTradeLoader and CXlsxBuilder read this struct directly, so it lives in its own file to guarantee a single shared definition.

Class Declaration

//+------------------------------------------------------------------+
//|                                                  TradeRecord.mqh |
//+------------------------------------------------------------------+

#ifndef TRADERECORD_MQH
#define TRADERECORD_MQH

//+------------------------------------------------------------------+
//| Holds all data describing one completed round-trip trade.        |
//+------------------------------------------------------------------+
struct CTradeRecord
  {
private:
   //--- no private data; all fields are public for direct access

public:
   ulong             entry_ticket;   // ticket of the entry deal
   ulong             exit_ticket;    // ticket of the exit deal
   datetime          entry_time;     // server time of position open
   datetime          exit_time;      // server time of position close
   double            entry_price;    // fill price of the entry deal
   double            exit_price;     // fill price of the exit deal
   double            stop_loss;      // SL at entry; zero if unset
   double            take_profit;    // TP at entry; zero if unset
   double            profit;         // net profit in account currency
   double            volume;         // lot size of the trade
   string            symbol;         // traded instrument
   string            comment;        // order comment from the broker
   ENUM_DEAL_TYPE    direction;      // DEAL_TYPE_BUY or DEAL_TYPE_SELL
   long              magic;          // EA magic number; zero if manual

   double            RMultiple(void) const;
   int               DurationSeconds(void) const;
   double            PipsProfit(void) const;
  };

The struct stores raw trade fields in plain public members so that both CTradeLoader and CXlsxBuilder can read them without accessor overhead. entry_ticket and exit_ticket identify the two deal records from which the struct was built. stop_loss and take_profit may be zero if neither the deal record nor the originating order carried those values. direction uses ENUM_DEAL_TYPE rather than a boolean so the value is self-documenting when inspected in a debugger. Both computed methods are declared const so they can be called through the const CTradeRecord & references used in CXlsxBuilder.

RMultiple()

//+------------------------------------------------------------------+
//| Expresses profit as a multiple of the initial risk amount.       |
//+------------------------------------------------------------------+
double CTradeRecord::RMultiple(void) const
  {
   if(stop_loss == 0.0)
      return(0.0); // no SL means R cannot be computed

   double risk_per_unit = MathAbs(entry_price - stop_loss);
   if(risk_per_unit == 0.0)
      return(0.0); // degenerate: SL equals entry price

   double profit_per_unit = (direction == DEAL_TYPE_BUY)
                            ? (exit_price - entry_price)
                            : (entry_price - exit_price);
   return(profit_per_unit / risk_per_unit);
  }

RMultiple() divides the per-unit profit by the per-unit risk distance from entry to stop-loss. If stop_loss is zero, the method returns zero because risk is undefined. The direction check is necessary because a buy trade profits when exit is above entry, while a sell trade profits when exit is below entry.

DurationSeconds()

//+------------------------------------------------------------------+
//| Returns the holding period of the trade in whole seconds.        |
//+------------------------------------------------------------------+
int CTradeRecord::DurationSeconds(void) const
  {
   return((int)(exit_time - entry_time));
  }

MQL5 datetime values are Unix timestamps stored as integers, so subtracting entry_time from exit_time directly produces a signed integer count of seconds.

PipsProfit()

//+------------------------------------------------------------------+
//| Converts profit to pips using the symbol's pip value.            |
//+------------------------------------------------------------------+
double CTradeRecord::PipsProfit(void) const
  {
   double pip_size = ::SymbolInfoDouble(symbol, SYMBOL_POINT) * 10.0;
   if(pip_size == 0.0)
      return(0.0); // guard against division by zero on unknown symbols

   double price_diff = (direction == DEAL_TYPE_BUY)
                       ? (exit_price - entry_price)
                       : (entry_price - exit_price);
   return(price_diff / pip_size);
  }

SymbolInfoDouble() retrieves the instrument's point size, which is multiplied by 10 to convert from points to pips for five-decimal instruments. The direction-aware price difference is then divided by the pip size to yield the pip count. A zero guard prevents division by zero when the symbol is absent from the market watch.


Implementation — TradeLoader.mqh

CTradeLoader encapsulates all interaction with the MetaTrader 5 deal history API. Its single public method Load() selects history for a requested date range, iterates all deals, reconstructs complete trades by pairing entry and exit deals on their shared DEAL_POSITION_ID, applies the two-pass SL/TP lookup, and populates an output array of CTradeRecord structs.

Class Declaration

//+------------------------------------------------------------------+
//|                                                  TradeLoader.mqh |
//+------------------------------------------------------------------+

#ifndef TRADELOADER_MQH
#define TRADELOADER_MQH

#include "TradeRecord.mqh"

//+------------------------------------------------------------------+
//| Reads closed trade history and fills an array of CTradeRecord.   |
//+------------------------------------------------------------------+
class CTradeLoader
  {
private:
   string            m_symbol;  // symbol filter; empty string = all symbols
   long              m_magic;   // magic filter; 0 = all magic numbers

   double            FetchSL(ulong deal_ticket, ulong order_ticket);
   double            FetchTP(ulong deal_ticket, ulong order_ticket);

public:
                     CTradeLoader(void);
                    ~CTradeLoader(void);

   int               Load(datetime from, datetime to,
                          const string &symbol, long magic,
                          CTradeRecord &out[]);
  };

m_symbol and m_magic are set at the start of each Load() call before deal iteration begins. The private FetchSL() and FetchTP() methods implement the two-pass lookup individually for each value, making the logic straightforward to follow in isolation.

Constructor

//+------------------------------------------------------------------+
//| Constructor — initializes filter fields to neutral values.       |
//+------------------------------------------------------------------+
CTradeLoader::CTradeLoader(void)
  {
   m_symbol = ""; // empty = no symbol filter
   m_magic  = 0;  // zero = no magic filter
  }

The constructor sets m_symbol to an empty string and m_magic to zero, representing the accept-all state. These values are overwritten at the start of every Load() call.

Destructor

//+------------------------------------------------------------------+
//| Destructor — no heap resources to release.                       |
//+------------------------------------------------------------------+
CTradeLoader::~CTradeLoader(void)
  {
//--- CTradeLoader owns no dynamically allocated objects
  }

CTradeLoader holds only primitive members and one MQL5-managed string. No explicit cleanup is needed.

FetchSL()

//+------------------------------------------------------------------+
//| Returns the SL for a deal, falling back to the order record.     |
//+------------------------------------------------------------------+
double CTradeLoader::FetchSL(ulong deal_ticket, ulong order_ticket)
  {
   double sl = ::HistoryDealGetDouble(deal_ticket, DEAL_SL);
   if(sl == 0.0 && order_ticket > 0)
      sl = ::HistoryOrderGetDouble(order_ticket, ORDER_SL); // second pass via order
   return(sl);
  }

FetchSL() reads the stop-loss from the deal record first. If that value is zero and a valid order ticket exists, it performs a second read from the originating order record using HistoryOrderGetDouble() with ORDER_SL. The order ticket is obtained from the deal via DEAL_ORDER and is passed in by the caller.

FetchTP()

//+------------------------------------------------------------------+
//| Returns the TP for a deal, falling back to the order record.     |
//+------------------------------------------------------------------+
double CTradeLoader::FetchTP(ulong deal_ticket, ulong order_ticket)
  {
   double tp = ::HistoryDealGetDouble(deal_ticket, DEAL_TP);
   if(tp == 0.0 && order_ticket > 0)
      tp = ::HistoryOrderGetDouble(order_ticket, ORDER_TP); // second pass via order
   return(tp);
  }

FetchTP() follows the same two-pass pattern as FetchSL(), reading DEAL_TP first and falling back to ORDER_TP if the deal record carries zero.

Load()

//+------------------------------------------------------------------+
//| Selects history and fills out[] with reconstructed trade records.|
//+------------------------------------------------------------------+
int CTradeLoader::Load(datetime from, datetime to,
                       const string &symbol, long magic,
                       CTradeRecord &out[])
  {
   m_symbol = symbol;
   m_magic  = magic;
   ::ArrayResize(out, 0); // start with an empty output array

   if(!::HistorySelect(from, to))
     {
      ::Print("CTradeLoader::Load: HistorySelect failed");
      return(0);
     }

   int total_deals = (int)::HistoryDealsTotal();

//--- first pass: collect all entry deals, keyed by position ID
   ulong entry_tickets[];
   ulong position_ids[];
   int   entry_count = 0;

   ::ArrayResize(entry_tickets, total_deals);
   ::ArrayResize(position_ids,  total_deals);

   for(int i = 0; i < total_deals; i++)
     {
      ulong ticket = ::HistoryDealGetTicket(i);
      if(ticket == 0)
         continue;

      //--- apply symbol filter
      if(m_symbol != "" &&
         ::HistoryDealGetString(ticket, DEAL_SYMBOL) != m_symbol)
         continue;

      //--- apply magic filter
      if(m_magic != 0 &&
         ::HistoryDealGetInteger(ticket, DEAL_MAGIC) != m_magic)
         continue;

      ENUM_DEAL_ENTRY entry_type =
         (ENUM_DEAL_ENTRY)::HistoryDealGetInteger(ticket, DEAL_ENTRY);

      if(entry_type == DEAL_ENTRY_IN)
        {
         entry_tickets[entry_count] = ticket;
         position_ids[entry_count]  =
            ::HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
         entry_count++;
        }
     }

//--- second pass: match exit deals to entry deals by position ID
   for(int i = 0; i < total_deals; i++)
     {
      ulong ticket = ::HistoryDealGetTicket(i);
      if(ticket == 0)
         continue;

      ENUM_DEAL_ENTRY entry_type =
         (ENUM_DEAL_ENTRY)::HistoryDealGetInteger(ticket, DEAL_ENTRY);

      if(entry_type != DEAL_ENTRY_OUT)
         continue;

      ulong pos_id =
         ::HistoryDealGetInteger(ticket, DEAL_POSITION_ID);

      //--- locate the matching entry deal by position ID
      int match = -1;
      for(int j = 0; j < entry_count; j++)
        {
         if(position_ids[j] == pos_id)
           {
            match = j;
            break;
           }
        }

      if(match < 0)
         continue; // orphaned exit deal; no matching entry found

      ulong entry_tkn  = entry_tickets[match];
      ulong order_tkn  =
         ::HistoryDealGetInteger(ticket, DEAL_ORDER);

      int idx = ::ArraySize(out);
      ::ArrayResize(out, idx + 1);

      out[idx].entry_ticket  = entry_tkn;
      out[idx].exit_ticket   = ticket;
      out[idx].entry_time    =
         (datetime)::HistoryDealGetInteger(entry_tkn, DEAL_TIME);
      out[idx].exit_time     =
         (datetime)::HistoryDealGetInteger(ticket, DEAL_TIME);
      out[idx].entry_price   =
         ::HistoryDealGetDouble(entry_tkn, DEAL_PRICE);
      out[idx].exit_price    =
         ::HistoryDealGetDouble(ticket, DEAL_PRICE);
      out[idx].profit        =
         ::HistoryDealGetDouble(ticket, DEAL_PROFIT);
      out[idx].volume        =
         ::HistoryDealGetDouble(entry_tkn, DEAL_VOLUME);
      out[idx].symbol        =
         ::HistoryDealGetString(entry_tkn, DEAL_SYMBOL);
      out[idx].comment       =
         ::HistoryDealGetString(ticket, DEAL_COMMENT);
      out[idx].direction     =
         (ENUM_DEAL_TYPE)::HistoryDealGetInteger(entry_tkn, DEAL_TYPE);
      out[idx].magic         =
         ::HistoryDealGetInteger(entry_tkn, DEAL_MAGIC);

      //--- SL/TP: two-pass lookup (deal record first, order record fallback)
      out[idx].stop_loss   = FetchSL(entry_tkn, order_tkn);
      out[idx].take_profit = FetchTP(entry_tkn, order_tkn);
     }

   return(::ArraySize(out));
  }

Load() operates in two passes over the full deal list. The first pass collects all entry deals that pass the symbol and magic filters, storing their tickets and position IDs in parallel arrays. The second pass iterates every deal again looking for exit deals, performs a linear search through the entry arrays for a matching position ID, and constructs a CTradeRecord from the paired deals. The SL/TP fields are populated last using the private two-pass helpers. The function returns the total number of trades written to out[], giving the caller an immediate count without a separate call.


Implementation — XlsxBuilder.mqh

CXlsxBuilder generates every XML component required for a valid XLSX file. Its methods return strings rather than writing to disk, keeping file I/O strictly out of this class. This separation means the XML output can be tested in isolation without touching the file system, as the verification script in Section 11 demonstrates.

Class Declaration

//+------------------------------------------------------------------+
//|                                                  XlsxBuilder.mqh |
//+------------------------------------------------------------------+

#ifndef XLSXBUILDER_MQH
#define XLSXBUILDER_MQH

#include "TradeRecord.mqh"

//+------------------------------------------------------------------+
//| Generates all XML components for a valid XLSX file.              |
//| Returns strings only; never touches the file system.             |
//+------------------------------------------------------------------+
class CXlsxBuilder
  {
private:
   string            m_sheet_name;   // worksheet tab label shown in Excel

   string            BuildHeaderRow(void);
   string            BuildTradeRow(const CTradeRecord &trade, int row_index);
   string            CellRef(int col, int row);
   long              DateToSerial(datetime dt);
   string            EscapeXml(const string &s);

public:
                     CXlsxBuilder(void);
                    ~CXlsxBuilder(void);

   void              SetSheetName(const string &name);
   string            BuildContentTypes(void);
   string            BuildRootRels(void);
   string            BuildWorkbook(void);
   string            BuildWorkbookRels(void);
   string            BuildStyles(void);
   string            BuildSharedStrings(const string &headers[], int count);
   string            BuildSheet(const CTradeRecord &trades[], int count);
  };

m_sheet_name controls the worksheet tab label that Excel displays. All string-returning build methods are public so that CXlsxWriter can call them in the correct order. The five private helpers are internal utilities called only from within BuildSheet() and BuildSharedStrings().

Constructor

//+------------------------------------------------------------------+
//| Constructor — sets the default worksheet tab name.               |
//+------------------------------------------------------------------+
CXlsxBuilder::CXlsxBuilder(void)
  {
   m_sheet_name = "Trade Report"; // default tab label visible in Excel
  }

The constructor sets a sensible default for the sheet name. Callers that want a different label can invoke SetSheetName() before calling BuildWorkbook().

Destructor

//+------------------------------------------------------------------+
//| Destructor — no heap resources to release.                       |
//+------------------------------------------------------------------+
CXlsxBuilder::~CXlsxBuilder(void)
  {
//--- all members are MQL5-managed strings; no manual cleanup needed
  }

CXlsxBuilder holds only an MQL5-managed string. No explicit cleanup is required.

SetSheetName()

//+------------------------------------------------------------------+
//| Sets the worksheet tab name used in BuildWorkbook().             |
//+------------------------------------------------------------------+
void CXlsxBuilder::SetSheetName(const string &name)
  {
   m_sheet_name = name;
  }

SetSheetName() must be called before BuildWorkbook() because that method embeds m_sheet_name into the workbook XML.

EscapeXml()

//+------------------------------------------------------------------+
//| Escapes the five XML-significant characters in a string value.   |
//+------------------------------------------------------------------+
string CXlsxBuilder::EscapeXml(const string &s)
  {
   string r = s;
   ::StringReplace(r, "&",  "&amp;");  // ampersand must be first
   ::StringReplace(r, "<",  "&lt;");   // less-than opens a tag
   ::StringReplace(r, ">",  "&gt;");   // greater-than closes a tag
   ::StringReplace(r, "\"", "&quot;"); // double quote breaks attribute values
   ::StringReplace(r, "'",  "&apos;"); // single quote breaks attribute values
   return(r);
  }

EscapeXml() replaces the five characters that have syntactic meaning inside XML. Ampersand must be replaced first, because all four entity references that follow begin with an ampersand. Replacing < before & would be harmless, but replacing & after < would cause the just-written &lt; to be re-escaped into &amp;lt;. Without escaping, a trade comment such as <scalp> would be interpreted as an XML tag, corrupting the cell element structure and causing Excel to display a repair error when opening the file. Double-quote and single-quote are escaped because broker comments may contain these characters, and they appear inside XML attribute values in some cell constructs.

DateToSerial()

//+------------------------------------------------------------------+
//| Converts a MQL5 datetime to an Excel date serial number.         |
//+------------------------------------------------------------------+
long CXlsxBuilder::DateToSerial(datetime dt)
  {
//--- Excel epoch: Jan 1 1900 = serial 1; Unix epoch offset = 25569 days
//--- Add 1 to compensate for Excel's phantom Feb 29 1900 (leap year bug)
   long unix_days = (long)dt / 86400; // seconds → whole days
   return(unix_days + 25569 + 1);
  }

DateToSerial() converts a MQL5 datetime value, which is a Unix timestamp in seconds, to an Excel date serial number. The conversion requires two adjustments. The first is the epoch shift: Excel counts days from January 1, 1900, while Unix timestamps count seconds from January 1, 1970. The offset between these epochs is 25569 days. The second is a quirk in Excel's implementation: Excel incorrectly treats 1900 as a leap year, inserting a phantom February 29 at serial 60. All dates on or after March 1, 1900 must be shifted forward by one to align with Excel's counting. Adding 1 to the result corrects for this. For a concrete example: January 15, 2024 has Unix timestamp 1705276800, which divides to 19737 days, and adding 25570 produces 45307, which is the serial Excel renders as 1/15/2024.

CellRef()

//+------------------------------------------------------------------+
//| Returns an A1-style cell reference string for col and row.       |
//+------------------------------------------------------------------+
string CXlsxBuilder::CellRef(int col, int row)
  {
//--- handles columns 1–26 (A–Z); sufficient for 13-column statement
   string col_str = ::ShortToString((ushort)('A' + col - 1));
   return(col_str + ::IntegerToString(row));
  }

CellRef() converts one-based column and row indices to an A1-style reference string. The character arithmetic 'A' + col - 1 shifts through the ASCII alphabet: column 1 maps to A, column 13 maps to M. ShortToString() converts the ushort code point to a single-character string.

BuildContentTypes()

//+------------------------------------------------------------------+
//| Returns the [Content_Types].xml string for the XLSX archive.     |
//+------------------------------------------------------------------+
string CXlsxBuilder::BuildContentTypes(void)
  {
   string xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n";
   xml += "<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\r\n";
   xml += "  <Default Extension=\"rels\" ";
   xml +=      "ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\r\n";
   xml += "  <Default Extension=\"xml\" ";
   xml +=      "ContentType=\"application/xml\"/>\r\n";
   xml += "  <Override PartName=\"/xl/workbook.xml\" ";
   xml +=      "ContentType=\"application/vnd.openxmlformats-officedocument";
   xml +=      ".spreadsheetml.sheet.main+xml\"/>\r\n";
   xml += "  <Override PartName=\"/xl/worksheets/sheet1.xml\" ";
   xml +=      "ContentType=\"application/vnd.openxmlformats-officedocument";
   xml +=      ".spreadsheetml.worksheet+xml\"/>\r\n";
   xml += "  <Override PartName=\"/xl/sharedStrings.xml\" ";
   xml +=      "ContentType=\"application/vnd.openxmlformats-officedocument";
   xml +=      ".spreadsheetml.sharedStrings+xml\"/>\r\n";
   xml += "  <Override PartName=\"/xl/styles.xml\" ";
   xml +=      "ContentType=\"application/vnd.openxmlformats-officedocument";
   xml +=      ".spreadsheetml.styles+xml\"/>\r\n";
   xml += "</Types>";
   return(xml);
  }

BuildContentTypes() produces the [Content_Types].xml file that Excel reads first when opening an XLSX archive. It registers default content types for .rels and .xml extensions, then declares override content types for each named XML part. If any part in the archive is missing from this declaration, Excel reports a corruption error.

BuildRootRels()

//+------------------------------------------------------------------+
//| Returns the _rels/.rels root relationships file content.         |
//+------------------------------------------------------------------+
string CXlsxBuilder::BuildRootRels(void)
  {
   string xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n";
   xml += "<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\r\n";
   xml += "  <Relationship Id=\"rId1\" ";
   xml +=      "Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\" ";
   xml +=      "Target=\"xl/workbook.xml\"/>\r\n";
   xml += "</Relationships>";
   return(xml);
  }

BuildRootRels() declares the single root relationship that connects the archive root to the workbook file. The Type URI is the fixed string defined by the OOXML specification for the office document relationship. Excel follows this relationship to locate the workbook before reading anything else.

BuildWorkbook()

//+------------------------------------------------------------------+
//| Returns the xl/workbook.xml content listing the single worksheet.|
//+------------------------------------------------------------------+
string CXlsxBuilder::BuildWorkbook(void)
  {
   string xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n";
   xml += "<workbook xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" ";
   xml +=         "xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">\r\n";
   xml += "  <sheets>\r\n";
//--- sheet name is escaped in case it contains XML-significant characters
   xml += "    <sheet name=\"" + EscapeXml(m_sheet_name) + "\" ";
   xml +=          "sheetId=\"1\" r:id=\"rId1\"/>\r\n";
   xml += "  </sheets>\r\n";
   xml += "</workbook>";
   return(xml);
  }

BuildWorkbook() declares a single worksheet named with the value of m_sheet_name. The sheet name is passed through EscapeXml() because a caller might set it to a string that contains XML-significant characters. The r:id attribute links this sheet declaration to the corresponding entry in the workbook relationships file.

BuildWorkbookRels()

//+------------------------------------------------------------------+
//| Returns the xl/_rels/workbook.xml.rels relationship file.        |
//+------------------------------------------------------------------+
string CXlsxBuilder::BuildWorkbookRels(void)
  {
   string xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n";
   xml += "<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\r\n";
   xml += "  <Relationship Id=\"rId1\" ";
   xml +=      "Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\" ";
   xml +=      "Target=\"worksheets/sheet1.xml\"/>\r\n";
   xml += "  <Relationship Id=\"rId2\" ";
   xml +=      "Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings\" ";
   xml +=      "Target=\"sharedStrings.xml\"/>\r\n";
   xml += "  <Relationship Id=\"rId3\" ";
   xml +=      "Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\" ";
   xml +=      "Target=\"styles.xml\"/>\r\n";
   xml += "</Relationships>";
   return(xml);
  }

BuildWorkbookRels() maps the three r:id references used in the workbook to their file paths within the xl/ folder. rId1 resolves to sheet1.xml, rId2 to sharedStrings.xml, and rId3 to styles.xml. All target paths are relative to xl/.

BuildStyles()

//+------------------------------------------------------------------+
//| Returns xl/styles.xml with bold header style and date format.    |
//+------------------------------------------------------------------+
string CXlsxBuilder::BuildStyles(void)
  {
   string xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n";
   xml += "<styleSheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">\r\n";

//--- font 0 = normal, font 1 = bold (used by header row)
   xml += "  <fonts count=\"2\">\r\n";
   xml += "    <font><sz val=\"11\"/><name val=\"Calibri\"/></font>\r\n";
   xml += "    <font><b/><sz val=\"11\"/><name val=\"Calibri\"/></font>\r\n";
   xml += "  </fonts>\r\n";

//--- two placeholder fills required by the OOXML schema before real fills
   xml += "  <fills count=\"2\">\r\n";
   xml += "    <fill><patternFill patternType=\"none\"/></fill>\r\n";
   xml += "    <fill><patternFill patternType=\"gray125\"/></fill>\r\n";
   xml += "  </fills>\r\n";

//--- one empty border entry required as the default border
   xml += "  <borders count=\"1\">\r\n";
   xml += "    <border><left/><right/><top/><bottom/><diagonal/></border>\r\n";
   xml += "  </borders>\r\n";

//--- cellStyleXfs: base format record required by the OOXML schema
   xml += "  <cellStyleXfs count=\"1\">\r\n";
   xml += "    <xf numFmtId=\"0\" fontId=\"0\" fillId=\"0\" borderId=\"0\"/>\r\n";
   xml += "  </cellStyleXfs>\r\n";

//--- style index 0=default, 1=bold centered header, 2=date format
   xml += "  <cellXfs count=\"3\">\r\n";
   xml += "    <xf numFmtId=\"0\"  fontId=\"0\" fillId=\"0\" borderId=\"0\" xfId=\"0\"/>\r\n";
   xml += "    <xf numFmtId=\"0\"  fontId=\"1\" fillId=\"0\" borderId=\"0\" xfId=\"0\">\r\n";
   xml += "      <alignment horizontal=\"center\"/>\r\n";
   xml += "    </xf>\r\n";
//--- numFmtId 14 = Excel built-in short date; applyNumberFormat required
   xml += "    <xf numFmtId=\"14\" fontId=\"0\" fillId=\"0\" borderId=\"0\" xfId=\"0\" ";
   xml +=      "applyNumberFormat=\"1\"/>\r\n";
   xml += "  </cellXfs>\r\n";

   xml += "</styleSheet>";
   return(xml);
  }

BuildStyles() constructs a minimal but schema-valid xl/styles.xml. The file must declare fonts, fills, borders, cellStyleXfs, and cellXfs sections in that order. Omitting any section causes Excel to display a repair prompt. The two fill entries before any real fills are a requirement of the OOXML schema: the first two fill slots are reserved for the none and gray125 patterns. Style index 1 applies fontId="1" for the bold font and centers text horizontally for header readability. Style index 2 applies numFmtId="14", which is Excel's built-in short date format. If applyNumberFormat="1" is omitted, Excel displays the date serial number as a plain integer rather than a formatted date.

BuildSharedStrings()

//+------------------------------------------------------------------+
//| Returns xl/sharedStrings.xml containing the column header names. |
//+------------------------------------------------------------------+
string CXlsxBuilder::BuildSharedStrings(const string &headers[], int count)
  {
   string xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n";
   xml += "<sst xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" ";
   xml +=     "count=\"" + ::IntegerToString(count) + "\" ";
   xml +=     "uniqueCount=\"" + ::IntegerToString(count) + "\">\r\n";

   for(int i = 0; i < count; i++)
     {
      //--- each header escaped in case it contains XML-significant characters
      xml += "  <si><t>" + EscapeXml(headers[i]) + "</t></si>\r\n";
     }

   xml += "</sst>";
   return(xml);
  }

BuildSharedStrings() receives the column header strings as an array and writes each one as an <si><t>...</t></si> entry. The count and uniqueCount attributes on the root <sst> element must reflect the actual number of strings present. Each header string is passed through EscapeXml().

BuildHeaderRow()

//+------------------------------------------------------------------+
//| Returns the XML for the header row using shared string refs.     |
//+------------------------------------------------------------------+
string CXlsxBuilder::BuildHeaderRow(void)
  {
   int col_count = 13; // This must match the column count in BuildTradeRow()
   string row = "    <row r=\"1\">\r\n";
   for(int i = 1; i <= col_count; i++)
     {
      //--- t="s" = shared string reference; s="1" = bold centered style
      row += "      <c r=\"" + CellRef(i, 1) + "\" t=\"s\" s=\"1\">";
      row += "<v>" + ::IntegerToString(i - 1) + "</v>";
      row += "</c>\r\n";
     }
   row += "    </row>\r\n";
   return(row);
  }

BuildHeaderRow() generates row 1 with one cell per column header. Each cell carries t="s" for shared string reference, s="1" for the bold centered style, and a <v> element containing the zero-based index into the shared strings table.

BuildTradeRow()

//+------------------------------------------------------------------+
//| Returns the XML for one trade data row at the given row index.   |
//+------------------------------------------------------------------+
string CXlsxBuilder::BuildTradeRow(const CTradeRecord &trade, int row_index)
  {
   int    r   = row_index;
   string row = "    <row r=\"" + ::IntegerToString(r) + "\">\r\n";

//--- col 1: entry time as date serial; s="2" applies the date style
   row += "      <c r=\"" + CellRef(1, r) + "\" s=\"2\">";
   row += "<v>" + ::IntegerToString(DateToSerial(trade.entry_time)) + "</v>";
   row += "</c>\r\n";

//--- col 2: exit time as date serial
   row += "      <c r=\"" + CellRef(2, r) + "\" s=\"2\">";
   row += "<v>" + ::IntegerToString(DateToSerial(trade.exit_time)) + "</v>";
   row += "</c>\r\n";

//--- col 3: symbol as inline string
   row += "      <c r=\"" + CellRef(3, r) + "\" t=\"inlineStr\">";
   row += "<is><t>" + EscapeXml(trade.symbol) + "</t></is>";
   row += "</c>\r\n";

//--- col 4: direction as inline string
   string dir = (trade.direction == DEAL_TYPE_BUY) ? "BUY" : "SELL";
   row += "      <c r=\"" + CellRef(4, r) + "\" t=\"inlineStr\">";
   row += "<is><t>" + dir + "</t></is>";
   row += "</c>\r\n";

//--- col 5: volume (number; omit t attribute for default numeric type)
   row += "      <c r=\"" + CellRef(5, r) + "\">";
   row += "<v>" + ::DoubleToString(trade.volume, 2) + "</v>";
   row += "</c>\r\n";

//--- col 6: entry price to five decimal places
   row += "      <c r=\"" + CellRef(6, r) + "\">";
   row += "<v>" + ::DoubleToString(trade.entry_price, 5) + "</v>";
   row += "</c>\r\n";

//--- col 7: exit price
   row += "      <c r=\"" + CellRef(7, r) + "\">";
   row += "<v>" + ::DoubleToString(trade.exit_price, 5) + "</v>";
   row += "</c>\r\n";

//--- col 8: stop loss; may be zero if not set
   row += "      <c r=\"" + CellRef(8, r) + "\">";
   row += "<v>" + ::DoubleToString(trade.stop_loss, 5) + "</v>";
   row += "</c>\r\n";

//--- col 9: take profit; may be zero if not set
   row += "      <c r=\"" + CellRef(9, r) + "\">";
   row += "<v>" + ::DoubleToString(trade.take_profit, 5) + "</v>";
   row += "</c>\r\n";

//--- col 10: profit in account currency
   row += "      <c r=\"" + CellRef(10, r) + "\">";
   row += "<v>" + ::DoubleToString(trade.profit, 2) + "</v>";
   row += "</c>\r\n";

//--- col 11: holding period in seconds
   row += "      <c r=\"" + CellRef(11, r) + "\">";
   row += "<v>" + ::IntegerToString(trade.DurationSeconds()) + "</v>";
   row += "</c>\r\n";

//--- col 12: R-multiple to two decimal places
   row += "      <c r=\"" + CellRef(12, r) + "\">";
   row += "<v>" + ::DoubleToString(trade.RMultiple(), 2) + "</v>";
   row += "</c>\r\n";

//--- col 13: broker comment as inline string; always escaped
   row += "      <c r=\"" + CellRef(13, r) + "\" t=\"inlineStr\">";
   row += "<is><t>" + EscapeXml(trade.comment) + "</t></is>";
   row += "</c>\r\n";

   row += "    </row>\r\n";
   return(row);
  }

BuildTradeRow() writes all thirteen columns for one trade. The two date columns carry s="2" to apply the date style. String columns use t="inlineStr" with values wrapped in <is><t> elements. Numeric columns omit the t attribute entirely, which is the SpreadsheetML default for number cells. The comment field is passed through EscapeXml() because broker comments are free text and may contain any character.

BuildSheet()

//+------------------------------------------------------------------+
//| Returns the xl/worksheets/sheet1.xml content for all trades.     |
//+------------------------------------------------------------------+
string CXlsxBuilder::BuildSheet(const CTradeRecord &trades[], int count)
  {
   string xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n";
   xml += "<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" ";
   xml +=           "xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">\r\n";

//--- column widths: date cols wider; comment col widest
   xml += "  <cols>\r\n";
   xml += "    <col min=\"1\"  max=\"2\"  width=\"18\" customWidth=\"1\"/>\r\n";
   xml += "    <col min=\"3\"  max=\"4\"  width=\"10\" customWidth=\"1\"/>\r\n";
   xml += "    <col min=\"5\"  max=\"10\" width=\"12\" customWidth=\"1\"/>\r\n";
   xml += "    <col min=\"11\" max=\"11\" width=\"12\" customWidth=\"1\"/>\r\n";
   xml += "    <col min=\"12\" max=\"12\" width=\"10\" customWidth=\"1\"/>\r\n";
   xml += "    <col min=\"13\" max=\"13\" width=\"30\" customWidth=\"1\"/>\r\n";
   xml += "  </cols>\r\n";

   xml += "  <sheetData>\r\n";
   xml += BuildHeaderRow(); // row 1 = bold column headers

//--- data rows start at row 2 so they follow immediately below the header
   for(int i = 0; i < count; i++)
     {
      xml += BuildTradeRow(trades[i], i + 2);
     }

   xml += "  </sheetData>\r\n";
   xml += "</worksheet>";
   return(xml);
  }

BuildSheet() assembles the complete worksheet XML. The <cols> section sets default column widths so the file opens with readable proportions. BuildHeaderRow() is called first to produce row 1, then BuildTradeRow() is called for each trade with a row index starting at 2. Both BuildHeaderRow() and BuildTradeRow() are private; all external code calls only BuildSheet().


Implementation — XlsxWriter.mqh

CXlsxWriter owns the complete file-writing pipeline. It calls CXlsxBuilder to generate each XML component string, writes the strings to a staging folder in the correct relative paths, invokes PowerShell's .NET ZipFile class through ShellExecuteW() to package them, polls for a marker file that the PowerShell script writes on success or failure, reads back any error text, and then cleans up the staging files.

Import Block and Class Declaration

//+------------------------------------------------------------------+
//|                                                   XlsxWriter.mqh |
//+------------------------------------------------------------------+

#ifndef XLSXWRITER_MQH
#define XLSXWRITER_MQH

//--- Import ShellExecuteW directly from the Windows shell library.
#import "shell32.dll"
int ShellExecuteW(int    hwnd,
                  string lpOperation,
                  string lpFile,
                  string lpParameters,
                  string lpDirectory,
                  int    nShowCmd);
#import

#include "XlsxBuilder.mqh"

//+------------------------------------------------------------------+
//| Orchestrates XML generation, file staging, ZIP, and cleanup.     |
//| The ZIP step now writes its own success/error marker file so     |
//| MQL5 can find out exactly what happened inside the hidden        |
//| PowerShell process instead of only guessing from a timeout.      |
//+------------------------------------------------------------------+
class CXlsxWriter
  {
private:
   string            m_output_path;   // final XLSX filename (relative to MQL5/Files/)
   string            m_temp_folder;   // staging subfolder name under MQL5/Files/
   string            m_base_path;     // absolute path to MQL5/Files/ with trailing slash
   string            m_done_rel;      // relative marker filename signalling success
   string            m_err_rel;       // relative marker filename signalling failure

   bool              WriteComponent(const string &rel_path,
                                    const string &content);
   void              CleanupTempFolder(void);
   void              CleanupMarkers(void);
   string            StripTrailingSlash(const string &path);
   string            ReadTextFile(const string &rel_path);
   int               PollForMarkers(int timeout_ms);

public:
                     CXlsxWriter(void);
                    ~CXlsxWriter(void);

   bool              Write(const CTradeRecord &trades[], int count,
                           const string &filename);
   string            GetOutputPath(void);
  };

ShellExecuteW is imported directly from shell32.dll rather than via a header file, because the standard WinUser32.mqh include is not present in all MetaTrader 5 installations. The #import block declares the Unicode variant of the function with the exact Windows API signature. MQL5 strings are internally wide strings, so ShellExecuteW is the correct choice over ShellExecuteA.

m_done_rel and m_err_rel store the relative filenames of the two marker files the PowerShell script writes to signal its outcome. Using marker files rather than a fixed sleep makes the wait as short as possible: the poller exits the moment either file appears, which in practice takes under one second for a typical trade statement.

Constructor

//+------------------------------------------------------------------+
//| Constructor — initializes path fields to empty strings.          |
//+------------------------------------------------------------------+
CXlsxWriter::CXlsxWriter(void)
  {
   m_output_path = "";
   m_temp_folder = "";
   m_base_path   = "";
   m_done_rel    = "";
   m_err_rel     = "";
  }

All path fields are initialized to empty strings. They are all populated at the start of Write() once the caller-supplied filename is available.

Destructor

//+------------------------------------------------------------------+
//| Destructor — no heap resources to release.                       |
//+------------------------------------------------------------------+
CXlsxWriter::~CXlsxWriter(void)
  {
//--- path strings are MQL5-managed; no manual cleanup needed
  }

CXlsxWriter holds only MQL5-managed strings. The destructor body is empty.

StripTrailingSlash()

//+------------------------------------------------------------------+
//| Removes a trailing backslash from a path string if present.      |
//+------------------------------------------------------------------+
string CXlsxWriter::StripTrailingSlash(const string &path)
  {
   string s = path;
   int len = StringLen(s);
   if(len > 0 && StringGetCharacter(s, len - 1) == '\\')
      StringSetCharacter(s, len - 1, 0); // remove trailing separator
   return(s);
  }

TerminalInfoString(TERMINAL_DATA_PATH) occasionally returns a path ending with a backslash on some MetaTrader 5 builds. StripTrailingSlash() removes it before any path concatenation, so the assembled absolute paths never contain a double backslash.

ReadTextFile()

//+------------------------------------------------------------------+
//| Reads a small text file relative to MQL5/Files/ and returns its  |
//| content as a single string, or "" if the file cannot be opened.  |
//+------------------------------------------------------------------+
string CXlsxWriter::ReadTextFile(const string &rel_path)
  {
   int handle = ::FileOpen(rel_path, FILE_READ | FILE_TXT | FILE_ANSI);
   if(handle == INVALID_HANDLE)
      return("");

   string content = "";
   while(!::FileIsEnding(handle))
     {
      content += ::FileReadString(handle) + " ";
     }
   ::FileClose(handle);
   return(content);
  }

ReadTextFile() is used solely to read the content of the error marker file when PowerShell reports a failure. The file contains the text of the .NET exception that was caught inside the PowerShell try/catch block, which is then logged to the Experts tab so the operator knows exactly what went wrong.

PollForMarkers()

//+------------------------------------------------------------------+
//| Polls for either the done marker or the error marker.            |
//| Returns 1 on done marker found, -1 on error marker found,        |
//| 0 if neither appeared within the timeout.                        |
//+------------------------------------------------------------------+
int CXlsxWriter::PollForMarkers(int timeout_ms)
  {
   int elapsed  = 0;
   int interval = 400; // poll frequently since the ZIP step is fast

   while(elapsed < timeout_ms)
     {
      if(::FileIsExist(m_err_rel))
         return(-1);
      if(::FileIsExist(m_done_rel))
         return(1);
      ::Sleep(interval);
      elapsed += interval;
     }
   return(0);
  }

PollForMarkers() checks for the error marker before the done marker on each iteration, so a fast-failing PowerShell script is detected immediately without waiting out the full timeout. The 400 ms poll interval is short enough that the total wait feels near-instant for a file that appears within one second.

WriteComponent()

//+------------------------------------------------------------------+
//| Writes one XML component file to the staging folder.             |
//+------------------------------------------------------------------+
bool CXlsxWriter::WriteComponent(const string &rel_path,
                                 const string &content)
  {
   string full_path = m_temp_folder + "\\" + rel_path;
   int handle = ::FileOpen(full_path, FILE_WRITE | FILE_TXT | FILE_ANSI);
   if(handle == INVALID_HANDLE)
     {
      ::PrintFormat("CXlsxWriter::WriteComponent: cannot open '%s' error %d",
                    full_path, ::GetLastError());
      return(false);
     }
   ::FileWriteString(handle, content);
   ::FileClose(handle);
   return(true);
  }

WriteComponent() constructs the full MQL5-relative path by prepending the staging folder name, opens the file with FileOpen() using FILE_WRITE | FILE_TXT | FILE_ANSI, writes the entire content string in a single FileWriteString() call to avoid any partial-write state, and closes the handle with FileClose(). Each caller passes named string variables rather than literals, which satisfies MQL5's requirement that const string & parameters receive lvalues.

CleanupTempFolder()

//+------------------------------------------------------------------+
//| Deletes the known staging XML files after the ZIP step completes.|
//+------------------------------------------------------------------+
void CXlsxWriter::CleanupTempFolder(void)
  {
   string files[] =
     {
      "[Content_Types].xml",
      "_rels\\.rels",
      "xl\\workbook.xml",
      "xl\\_rels\\workbook.xml.rels",
      "xl\\worksheets\\sheet1.xml",
      "xl\\sharedStrings.xml",
      "xl\\styles.xml"
     };

   for(int i = 0; i < ArraySize(files); i++)
     {
      string path = m_temp_folder + "\\" + files[i];
      if(::FileIsExist(path))
         ::FileDelete(path);
     }
  }

CleanupTempFolder() iterates the known list of component file paths and deletes each one. MQL5's file API cannot delete directories, so empty subdirectory stubs may persist in MQL5/Files/ after cleanup. These empty folders do not interfere with subsequent export runs.

CleanupMarkers()

//+------------------------------------------------------------------+
//| Deletes the done/error marker files left by the PowerShell step. |
//+------------------------------------------------------------------+
void CXlsxWriter::CleanupMarkers(void)
  {
   if(::FileIsExist(m_done_rel))
      ::FileDelete(m_done_rel);
   if(::FileIsExist(m_err_rel))
      ::FileDelete(m_err_rel);
  }

CleanupMarkers() removes both marker files after the poll completes regardless of which one appeared, so they do not accumulate in MQL5/Files/ across multiple export runs.

Write()

//+------------------------------------------------------------------+
//| Full pipeline: generate XML, stage files, ZIP, and clean up.     |
//| The ZIP step uses .NET's System.IO.Compression.ZipFile directly, |
//| wrapped in try/catch, so a failure writes a readable error       |
//| marker file instead of vanishing inside a hidden process.        |
//+------------------------------------------------------------------+
bool CXlsxWriter::Write(const CTradeRecord &trades[], int count,
                        const string &filename)
  {
   m_output_path = filename;
   long ts = (long)::TimeCurrent();
   m_temp_folder = "xlsx_staging_" + ::IntegerToString(ts);
   m_done_rel    = "xlsx_done_"    + ::IntegerToString(ts) + ".flag";
   m_err_rel     = "xlsx_err_"     + ::IntegerToString(ts) + ".flag";

//--- build the absolute path to MQL5/Files/ with no trailing slash issue
   string data_path = StripTrailingSlash(
                         ::TerminalInfoString(TERMINAL_DATA_PATH));
   m_base_path = data_path + "\\MQL5\\Files\\";

   ::PrintFormat("CXlsxWriter::Write: base  = %s", m_base_path);
   ::PrintFormat("CXlsxWriter::Write: stage = %s", m_temp_folder);

   CXlsxBuilder builder;

   string headers[] =
     {
      "Entry Time", "Exit Time", "Symbol", "Direction",
      "Volume", "Entry Price", "Exit Price",
      "Stop Loss", "Take Profit", "Profit",
      "Duration (s)", "R-Multiple", "Comment"
     };
   int hcount = ::ArraySize(headers);

//--- Writes all seven XML component files into the staging subfolder

   string p1 = "[Content_Types].xml";
   string c1 = builder.BuildContentTypes();
   if(!WriteComponent(p1, c1))
      return(false);

   string p2 = "_rels\\.rels";
   string c2 = builder.BuildRootRels();
   if(!WriteComponent(p2, c2))
      return(false);

   string p3 = "xl\\workbook.xml";
   string c3 = builder.BuildWorkbook();
   if(!WriteComponent(p3, c3))
      return(false);

   string p4 = "xl\\_rels\\workbook.xml.rels";
   string c4 = builder.BuildWorkbookRels();
   if(!WriteComponent(p4, c4))
      return(false);

   string p5 = "xl\\worksheets\\sheet1.xml";
   string c5 = builder.BuildSheet(trades, count);
   if(!WriteComponent(p5, c5))
      return(false);

   string p6 = "xl\\sharedStrings.xml";
   string c6 = builder.BuildSharedStrings(headers, hcount);
   if(!WriteComponent(p6, c6))
      return(false);

   string p7 = "xl\\styles.xml";
   string c7 = builder.BuildStyles();
   if(!WriteComponent(p7, c7))
      return(false);

//--- All XML files confirmed written; build absolute paths for the ZIP step
   string stage_abs = m_base_path + m_temp_folder;
   string output_abs = m_base_path + m_output_path;
   string done_abs   = m_base_path + m_done_rel;
   string err_abs     = m_base_path + m_err_rel;

//--- Removes any leftover markers or output file from a prior run
   CleanupMarkers();
   if(::FileIsExist(m_output_path))
      ::FileDelete(m_output_path);

//--- Builds a PowerShell script that uses .NET ZipFile directly rather
//--- than the Compress-Archive cmdlet. This avoids any dependency on
//--- the Microsoft.PowerShell.Archive module failing to auto-load in
//--- a hidden, non-interactive session. includeBaseDirectory=$false
//--- ensures the XML files land at the ZIP root, not inside a
//--- subfolder, which is required for Excel to recognize the file.
//--- Any exception is caught and written to the error marker file so
//--- MQL5 can read back exactly what went wrong.
   string ps_cmd = "";
   ps_cmd += "$ErrorActionPreference='Stop';";
   ps_cmd += "try {";
   ps_cmd +=   "Add-Type -AssemblyName System.IO.Compression.FileSystem;";
   ps_cmd +=   "if(Test-Path '" + output_abs + "'){Remove-Item '" + output_abs + "' -Force};";
   ps_cmd +=   "[System.IO.Compression.ZipFile]::CreateFromDirectory("
               +      "'" + stage_abs + "',"
               +      "'" + output_abs + "',"
               +      "[System.IO.Compression.CompressionLevel]::Optimal,"
               +      "$false"
               +    ");";
   ps_cmd +=   "Set-Content -Path '" + done_abs + "' -Value 'OK';";
   ps_cmd += "} catch {";
   ps_cmd +=   "$_.Exception.Message | Set-Content -Path '" + err_abs + "';";
   ps_cmd += "}";

   string ps_args = "-NoProfile -NonInteractive -WindowStyle Hidden -Command \""
                    + ps_cmd + "\"";

   ::PrintFormat("CXlsxWriter::Write: ps_cmd = %s", ps_cmd);

//--- ShellExecuteW: lpDirectory = m_base_path for a predictable working dir
   int result = ShellExecuteW(0, "open", "powershell.exe", ps_args, m_base_path, 0);
   if(result <= 32)
     {
      ::PrintFormat("CXlsxWriter::Write: ShellExecuteW failed, result=%d "
                    "(check Tools>Options>Expert Advisors>Allow DLL imports)",
                    result);
      return(false);
     }

   ::PrintFormat("CXlsxWriter::Write: PowerShell launched (result=%d), polling for markers...",
                 result);

//--- poll for the done or error marker; the ZIP step is fast for a
//--- statement of this size, so 15 seconds is generous headroom
   int marker = PollForMarkers(15000);

   if(marker == 1)
     {
      ::Print("CXlsxWriter::Write: done marker found, ZIP step succeeded.");
      CleanupMarkers();
      CleanupTempFolder();
      return(true);
     }

   if(marker == -1)
     {
      string err_text = ReadTextFile(m_err_rel);
      ::PrintFormat("CXlsxWriter::Write: PowerShell reported an error: %s", err_text);
      CleanupMarkers();
      return(false);
     }

//--- neither marker appeared: PowerShell itself may not have started
//--- the script body at all (e.g. DLL imports blocked at a deeper level,
//--- or antivirus intercepted the launch silently)
   ::Print("CXlsxWriter::Write: no marker appeared within 15 s. PowerShell may "
           "not be executing the script body. Confirm 'Allow DLL imports' is "
           "enabled and that no antivirus policy is blocking powershell.exe "
           "launches from MetaTrader 5.");
   return(false);
  }

//+------------------------------------------------------------------+
//| Returns the output filename for log messages in the caller.      |
//+------------------------------------------------------------------+
string CXlsxWriter::GetOutputPath(void)
  {
   return(m_output_path);
  }

Write() begins by generating a timestamp-based staging folder name and two marker filenames, all derived from the same timestamp to ensure uniqueness across concurrent runs. It resolves the absolute MQL5/Files/ path using TerminalInfoString(TERMINAL_DATA_PATH) with the trailing slash stripped. After writing all seven XML components, it constructs the PowerShell command string. The PowerShell script uses $ErrorActionPreference='Stop' so any error terminates immediately and falls into the catch block rather than continuing silently. The try branch writes OK to the done marker, and the catch branch writes the exception message to the error marker. PollForMarkers() then returns with one of three outcomes, each of which produces a specific, actionable log message rather than a generic timeout.


Implementation — TradeStatementExporter.mq5

TradeStatementExporter.mq5 is the entry-point script. It accepts five inputs, loads closed trade history for the specified range and filters, and calls CXlsxWriter to produce the output file.

Input Block

//+------------------------------------------------------------------+
//|                                      TradeStatementExporter.mq5  |
//+------------------------------------------------------------------+

#property script_show_inputs

#include <Xlsx/TradeRecord.mqh>
#include <Xlsx/TradeLoader.mqh>
#include <Xlsx/XlsxWriter.mqh>

//--- Inputs shown in the script dialog
input string   InpSymbol         = "";                     // Symbol filter (empty = all)
input long     InpMagic          = 0;                      // Magic number filter (0 = all)
input datetime InpFromDate       = 0;                      // History start date (0 = earliest)
input datetime InpToDate         = 0;                      // History end date (0 = now)
input string   InpOutputFilename = "trade_statement.xlsx"; // Output filename in MQL5/Files/

The five inputs give full control over which trades are included and where the output goes. InpSymbol defaults to an empty string, which the loader interprets as no symbol filter. InpMagic defaults to zero, meaning all magic numbers. Both date inputs default to zero, which the script resolves to the full available history range. #property script_show_inputs ensures the dialog appears when the script is run, giving the user the opportunity to adjust the inputs before execution.

OnStart()

//+------------------------------------------------------------------+
//| Script entry point: load trades and write the XLSX file.         |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- resolve date range; zero means "use the full available range"
   datetime from = (InpFromDate == 0) ? 0             : InpFromDate;
   datetime to   = (InpToDate   == 0) ? TimeCurrent() : InpToDate;

   uint t_start = GetTickCount(); // for elapsed-time logging

//--- load trade history from the terminal
   CTradeLoader loader;
   CTradeRecord trades[];

   int count = loader.Load(from, to, InpSymbol, InpMagic, trades);

   if(count == 0)
     {
      Print("TradeStatementExporter: no trades found for the given filters.");
      return;
     }

   PrintFormat("TradeStatementExporter: %d trades loaded.", count);

//--- write the XLSX file
   CXlsxWriter writer;
   bool ok = writer.Write(trades, count, InpOutputFilename);

   if(!ok)
     {
      Print("TradeStatementExporter: write failed. Check the journal for details.");
      return;
     }

   uint elapsed = GetTickCount() - t_start;
   PrintFormat("TradeStatementExporter: export complete. File: MQL5/Files/%s  (%d ms)",
               writer.GetOutputPath(), elapsed);
  }

OnStart() resolves the date range, instantiates CTradeLoader and calls Load(), and exits with a log message if no trades were found rather than producing an empty file. On a successful load it calls CXlsxWriter::Write(). If the write fails, the Experts tab already contains specific error messages from within WriteComponent() or from the PowerShell marker. The final PrintFormat() logs the output path and elapsed time so the trader knows exactly where to find the file and how long the export took.


Verification — TestXlsxBuilder.mq5

TestXlsxBuilder.mq5 exercises CXlsxBuilder directly without touching trade history or the file system. It constructs trade records by hand, calls the builder methods, and checks the output strings using the ASSERT macro.

ASSERT Macro and Setup

//+------------------------------------------------------------------+
//|                                             TestXlsxBuilder.mq5  |
//+------------------------------------------------------------------+

#include <Xlsx/TradeRecord.mqh>
#include <Xlsx/XlsxBuilder.mqh>

//--- ASSERT: prints PASSED or FAILED with the test description string
#define ASSERT(cond, msg) \
   if(!(cond)) { PrintFormat("ASSERT FAILED : %s", msg); } \
   else        { PrintFormat("ASSERT PASSED : %s", msg); }

The ASSERT macro evaluates a boolean condition and prints either ASSERT PASSED or ASSERT FAILED to the Experts log alongside the test description. Using a macro keeps the call sites compact while preserving the ability to print the test name without calling a function.

OnStart()

//+------------------------------------------------------------------+
//| Script entry point: build sample data and assert XML content.    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- profitable BUY trade; comment contains & to exercise EscapeXml
   CTradeRecord buy;
   buy.entry_ticket  = 10001;
   buy.exit_ticket   = 10002;
   buy.entry_time    = StringToTime("2024.01.15 09:00:00");
   buy.exit_time     = StringToTime("2024.01.15 14:30:00");
   buy.entry_price   = 1.08500;
   buy.exit_price    = 1.09000;
   buy.stop_loss     = 1.08200;
   buy.take_profit   = 1.09100;
   buy.profit        = 50.00;
   buy.volume        = 0.10;
   buy.symbol        = "EURUSD";
   buy.comment       = "test & entry"; // ampersand to test EscapeXml
   buy.direction     = DEAL_TYPE_BUY;
   buy.magic         = 12345;

//--- losing SELL trade with a < in the comment to test further escaping
   CTradeRecord sell;
   sell.entry_ticket = 10003;
   sell.exit_ticket  = 10004;
   sell.entry_time   = StringToTime("2024.01.16 10:00:00");
   sell.exit_time    = StringToTime("2024.01.16 11:00:00");
   sell.entry_price  = 1.09200;
   sell.exit_price   = 1.09500;
   sell.stop_loss    = 1.09400;
   sell.take_profit  = 1.08900;
   sell.profit       = -30.00;
   sell.volume       = 0.10;
   sell.symbol       = "EURUSD";
   sell.comment      = "<scalp>"; // angle brackets to test EscapeXml
   sell.direction    = DEAL_TYPE_SELL;
   sell.magic        = 12345;

   CTradeRecord trades[2];
   trades[0] = buy;
   trades[1] = sell;

   CXlsxBuilder builder;

//--- generate the sheet XML for both trades
   string sheet_xml = builder.BuildSheet(trades, 2);

//--- Test 1: BUY direction string present in sheet XML
   ASSERT(StringFind(sheet_xml, "BUY") >= 0,
          "Sheet XML contains BUY direction string");

//--- Test 2: SELL direction string present in sheet XML
   ASSERT(StringFind(sheet_xml, "SELL") >= 0,
          "Sheet XML contains SELL direction string");

//--- Test 3: ampersand in comment escaped to &amp;
   ASSERT(StringFind(sheet_xml, "&amp;") >= 0,
          "Ampersand in comment is escaped to &amp;");

//--- Test 4: raw & not present outside an entity reference
//--- Confirm < in comment is escaped to &lt; (not left as raw angle bracket)
   ASSERT(StringFind(sheet_xml, "&lt;scalp&gt;") >= 0,
          "Angle brackets in comment escaped to &lt;scalp&gt;");

//--- Test 5: date serial number for 2024.01.15 00:00:00
//--- Unix timestamp 1705276800 / 86400 = 19737 days + 25569 + 1 = 45307
   datetime test_dt  = StringToTime("2024.01.15 00:00:00");
   long     expected = 45307;
//--- rebuild with just one trade to check the entry_time serial
   CTradeRecord one_trade[1];
   one_trade[0] = buy;
   string one_sheet = builder.BuildSheet(one_trade, 1);
   ASSERT(StringFind(one_sheet, IntegerToString(expected)) >= 0,
          "Date serial 45307 present for 2024.01.15");

//--- Test 6: shared strings XML contains "Entry Time" header
   string headers[] =
     {
      "Entry Time", "Exit Time", "Symbol", "Direction",
      "Volume", "Entry Price", "Exit Price",
      "Stop Loss", "Take Profit", "Profit",
      "Duration (s)", "R-Multiple", "Comment"
     };
   string ss_xml = builder.BuildSharedStrings(headers, ArraySize(headers));
   ASSERT(StringFind(ss_xml, "Entry Time") >= 0,
          "Shared strings XML contains 'Entry Time' header");

//--- Test 7: shared strings XML contains "R-Multiple"
   ASSERT(StringFind(ss_xml, "R-Multiple") >= 0,
          "Shared strings XML contains 'R-Multiple' header");

//--- Test 8: styles XML carries numFmtId="14" for date formatting
   string styles_xml = builder.BuildStyles();
   ASSERT(StringFind(styles_xml, "numFmtId=\"14\"") >= 0,
          "Styles XML contains date numFmtId 14");

//--- Test 9: content types XML contains the worksheet override entry
   string ct_xml = builder.BuildContentTypes();
   ASSERT(StringFind(ct_xml, "spreadsheetml.worksheet") >= 0,
          "Content types XML contains worksheet content type override");

//--- Test 10: workbook XML contains the configured sheet name
   string wb_xml = builder.BuildWorkbook();
   ASSERT(StringFind(wb_xml, "Trade Report") >= 0,
          "Workbook XML contains default sheet name 'Trade Report'");

//--- Test 11: workbook rels XML maps rId1 to sheet1.xml
   string wr_xml = builder.BuildWorkbookRels();
   ASSERT(StringFind(wr_xml, "sheet1.xml") >= 0,
          "Workbook rels XML maps rId1 to worksheets/sheet1.xml");

//--- Test 12: styles XML contains the bold font declaration
   ASSERT(StringFind(styles_xml, "<b/>") >= 0,
          "Styles XML contains bold font declaration");

   Print("TestXlsxBuilder: all assertions complete. Review lines above for results.");
  }

The test constructs a BUY trade with an ampersand in the comment and a SELL trade with angle brackets in the comment, exercising the two most important escaping paths in EscapeXml(). Test 3 confirms that the ampersand was replaced with &amp;, which would otherwise produce malformed XML that Excel would refuse to open. Test 4 confirms that <scalp> was replaced with &lt;scalp&gt;, preventing the comment from being interpreted as an XML tag. Test 5 verifies the date serial arithmetic against a known correct value. Tests 6 through 12 confirm that all structural XML components contain their critical content markers.


Extending the Exporter

A second worksheet can be added to the same workbook to display aggregate statistics. BuildWorkbook() would declare a second <sheet> element, BuildWorkbookRels() would add a matching relationship entry, and a new CXlsxBuilder::BuildSummarySheet() method would compute win rate, average R-multiple, total profit, and trade count from the CTradeRecord array before writing them into a separate xl/worksheets/sheet2.xml file. The staging and ZIP steps in CXlsxWriter would include the additional file without any other changes to the pipeline.

Conditional formatting can be applied inside xl/worksheets/sheet1.xml by adding a <conditionalFormatting> element after the <sheetData> element. A rule with type="cellIs" and operator="greaterThan" on the profit column would instruct Excel to fill profitable rows with a green background, and a matching rule with operator="lessThan" would fill losing rows with red. The fill styles referenced by these rules must be declared in xl/styles.xml as <dxf> differential format entries inside a <dxfs> section, which requires extending BuildStyles().

Scheduling the export to run at the end of each trading day would require converting the script into an EA and using EventSetTimer() to trigger OnTimer() at the market close time. The OnTimer() handler would instantiate CTradeLoader and CXlsxWriter and call them exactly as OnStart() does now, with InpFromDate set to the start of the current day and InpToDate set to the current server time. The EA could also send a WebRequest notification to a monitoring endpoint after a successful export.


Limitations

The ShellExecuteW() approach requires DLL imports to be enabled in the terminal settings under Tools > Options > Expert Advisors > Allow DLL imports. Without this setting, ShellExecuteW returns a result of 5 or less and the write fails immediately. On Linux or Wine environments running MetaTrader 5, shell32.dll may not be present, and the #import will fail at compile time or produce errors at runtime.

Each export run regenerates the XLSX file from scratch. The script reads all trades within the date range, builds the full XML for all of them, and overwrites any existing output file. There is no incremental write mode that appends new trades to an existing file.

The SpreadsheetML implementation covers the minimum viable file set. Features such as charts, pivot tables, named ranges, data validation, and freeze panes require additional XML elements and in some cases additional parts inside the archive.

Very large trade histories with tens of thousands of trades may cause the XML string built in BuildSheet() to be slow to assemble in MQL5. String concatenation in MQL5 is not buffered. For histories of that size, writing the sheet XML incrementally to a file using repeated FileWriteString() calls rather than assembling a single return string would significantly reduce peak memory use and construction time.


Conclusion

Building this script leaves the reader with four working components. CTradeRecord holds every field describing a single completed trade along with computed methods for R-multiple, duration, and pip profit. CTradeLoader reads the MetaTrader 5 deal history API, pairs entry and exit deals by their shared position ID, and resolves stop-loss and take-profit values using the two-pass fallback to the originating order record. CXlsxBuilder generates all seven XML components required for a valid XLSX file, applying correct cell types, date serial number conversion, XML escaping for all five hazardous characters, and the minimal style set needed for bold headers and formatted date columns. CXlsxWriter orchestrates the complete pipeline: staging the XML files in a timestamped temp folder, invoking PowerShell's .NET ZipFile.CreateFromDirectory through a direct ShellExecuteW call, polling for a done or error marker file that the PowerShell script writes on completion, reading back any exception text on failure, and cleaning up the staging files.

The concrete operational guarantees are these: all trades within the requested date range that pass the symbol and magic filters are included. The file opens directly in Excel and Google Sheets without a conversion step. Price and profit columns are stored as numbers, date columns as serial numbers with a date format applied, and the header row is bold and centered. The script logs the trade count, each intermediate path, the done-marker confirmation, and the total elapsed time on completion. The honest limitations are the dependency on DLL imports and PowerShell being available on Windows, the absence of incremental write support, and the linear growth in XML string construction time for very large trade histories.


Programs used in the article:

# Name Type Description
1 TradeRecord.mqh Include File Defines the struct that holds all fields and computed metrics for a single closed trade.
2 TradeLoader.mqh Include File Reads MetaTrader 5 deal history, reconstructs complete trades by pairing entry and exit deals, and resolves SL/TP via two-pass lookup.
3 XlsxBuilder.mqh Include File Generates the seven XML component strings required for a valid XLSX file, including the worksheet rows, shared strings, styles, and structural relationship files.
4 XlsxWriter.mqh Include File Writes the XML components to a staging folder, invokes PowerShell's .NET ZipFile class via ShellExecuteW, polls for a success or error marker, and cleans up the temporary files.
5 TradeStatementExporter.mq5 Script Accepts user inputs for date range, symbol, magic number, and output filename, loads trade history, and calls the writer to produce the XLSX file in MQL5/Files/.
6 TestXlsxBuilder.mq5 Script Constructs sample trade records manually and asserts that the XML builder produces correct direction strings, escaped characters, date serial numbers, column headers, and style identifiers.
7 Xlsx_Builder.zip Zip Archive Zip archive containing all the attached files and their paths relative to the terminal's root folder.
Attached files |
TradeRecord.mqh (3.41 KB)
TradeLoader.mqh (6.71 KB)
XlsxBuilder.mqh (16.43 KB)
XlsxWriter.mqh (12.86 KB)
Xlsx_Builder.zip (13.9 KB)
Building Your Personal Expert Advisor (Part 1): From Fragile Script to Working EA Building Your Personal Expert Advisor (Part 1): From Fragile Script to Working EA
This article focuses on EA architecture rather than signal design. Starting with a flawed Moving Average crossover EA, we add new‑bar detection to prevent duplicate entries, Magic Number and position awareness, ATR‑based risk levels, and data and trade result validation, along with basic safeguards. You obtain a practical base to build and test advanced systems.
Bayesian Online Change-Point Detection (BOCPD) in MQL5: One Regime-Break Signal, Three Ways to Use It Bayesian Online Change-Point Detection (BOCPD) in MQL5: One Regime-Break Signal, Three Ways to Use It
This article delivers Bayesian Online Change-Point Detection as a single, dependency-free MQL5 class that maintains a per-bar, causal probability of a regime break. We use it three ways: a live monitor, a moving average that flushes on breaks, and a risk overlay with a matched-frequency random control. Readers get a reusable primitive to watch structural change, adapt indicators, and gate exposure after detected shifts.
The Blue Monkey (BM) Algorithm The Blue Monkey (BM) Algorithm
The article presents an implementation of the Blue Monkey metaheuristic algorithm, which is based on a model of the social behavior of blue monkeys. The article examines the key mechanisms of the algorithm — the group structure of the population, following local leaders, and generational renewal through the replacement of the worst adults with the best offspring — and analyzes the test results.
Creating a Probabilistic Market-Neutral Trading Robot Based on a Return Distribution Creating a Probabilistic Market-Neutral Trading Robot Based on a Return Distribution
A market-neutral trading strategy based on the empirical return distribution offers an alternative to traditional technical analysis methods, replacing price direction forecasting with the statistical placement of orders at levels the price is likely to reach. This article provides a detailed analysis of the mathematical framework for calculating percentiles, algorithms for weighting position sizes based on the probability of an order being triggered, and mechanisms for adapting to changing market conditions through grid expiration. A complete implementation in MQL5 is provided.