preview
Engineering Trading Discipline into Code (Part 9): Building a Trade Activity Audit Trail in MQL5

Engineering Trading Discipline into Code (Part 9): Building a Trade Activity Audit Trail in MQL5

MetaTrader 5 — Examples |
207 0
Christian Benjamin
Christian Benjamin

Contents


Introduction

Part 8 completed a trade-authorization layer for a cooperating strategy. The strategy described its setup state, checked the active session, measured signal freshness, respected the global lock, and called the authorization function before placing a trade. That arrangement works because the strategy agrees to ask before calling OrderSend().

The trading account has a wider boundary. A trader can place an order from the desktop terminal, a mobile device, or the web terminal. Another Expert Advisor may use a different authorization system or none at all. A pending order may later execute because price reaches its trigger. A stop loss or take profit can close a position without the original entry program initiating a new market decision.

Those events are visible to the account even when they do not pass through the Part 8 strategy. They require a different component.

An account statement records the financial outcome. It does not always show the sequence that produced it. During a later review, the origin of an order may not be immediately clear. It may have come from the desktop terminal, a mobile device, an Expert Advisor, or an automatic protective level. Terminal messages help during operation, but they are not arranged as a persistent activity journal.

This matters even more when multiple terminals, clients, or programs share one account. Without a common record, distinguishing intentional, accidental, and system-generated positions depends on memory and scattered terminal information. The problem is therefore not simply whether a trade exists. It is whether the trader can reconstruct how it entered the account and connect it to the other stages of the same operation.

Part 9 develops a Trade Activity Auditor. It is attached to one chart, but its transaction callback observes the trading account rather than just the chart symbol. It records supported live transaction stages and retains available references for correlating requests, orders, deals, and positions. These references do not independently identify a person, originating terminal, or particular EA instance.

The auditor does not judge the quality of the trading idea and does not interfere with an open position. Its role is to retain available transaction fields, observation time, reported reason categories, request results when supplied, and references to related trading objects. This gives traders a clearer activity history and gives developers a reproducible trail when transaction behavior must be investigated.

An Expert Advisor cannot receive callbacks while detached or while the terminal is closed. After restart, the auditor scans the configured history window and compares historical object identities with successfully persisted journal rows. It appends historical orders and deals not already represented by the relevant recorded object types. Callback records are marked LIVE; history-scan records are marked RECOVERED.


Defining the audit boundary

Keeping the auditor independent

The auditor belongs to the discipline series, but it does not include DisciplineLayer.mqh, DisciplineGuardian.mqh, or the example strategy from Part 8. It compiles and runs independently.

Responsibilities are divided as follows:

  • the authorization layer decides whether a cooperating EA may send its own request;
  • the activity auditor records supported notifications delivered to its terminal while running;
  • account history supplies available historical orders and deals within the selected recovery interval.

Live and recovered records

LIVE describes an event received through OnTradeTransaction() while the auditor was running. It does not mean that the position remains open or that the trade was authorized.

RECOVERED describes an order or deal discovered through a later history scan. It retains available historical object properties, not the original request/result context or missing intermediate notifications. An order still active at scan time is outside this history-only recovery path. This implementation applies symbol-policy classification only to live entry and reversal deals; retrospective classification would require a separately defined procedure and the effective historical policy.

Figure 1. Live capture and history recovery.

What is worth saving

The journal saves requests, order changes, deals, position changes, history transitions, recovered orders and deals, and optional symbol-policy violations. It does not save ticks, timer calls, or panel redraws. The stages created by one market order remain separate because they describe different account changes.

A market order shows why this distinction matters. Notifications can include order addition, execution deal, order removal, and entry into order history. Their arrival order is not guaranteed. A deal can change a position without producing a separate TRADE_TRANSACTION_POSITION notification, which concerns position changes unrelated to deal execution. These callbacks can arrive within the same second and may share an order or position reference, but they describe different stages. A deal records an execution, including a possible partial fill. Entry into order history can also represent cancellation without execution.

Collapsing the sequence into one row would hide useful evidence. Repeated copies of the same callback, however, would inflate the totals. The journal retains distinct supported stages and excludes timer refreshes. A bounded session cache filters repeated DEAL_ADD observations by deal ticket after successful persistence. Updates, position notifications, and requests remain separate observations; their row counts are not guaranteed to equal unique user actions.


Designing the auditor

Project structure

The project uses one Expert Advisor and four include files:

Figure 2. Project structure of the Trade Activity Auditor EA and its supporting include files.

TradeAuditTypes.mqh defines the event vocabulary and one complete journal row. TradeAuditJournal.mqh owns CSV reading, writing, period summaries, and exports. TradeActivityAuditor.mqh translates terminal transactions and history records into the common structure. TradeAuditPanel.mqh owns the chart objects and period controls. The EA creates these objects and forwards terminal events.

Each module has one responsibility: the auditor builds the records, the journal stores them, and the panel presents the selected results.

Figure 3 shows both capture paths. Each produces STradeAuditRecord, and the panel reads its totals from the journal.

Figure 3. Audit capture, storage, and review flow.

Event names describe account changes

The event enumeration follows the transaction model rather than using a generic TRADE label:

//+------------------------------------------------------------------+
//| Defines the activity types recorded by the auditor               |
//+------------------------------------------------------------------+
enum ENUM_AUDIT_ACTIVITY
  {
   AUDIT_REQUEST = 0,
   AUDIT_ORDER_ADDED,
   AUDIT_ORDER_UPDATED,
   AUDIT_ORDER_REMOVED,
   AUDIT_DEAL_ADDED,
   AUDIT_POSITION_CHANGED,
   AUDIT_HISTORY_ORDER_ADDED,
   AUDIT_HISTORY_ORDER_UPDATED,
   AUDIT_HISTORY_ORDER_REMOVED,
   AUDIT_DEAL_RECOVERED,
   AUDIT_ORDER_RECOVERED,
   AUDIT_DEAL_UPDATED,
   AUDIT_DEAL_REMOVED,
   AUDIT_POSTTRADE_VIOLATION
  };

The live history events should not be confused with startup recovery. HISTORY_ORDER_ADDED describes a live callback reporting that an order entered account history. ORDER_RECOVERED describes an order discovered by the later reconciliation scan.

One record for every output path

STradeAuditRecord carries fields used by the main journal, exported reports, panel summaries, and future analysis:

//+------------------------------------------------------------------+
//| Stores the complete data associated with one audit event         |
//+------------------------------------------------------------------+
struct STradeAuditRecord
  {
   datetime             recordTime;
   datetime             eventTime;
   ENUM_AUDIT_ACTIVITY  eventType;
   string               captureMode;
   long                 accountLogin;
   string               server;
   string               symbol;
   string               source;
   string               action;
   string               entryType;
   double               volume;
   double               price;
   double               stopLoss;
   double               takeProfit;
   long                 magic;
   ulong                orderTicket;
   ulong                dealTicket;
   ulong                positionId;
   long                 resultCode;
   string               resultMessage;
   double               profit;
   double               commission;
   double               swap;
   string               note;
   string               requestAction;
   ulong                requestId;
   ulong                positionTicket;
   ulong                requestOrder;
   double               requestedVolume;
   double               requestedPrice;
   double               requestedSL;
   double               requestedTP;
  };

recordTime is assigned using TimeCurrent() when a row is written. It represents the terminal's last known server quote time rather than an exact local persistence timestamp. Live eventTime starts with observation time; deal additions use the available execution time. Recovered deals use historical deal time, while recovered orders use completion time with setup time as a fallback.

Sorting by eventTime provides a historical view, but second-level timestamps and unordered callback arrival prevent exact sequence reconstruction. recordTime and captureMode distinguish immediate observations from later history scans.

Order, deal, and position references also remain separate. A deal ticket identifies one execution. Its related order ticket identifies the instruction that produced it. positionId holds a persistent position identifier resolved from position, order, or deal properties. positionTicket separately retains the callback or request position ticket. An unresolved identifier remains zero instead of being replaced silently by the ticket.

Some fields are naturally empty for certain events. A pending-order update has no deal profit. A deal created by a take profit may not carry request fields inside the callback. Empty strings and zero identifiers indicate unavailable references where the event does not supply them. Numeric fields must still be interpreted according to event type: a default zero does not by itself prove that a measurement was available.

accountLogin and server anchor each row to a specific trading environment. This is important when reviewing exports from multiple terminals because ticket numbers alone are not reliable account identifiers. The journal and state filenames also include account login and a filename-safe server label. Existing journal rows and state metadata are checked against the current account/server. A held lock handle coordinates ownership among instances using the same journal path, and an account change stops capture until the auditor is reattached.

Input_AllowedSymbols is empty by default, leaving the auditor in observation mode. When a list is supplied, a live entry outside it receives an additional POSTTRADE_VIOLATION record; no trade is closed.


MQL5 implementation

The implementation is divided according to responsibility rather than transaction type. TradeAuditTypes.mqh defines the record exchanged by every component. TradeAuditJournal.mqh stores and reads those records. TradeActivityAuditor.mqh creates them from live callbacks and account history. TradeAuditPanel.mqh presents journal summaries, while TradeActivityAuditorEA.mq5 connects the classes to MetaTrader events.

The main call path is short:

Figure 4. Main call path from transaction capture to panel update.

Recovery builds the same record structure from available historical objects. The shared storage layout does not make LIVE callback rows and RECOVERED object rows equivalent units of trading activity.

1. TradeAuditTypes.mqh: defining the shared record

TradeAuditTypes.mqh is the lowest-level include. It has no knowledge of files, chart objects, or terminal callbacks. Its purpose is to define the event vocabulary and STradeAuditRecord, which acts as the contract between the other modules.

The enumeration and structure shown earlier give every activity a stable name and every journal row the same layout. The remaining function converts an enumeration value into the text stored in CSV and displayed on the panel:

//+------------------------------------------------------------------+
//| Returns the journal name assigned to one activity event          |
//+------------------------------------------------------------------+
string AuditActivityText(const ENUM_AUDIT_ACTIVITY eventType)
  {
   switch(eventType)
     {
      case AUDIT_REQUEST:
         return("TRADE_REQUEST");
      case AUDIT_ORDER_ADDED:
         return("ORDER_ADDED");
      case AUDIT_ORDER_UPDATED:
         return("ORDER_UPDATED");
      case AUDIT_ORDER_REMOVED:
         return("ORDER_REMOVED");
      case AUDIT_DEAL_ADDED:
         return("DEAL_ADDED");
      case AUDIT_POSITION_CHANGED:
         return("POSITION_CHANGED");
      case AUDIT_HISTORY_ORDER_ADDED:
         return("HISTORY_ORDER_ADDED");
      case AUDIT_HISTORY_ORDER_UPDATED:
         return("HISTORY_ORDER_UPDATED");
      case AUDIT_HISTORY_ORDER_REMOVED:
         return("HISTORY_ORDER_REMOVED");
      case AUDIT_DEAL_RECOVERED:
         return("DEAL_RECOVERED");
      case AUDIT_ORDER_RECOVERED:
         return("ORDER_RECOVERED");
      case AUDIT_DEAL_UPDATED:
         return("DEAL_UPDATED");
      case AUDIT_DEAL_REMOVED:
         return("DEAL_REMOVED");
      case AUDIT_POSTTRADE_VIOLATION:
         return("POSTTRADE_VIOLATION");
     }
   return("UNKNOWN");
  }

Keeping this conversion beside the enumeration prevents the journal and panel from maintaining separate naming tables. Adding a new activity begins in this file; the capture logic can then assign the value without changing how the name is resolved elsewhere.

2. TradeAuditJournal.mqh: owning persistence and queries

TradeAuditJournal.mqh receives completed records. It does not inspect terminal orders or deals and does not decide whether an event is a violation. Its public methods expose the storage operations needed by the auditor and panel:

//+------------------------------------------------------------------+
//| Initializes journal ownership state                              |
//+------------------------------------------------------------------+
class CTradeAuditJournal
  {
private:
   string               m_fileName, m_error;
   int                  m_lock;
   long                 m_login;
   string               m_server;
   bool                 ReadFields(int handle, string &values[]);
   bool                 Header(int handle, bool create);
   bool                 ReadRow(int handle, STradeAuditRecord &record);
   bool                 WriteRow(int handle, STradeAuditRecord &record);
   datetime             PeriodStart(int days);
public:

//+------------------------------------------------------------------+
//| Initializes journal ownership state                              |
//+------------------------------------------------------------------+
   CTradeAuditJournal()
     {
      m_lock = INVALID_HANDLE;
     }

//+------------------------------------------------------------------+
//| Releases the journal ownership handle                            |
//+------------------------------------------------------------------+
   ~CTradeAuditJournal()
     {
      if(m_lock != INVALID_HANDLE)
         FileClose(m_lock);
     }

   bool                 Initialize(string fileName);
   bool                 Append(STradeAuditRecord &record);
   bool                 GetSummary(int days,
                                   int &events,
                                   int &violations,
                                   int &liveEvents,
                                   int &recoveredEvents,
                                   STradeAuditRecord &recent[]);
   bool                 ExportPeriod(int days, string &savedAs);
   bool                 RecordedObjects(ulong &orders[], ulong &deals[]);

//+------------------------------------------------------------------+
//| Returns the current journal error                                |
//+------------------------------------------------------------------+
   string Error() const
     {
      return(m_error);
     }

  };

Initialize() obtains ownership of the account-bound journal, creates a header only for an empty file, and validates all 32 column names and their order for an existing file. It also validates existing rows and their account identity before capture begins. An incompatible or malformed file is rejected rather than silently migrated.

//+------------------------------------------------------------------+
//| Initializes account-bound storage and settings                   |
//+------------------------------------------------------------------+
bool CTradeAuditJournal::Initialize(string fileName)
  {
   m_error = "";
   m_fileName = fileName;
   m_login = AccountInfoInteger(ACCOUNT_LOGIN);
   m_server = AccountInfoString(ACCOUNT_SERVER);
   FolderCreate("TradeAudit");
   m_lock = FileOpen(m_fileName + ".lock", FILE_READ | FILE_WRITE | FILE_BIN);
   if(m_lock == INVALID_HANDLE)
     {
      m_error = "Journal already owned or unavailable";
      return(false);
     }
   int h = FileOpen(m_fileName,
                  FILE_READ | FILE_WRITE | FILE_CSV | FILE_ANSI | FILE_SHARE_READ,
                  ';',
                  CP_UTF8);
   if(h == INVALID_HANDLE)
     {
      m_error = "Journal open error";
      return(false);
     }
   bool ok = Header(h, true);
   STradeAuditRecord row;
   while(ok && !FileIsEnding(h))
      ok = ReadRow(h, row);
   FileClose(h);
   return(ok);
  }

Append() opens the journal, checks the seek and row-write results, flushes, and closes it. FILE_SHARE_READ permits compatible readers but cannot override another process that denies writing. A reported storage error remains visible and blocks further capture until the problem is resolved and the EA is reattached. A flush does not guarantee protection against every storage or power failure.

//+------------------------------------------------------------------+
//| Appends and flushes one audit record                             |
//+------------------------------------------------------------------+
bool CTradeAuditJournal::Append(STradeAuditRecord &record)
  {
   if(record.accountLogin != m_login || record.server != m_server)
     {
      m_error = "Account identity mismatch";
      return(false);
     }
   if(m_error != "")
      return(false);
   int h = FileOpen(m_fileName,
                  FILE_READ | FILE_WRITE | FILE_CSV | FILE_ANSI | FILE_SHARE_READ,
                  ';',
                  CP_UTF8);
   if(h == INVALID_HANDLE)
     {
      m_error = "Journal open error";
      return(false);
     }
   bool ok = FileSeek(h, 0, SEEK_END) && WriteRow(h, record);
   ResetLastError();
   FileFlush(h);
   if(GetLastError() != 0)
      ok = false;
   FileClose(h);
   if(!ok)
      m_error = "Journal write error";
   return(ok);
  }

GetSummary() reads validated rows for the selected eventTime period and selects up to three eligible records in descending eventTime order. Failed reads produce an explicit error and unavailable totals. ExportPeriod() uses the same time basis and creates a separate uniquely named CSV. Text is UTF-8; reserved delimiters, quotes, percent signs, and line breaks in string fields are reversibly percent-escaped so each record occupies one physical line. Numeric values are serialized with ten decimal places. The journal remains externally editable and is not a tamper-evident record.

3. TradeActivityAuditor.mqh: translating account activity

TradeActivityAuditor.mqh contains the account-facing logic. It owns the journal, account identity, state-file path, optional symbol list, recent-key cache, and current status. Its interface exposes initialization, live capture, and history recovery to the EA:

//+------------------------------------------------------------------+
//| Coordinates transaction capture and history recovery             |
//+------------------------------------------------------------------+
class CTradeActivityAuditor
  {
private:
   CTradeAuditJournal   m_journal;
   string               m_stateFile;
   string               m_allowedSymbols[];
   int                  m_recoveryDays;
   string               m_recentKeys[];
   string               m_status;
   long                 m_login;
   string               m_server;
   bool                 m_fault;
   bool                 LoadState();
   bool                 SaveState();
   bool                 IsDuplicate(string key) const;
   void                 Remember(string key);
   bool                 SameAccount() const;
   bool                 IsSymbolAllowed(string symbol) const;
   string               DealSource(long reason) const;
   string               OrderSource(long reason) const;
   bool                 CompleteDealRecord(ulong ticket, STradeAuditRecord &record) const;
   bool                 CompleteOrderRecord(ulong ticket, STradeAuditRecord &record) const;
   bool                 CompleteActiveOrder(ulong ticket, STradeAuditRecord &record) const;
   bool                 Save(STradeAuditRecord &record);
   void                 CheckPolicy(STradeAuditRecord &record);
public:
   bool                 Initialize(string journalFile, string stateFile, string allowedSymbols, int recoveryDays);
   void                 Capture(const MqlTradeTransaction &trans,
                                const MqlTradeRequest &request,
                                const MqlTradeResult &result);
   void                 Recover();
   CTradeAuditJournal   * Journal();
   string               Status() const;
  };

Initialization parses the symbol list, derives account-bound filenames, opens the journal, and checks account identity metadata. Recovery identities are rebuilt from validated journal rows, so the state file is diagnostic metadata rather than an authoritative ticket cutoff. Journal() provides access to summaries and exports.

Building a live record

Capture() branches on transaction type before reading type-dependent fields. It uses request and result only for TRADE_TRANSACTION_REQUEST. The session cache applies only to DEAL_ADD observations and is updated after a successful journal append. Other stages, including identical-looking requests and updates, are retained without heuristic merging.

//+------------------------------------------------------------------+
//| Rejects a repeated live transaction key                          |
//+------------------------------------------------------------------+
bool CTradeActivityAuditor::IsDuplicate(string key) const
  {
   for(int i = 0; i < ArraySize(m_recentKeys); i++)
      if(m_recentKeys[i] == key)
         return(true);
   return(false);
  }

IsDuplicate() checks the cached keys without changing them. Remember() inserts a key only after its deal-addition row has been appended successfully. The cache retains at most 256 keys for the current session.

//+------------------------------------------------------------------+
//| Caches a successfully persisted deal addition                    |
//+------------------------------------------------------------------+
void CTradeActivityAuditor::Remember(string key)
  {
   int n = ArraySize(m_recentKeys);
   if(n < 256)
     {
      if(ArrayResize(m_recentKeys, n + 1) != n + 1)
         return;
      m_recentKeys[n] = key;
     }
   else
     {
      for(int i = 1; i < n; i++)
         m_recentKeys[i - 1] = m_recentKeys[i];
      m_recentKeys[n - 1] = key;
     }
  }

Request rows retain requestAction, requestId, requestOrder, positionTicket, requestedVolume, requestedPrice, requestedSL, and requestedTP independently of execution enrichment. action holds an applicable order type for placement requests and the request operation for other actions. Returned objects can supply execution properties and reason categories without replacing the dedicated requested-value fields:

//+------------------------------------------------------------------+
//| Converts one live transaction callback into an audit record      |
//+------------------------------------------------------------------+
void CTradeActivityAuditor::Capture(const MqlTradeTransaction &trans,
               const MqlTradeRequest &request,
               const MqlTradeResult &result)
  {
   if(!SameAccount())
     {
      m_fault = true;
      m_status = "Account changed - reattach";
      return;
     }
   if(m_fault)
      return;
   STradeAuditRecord r;
   ZeroMemory(r);
   r.eventTime = TimeCurrent();
   r.captureMode = "LIVE";
   r.source = "UNRESOLVED";
   r.note = "build=P9-2";
   string key = "";
   if(trans.type == TRADE_TRANSACTION_REQUEST)
     {
      r.eventType = AUDIT_REQUEST;
      r.requestAction = EnumToString(request.action);
      r.requestId = result.request_id;
      r.requestOrder = request.order;
      r.positionTicket = request.position;
      r.requestedVolume = request.volume;
      r.requestedPrice = request.price;
      r.requestedSL = request.sl;
      r.requestedTP = request.tp;
      r.symbol = request.symbol;
      r.volume = request.volume;
      r.price = request.price;
      r.stopLoss = request.sl;
      r.takeProfit = request.tp;
      r.magic = (long) request.magic;
      r.orderTicket = result.order > 0 ? result.order : request.order;
      r.dealTicket = result.deal;
      r.resultCode = result.retcode;
      r.resultMessage = result.comment;
      if(result.deal > 0)
         CompleteDealRecord(result.deal, r);
      else
         if(result.order > 0)
           {
            if(!CompleteActiveOrder(result.order, r))
               CompleteOrderRecord(result.order, r);
           }
      r.action = (request.action == TRADE_ACTION_DEAL ||
         request.action == TRADE_ACTION_PENDING) ? EnumToString(request.type) : r.requestAction;
      if(request.position > 0 && PositionSelectByTicket(request.position))
         r.positionId = (ulong) PositionGetInteger(POSITION_IDENTIFIER);
      //--- Request observations are retained; similar requests can be deliberate.
     }
   else
     {
      r.symbol = trans.symbol;
      if(trans.type == TRADE_TRANSACTION_ORDER_ADD ||
         trans.type == TRADE_TRANSACTION_ORDER_UPDATE ||
         trans.type == TRADE_TRANSACTION_ORDER_DELETE ||
         trans.type == TRADE_TRANSACTION_HISTORY_ADD ||
         trans.type == TRADE_TRANSACTION_HISTORY_UPDATE ||
         trans.type == TRADE_TRANSACTION_HISTORY_DELETE)
        {
         r.orderTicket = trans.order;
         r.volume = trans.volume;
         r.price = trans.price;
         r.stopLoss = trans.price_sl;
         r.takeProfit = trans.price_tp;
         r.note += "; state=" + EnumToString(trans.order_state) + "; expiration=" + TimeToString(trans.time_expiration,
                        TIME_DATE | TIME_SECONDS);
        }
      else
         if(trans.type == TRADE_TRANSACTION_DEAL_ADD ||
            trans.type == TRADE_TRANSACTION_DEAL_UPDATE ||
            trans.type == TRADE_TRANSACTION_DEAL_DELETE)
           {
            r.orderTicket = trans.order;
            r.dealTicket = trans.deal;
            r.positionTicket = trans.position;
            r.volume = trans.volume;
            r.price = trans.price;
            r.stopLoss = trans.price_sl;
            r.takeProfit = trans.price_tp;
           }
         else
            if(trans.type == TRADE_TRANSACTION_POSITION)
              {
               r.positionTicket = trans.position;
               r.volume = trans.volume;
               r.price = trans.price;
               r.stopLoss = trans.price_sl;
               r.takeProfit = trans.price_tp;
              }
      if(r.positionTicket > 0 && PositionSelectByTicket(r.positionTicket))
         r.positionId = (ulong) PositionGetInteger(POSITION_IDENTIFIER);
      switch(trans.type)
        {
         case TRADE_TRANSACTION_ORDER_ADD:
            r.eventType = AUDIT_ORDER_ADDED;
            r.action = EnumToString(trans.order_type);
            CompleteActiveOrder(trans.order, r);
            break;
         case TRADE_TRANSACTION_ORDER_UPDATE:
            r.eventType = AUDIT_ORDER_UPDATED;
            r.action = EnumToString(trans.order_type);
            CompleteActiveOrder(trans.order, r);
            break;
         case TRADE_TRANSACTION_ORDER_DELETE:
            r.eventType = AUDIT_ORDER_REMOVED;
            r.action = EnumToString(trans.order_type);
            CompleteOrderRecord(trans.order, r);
            break;
         case TRADE_TRANSACTION_DEAL_ADD:
            r.eventType = AUDIT_DEAL_ADDED;
            r.action = EnumToString(trans.deal_type);
            CompleteDealRecord(trans.deal, r);
            break;
         case TRADE_TRANSACTION_DEAL_UPDATE:
            r.eventType = AUDIT_DEAL_UPDATED;
            r.action = EnumToString(trans.deal_type);
            CompleteDealRecord(trans.deal, r);
            break;
         case TRADE_TRANSACTION_DEAL_DELETE:
            r.eventType = AUDIT_DEAL_REMOVED;
            r.action = "DEAL_DELETED";
            break;
         case TRADE_TRANSACTION_POSITION:
            r.eventType = AUDIT_POSITION_CHANGED;
            r.action = "POSITION_UPDATE";
            break;
         case TRADE_TRANSACTION_HISTORY_ADD:
            r.eventType = AUDIT_HISTORY_ORDER_ADDED;
            CompleteOrderRecord(trans.order, r);
            break;
         case TRADE_TRANSACTION_HISTORY_UPDATE:
            r.eventType = AUDIT_HISTORY_ORDER_UPDATED;
            CompleteOrderRecord(trans.order, r);
            break;
         case TRADE_TRANSACTION_HISTORY_DELETE:
            r.eventType = AUDIT_HISTORY_ORDER_REMOVED;
            r.action = "HISTORY_ORDER_DELETED";
            break;
         default:
            return;
        }
      //--- Cache only durable object additions. Repeated updates remain observable.
      if(trans.type == TRADE_TRANSACTION_DEAL_ADD && trans.deal > 0)
         key = "DEAL_ADD|" + (string) trans.deal;
      if(key != "" && IsDuplicate(key))
         return;
     }
   if(!Save(r))
      return;
   if(key != "")
      Remember(key);
   CheckPolicy(r);
   if(!m_fault)
      m_status = "Live";
  }

Order callbacks call either CompleteActiveOrder() or CompleteOrderRecord(). A new deal calls CompleteDealRecord(), which adds the execution time, deal reason, entry type, related order, position identifier, profit, commission, and swap. The same completion methods are reused by recovery, keeping field population consistent across both capture modes.

Resolving the source

DealSource() and OrderSource() map MetaTrader reason values to the journal vocabulary. The two functions remain separate because MQL5 exposes different enumerations for orders and deals, even though the returned labels are similar.

//+------------------------------------------------------------------+
//| Returns the source stored with a historical deal                 |
//+------------------------------------------------------------------+
string CTradeActivityAuditor::DealSource(long reason) const
  {
   switch((ENUM_DEAL_REASON)reason)
     {
      case DEAL_REASON_CLIENT:
         return("DESKTOP");
      case DEAL_REASON_MOBILE:
         return("MOBILE");
      case DEAL_REASON_WEB:
         return("WEB");
      case DEAL_REASON_EXPERT:
         return("EXPERT");
      case DEAL_REASON_SL:
         return("STOP_LOSS");
      case DEAL_REASON_TP:
         return("TAKE_PROFIT");
      case DEAL_REASON_SO:
         return("STOP_OUT");
     }
   return("ACCOUNT");
  }

The order version uses ENUM_ORDER_REASON and the corresponding constants. ACCOUNT denotes an unmapped reason; UNRESOLVED denotes missing enrichment. These labels do not authenticate an actor. Desktop/mobile/web/Expert Advisor categories describe origin, while SL/TP/stop-out categories describe execution causes.

Saving and recovering records

Save() assigns observation time and account identity, verifies that the terminal account still matches, and returns the append result. Failure sets a persistent fault instead of allowing a later success status to conceal it:

//+------------------------------------------------------------------+
//| Adds account identity and writes one record                      |
//+------------------------------------------------------------------+
bool CTradeActivityAuditor::Save(STradeAuditRecord &record)
  {
   if(!SameAccount())
     {
      m_status = "Account changed - reattach";
      m_fault = true;
      return(false);
     }

   record.recordTime = TimeCurrent();
   record.accountLogin = m_login;
   record.server = m_server;

   if(!m_journal.Append(record))
     {
      m_status = "Journal error";
      m_fault = true;
      Print("[AUDIT] ", m_journal.Error(),
            "; reattach after resolving storage");
      return(false);
     }

   return(true);
  }

Recover() rebuilds sorted sets of recorded historical order/deal identities from the journal and selects the configured lookback window on every scan. It snapshots both candidate ticket lists before enrichment, preventing selection helpers from invalidating enumeration. Each historical object absent from its recorded set is enriched, marked RECOVERED, and passed to Save(). Ticket magnitude is not used as a chronological cutoff.

//+------------------------------------------------------------------+
//| Reconciles historical objects against persisted identities       |
//+------------------------------------------------------------------+
void CTradeActivityAuditor::Recover()
  {
   if(!SameAccount())
     {
      m_fault = true;
      m_status = "Account changed - reattach";
      return;
     }

   if(m_fault || m_journal.Error() != "")
      return;

   m_status = "Recovering";

   ulong knownOrders[], knownDeals[];
   if(!m_journal.RecordedObjects(knownOrders, knownDeals))
     {
      m_status = "Journal error";
      m_fault = true;
      return;
     }

   datetime from = TimeCurrent() - m_recoveryDays * 86400;
   if(!HistorySelect(from, TimeCurrent()))
     {
      m_status = "History error";
      return;
     }

   int no = HistoryOrdersTotal(), nd = HistoryDealsTotal();
   ulong orders[], deals[];

   if(ArrayResize(orders, no) != no ||
      ArrayResize(deals, nd) != nd)
     {
      m_status = "Allocation error";
      return;
     }

//--- Snapshot both lists before enrichment changes history selection
   for(int i = 0; i < no; i++)
     {
      orders[i] = HistoryOrderGetTicket(i);
      if(orders[i] == 0)
        {
         m_status = "History error";
         return;
        }
     }

   for(int i = 0; i < nd; i++)
     {
      deals[i] = HistoryDealGetTicket(i);
      if(deals[i] == 0)
        {
         m_status = "History error";
         return;
        }
     }

   bool incomplete = false;

//--- Recover historical orders not already represented
   for(int i = 0; i < no; i++)
     {
      int k = ArraySize(knownOrders) > 0
              ? ArrayBsearch(knownOrders, orders[i]) : -1;

      if(k >= 0 && k < ArraySize(knownOrders) &&
         knownOrders[k] == orders[i])
         continue;

      STradeAuditRecord r;
      ZeroMemory(r);
      r.eventType = AUDIT_ORDER_RECOVERED;
      r.captureMode = "RECOVERED";
      r.note = "build=P9-2; historical object discovered by scan";

      if(!CompleteOrderRecord(orders[i], r))
        {
         incomplete = true;
         continue;
        }

      if(!Save(r))
         return;
     }

//--- Recover historical deals not already represented
   for(int i = 0; i < nd; i++)
     {
      int k = ArraySize(knownDeals) > 0
              ? ArrayBsearch(knownDeals, deals[i]) : -1;

      if(k >= 0 && k < ArraySize(knownDeals) &&
         knownDeals[k] == deals[i])
         continue;

      STradeAuditRecord r;
      ZeroMemory(r);
      r.eventType = AUDIT_DEAL_RECOVERED;
      r.captureMode = "RECOVERED";
      r.note = "build=P9-2; original request context unavailable";

      if(!CompleteDealRecord(deals[i], r))
        {
         incomplete = true;
         continue;
        }

      if(!Save(r))
         return;
     }

//--- Save diagnostic metadata after processing the selected history
   if(!SaveState())
     {
      m_status = "State error";
      m_fault = true;
      return;
     }

   m_status = incomplete ? "History error" : "Up to date";
  }

A successfully enriched live deal or live order-history row contributes to the corresponding recorded-object set; a request or active-order observation alone does not. After restart, complete persisted rows rebuild these sets even if diagnostic state was not updated. Failed candidates remain eligible for a later scan. The scan stops on a journal error and reports failed enrichment rather than claiming completion. A malformed trailing row requires explicit inspection and repair; it is not silently discarded. Activity older than the configured window requires an increased lookback. Already represented objects are not replayed to reconstruct missed revisions.

Applying the optional symbol policy

Policy classification runs after a live deal has been appended successfully. It is limited to entry and entry-reversal deals; closing activity is not treated as new exposure. The deal and annotation are separate writes rather than one atomic transaction.

//+------------------------------------------------------------------+
//| Classifies a live entry against the optional symbol list         |
//+------------------------------------------------------------------+
void CTradeActivityAuditor::CheckPolicy(STradeAuditRecord &record)
  {
   if(record.eventType != AUDIT_DEAL_ADDED ||
      IsSymbolAllowed(record.symbol))
      return;

   if(record.entryType != "DEAL_ENTRY_IN" &&
      record.entryType != "DEAL_ENTRY_INOUT")
      return;

   STradeAuditRecord violation = record;
   violation.eventType = AUDIT_POSTTRADE_VIOLATION;
   violation.note = "Symbol is outside the auditor's optional allowed list";
   Save(violation);
  }
The original deal remains intact. The second row records the policy finding and retains the same deal, order, and position references. An empty symbol list makes IsSymbolAllowed() return true for every symbol, leaving the auditor in observation mode.

4. TradeAuditPanel.mqh: presenting journal results

TradeAuditPanel.mqh owns only chart objects and view state. It receives a journal reference instead of copying its records into another collection.

//+------------------------------------------------------------------+
//| Manages the audit panel display and chart controls               |
//+------------------------------------------------------------------+
class CTradeAuditPanel
  {
private:
   string               m_prefix;
   int                  m_x;
   int                  m_y;
   int                  m_periodDays;
   bool                 m_showDetails;

   void                 Label(string name, string text,
                              int x, int y, color clr, int size=10);
   void                 Button(string name, string text,
                               int x, int y, int width);
   void                 DeleteObjects();

public:
                        CTradeAuditPanel();
   void                 Initialize(int x, int y);
   void                 Update(CTradeAuditJournal &journal, string status);
   bool                 HandleClick(string objectName,
                                    CTradeAuditJournal &journal,
                                    string &message);
   void                 Shutdown();
  };

Initialize() creates the background and seven buttons. Update() asks the journal for a summary using m_periodDays, then refreshes the status, totals, three activity rows, and optional detail line. It never calls Append().

//--- Read totals and primary activity from the persistent journal
   int events, violations, liveEvents, recoveredEvents;
   STradeAuditRecord recent[];

   bool readable = journal.GetSummary(m_periodDays,
                                      events,
                                      violations,
                                      liveEvents,
                                      recoveredEvents,
                                      recent);

   if(!readable)
      status = "Journal error";

HandleClick() changes the selected period for TODAY, 7 DAYS, 30 DAYS, and ALL. DETAILS toggles m_showDetails. EXPORT calls journal.ExportPeriod() directly because export is a journal operation. REFRESH returns true to the EA, which then asks the auditor to recover history. This return value keeps account-history access outside the panel class.

5. TradeActivityAuditorEA.mq5: connecting MetaTrader events

The EA is the assembly point. It creates one auditor and one panel, then forwards platform events to the class that owns the required behavior. OnInit() establishes the dependency order: the journal is initialized through the auditor before the panel reads it.

//+------------------------------------------------------------------+
//| Initializes the account auditor                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Create the auditor before opening or recovering its journal
   g_auditor = new CTradeActivityAuditor();
   if(g_auditor == NULL ||
      !g_auditor.Initialize(Input_JournalFile,
                            Input_StateFile,
                            Input_AllowedSymbols,
                            Input_RecoveryDays))
     {
      if(g_auditor != NULL)
        {
         delete g_auditor;
         g_auditor = NULL;
        }
      return(INIT_FAILED);
     }

//--- Build the panel only after the journal is available
   g_panel = new CTradeAuditPanel();
   if(g_panel == NULL)
     {
      delete g_auditor;
      g_auditor = NULL;
      return(INIT_FAILED);
     }
   g_panel.Initialize(Input_PanelX, Input_PanelY);
   g_auditor.Recover();
   g_panel.Update(*g_auditor.Journal(), g_auditor.Status());
   EventSetTimer(2);
   return(INIT_SUCCEEDED);
  }

The transaction handler forwards capture without refreshing the panel. The existing timer performs display updates, avoiding a journal-summary scan inside every transaction callback. Capture still performs synchronous row persistence and therefore requires load testing:

//+------------------------------------------------------------------+
//| Receives live account transaction stages                         |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction &trans,
               const MqlTradeRequest &request,
               const MqlTradeResult &result)
  {
//--- Store the transaction while the auditor is available
   if(g_auditor == NULL)
      return;
   g_auditor.Capture(trans, request, result);
//--- Display refresh is handled by the existing timer
  }
//+------------------------------------------------------------------+
//| Refreshes on-chart values                                        |
//+------------------------------------------------------------------+
void OnTimer()
  {
//--- Refresh the panel without adding journal rows
   if(g_auditor != NULL && g_panel != NULL)
      g_panel.Update(*g_auditor.Journal(), g_auditor.Status());
  }
OnChartEvent() completes the control path. The panel interprets the object name and returns true only for REFRESH. The EA then invokes recovery before drawing the updated totals.
//+------------------------------------------------------------------+
//| Forwards chart button events to the panel                        |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
   if(id != CHARTEVENT_OBJECT_CLICK)
      return;

   if(g_auditor == NULL || g_panel == NULL)
      return;

//--- Let the panel interpret its buttons and selected period
   string message;
   bool refresh = g_panel.HandleClick(sparam, *g_auditor.Journal(), message);
   if(refresh)
      g_auditor.Recover();
   if(message != "")
      Print("[AUDIT] ", message);
   g_panel.Update(*g_auditor.Journal(), g_auditor.Status());
  }

OnDeinit() stops the timer, removes the panel objects, and deletes the panel before the auditor that owns its journal reference.

This file arrangement makes each dependency visible. The EA knows the auditor and panel. The panel knows the journal. The auditor owns the journal and produces the shared record. The journal knows only the record types. Terminal transaction details therefore do not leak into storage or chart-object code.


Operating the chart panel

The panel is a compact view of the journal, not a replacement for it. It displays:

  • auditor status;
  • selected period;
  • total journal rows;
  • violations;
  • live and recovered counts; and
  • up to three eligible order, deal, recovery, or violation records with the latest eventTime values inside the selected period.
Request, position, and history-transition stages remain included in the totals and CSV. They are omitted from the three chart rows so that technical callbacks do not displace the activity a trader is most likely to inspect.

The period controls are TODAY, 7 DAYS, 30 DAYS, and ALL. TODAY begins at midnight in the broker terminal's current time. The seven- and thirty-day controls use rolling windows.

DETAILS expands the latest visible record with its strongest ticket reference, action, and note. Pressing it again collapses the line.

REFRESH reconciles the configured history window against recorded object identities. It does not reload the chart, restore the missing callback sequence, or create a timer row.

EXPORT creates a separate CSV containing rows within the selected eventTime period. Its filename includes the account login, broker date, selected period, and a counter used to distinguish exports:

MQL5\Files\TradeAudit\TradeAudit_<login>_<broker-date>_<period>_<counter>.csv

The period component is Today, 7Days, 30Days, or All.

Export creates a separate uniquely named file and leaves the journal unchanged. The source is opened before a target is created, and existing export names are not reused. Journal writes are attempted immediately, so a separate Save button is unnecessary; failures remain visible. Omitting a Clear Log control reduces accidental deletion through the panel but does not prevent external modification.

The timer refreshes displayed totals every two seconds without creating audit records. A manual REFRESH can still append previously unrepresented historical objects. LIVE and RECOVERED totals count different kinds of observations, not equivalent numbers of trades.


Testing

The EA compiled in MetaEditor with zero errors and zero warnings. The functional test used a Deriv demo account with the auditor attached to an XAUUSD H1 chart. It covered desktop and mobile observations, period selection, activity while detached, history reconciliation, repeated refresh, detail display, and export.

Live desktop and mobile activity

The recording shows live XAUUSD activity labeled DESKTOP and EURUSD activity labeled MOBILE while the auditor remains attached to XAUUSD H1. This demonstrates account-wide observation across symbols rather than capture restricted to the chart symbol.

The export contains 40 LIVE rows: eight each of ORDER_ADDED, DEAL_ADDED, ORDER_REMOVED, HISTORY_ORDER_ADDED, and TRADE_REQUEST. These represent transaction stages associated with eight execution deals, not 40 separate trades.

The eight request rows retain distinct nonzero request IDs and TRADE_ACTION_DEAL as their request action. All eight have result code 10009. Dedicated requested-value columns remain separate from the enriched execution fields.

At the end of the live sequence, TODAY displays 52 rows: 40 LIVE and 12 RECOVERED. Switching to 30 DAYS displays 369 rows: 40 LIVE and 329 RECOVERED. The difference reflects historical objects outside the current day rather than additional live operations.


Figure 5. Live desktop and mobile observations, followed by period selection on the account auditor panel.

Activity while the auditor was detached

The second recording starts with the auditor displaying the broader-period total of 369 rows. The auditor is removed from the chart, after which two XAUUSD positions are opened and closed. The panel is absent while those operations take place.

After one reattachment, TODAY displays 60 rows: 40 LIVE and 20 RECOVERED. The live count remains unchanged, while the recovered count increases by eight relative to the earlier TODAY view.

The CSV confirms four ORDER_RECOVERED rows and four DEAL_RECOVERED rows appended at 01:45:42 in the journal’s time basis. Their order references match, and their position identifiers group them into the two positions shown in the recording.

Subsequent REFRESH actions leave the visible TODAY total at 60. The recording also shows use of EXPORT and expansion of the detail line through DETAILS.


Figure 6. Detached XAUUSD activity, recovery after reattachment, and refresh without further growth in the displayed total.

Reviewing the exported journal

The ALL export is named:

TradeAudit_40306821_2026-10-16_All_244074968.csv

It contains 377 data rows, each with the expected 32 fields.

Measure

Verified value

All journal rows
377
LIVE rows
40
RECOVERED rows
337
Recovered orders
168
Recovered deals
169
TODAY rows
60
TODAY LIVE rows
40
TODAY RECOVERED rows
20

The recovered deals include one DEAL_TYPE_BALANCE record with no order ticket or trading symbol. This explains the difference between the recovered order and deal counts; historical rows should not be forced into one-to-one pairs.

No identical parsed data rows were found. Within each recovered object type, account/server/ticket checks found no repeated recovered identities. The nonzero order and deal ticket sets also do not overlap between LIVE and RECOVERED records in this export.

These checks support the observed repeat-scan behavior without establishing an exactly-once guarantee under every failure condition. LIVE rows describe supported callback observations, while RECOVERED rows describe historical objects. Neither total represents the number of trading ideas or positions.

Scope of the results

The test demonstrates live desktop/mobile labeling, cross-symbol observation, multi-object recovery after detachment, stable totals during the recorded refresh checks, and a structurally consistent export. The panel displays Live during transaction capture and Up to date after recovery.

Historical STOP_OUT entries are present in the export, but the recordings do not demonstrate a controlled live stop-out test. Other scenarios—including failed requests, interrupted writes, concurrent instances, account switching, and non-ASCII text—remain outside this demonstration. Recovery preserves available historical objects within the selected window; it does not reproduce missing request context or intermediate notifications.


Conclusion

The Trade Activity Auditor records supported live transaction stages and reconciles historical orders and deals within an explicit recovery window. Account-bound storage, separate requested-value fields, and persisted object identities make the resulting observations easier to correlate across sessions.

The panel provides a period view while the CSV preserves successfully written observations. Recovery cannot reproduce failed requests or intermediate stages missed during downtime, and the journal does not authenticate actors or provide tamper evidence. Authorization remains with the program sending the trade; the auditor supplies a technical record for later review.

Attachments

Name Type Responsibility
TradeActivityAuditorEA.mq5
Expert Advisor Creates the auditor and panel, then forwards transaction, timer, and chart events.
TradeActivityAuditor.mqh
Include
Converts live transactions and recovered history into structured records.
TradeAuditJournal.mqh
Include
Maintains the append-only journal, summaries, and period exports.
TradeAuditPanel.mqh
Include
Creates the chart dashboard and handles period, detail, refresh, and export controls.
TradeAuditTypes.mqh
Include
Defines audit events and the shared record structure.
MQL5.zip Archive Contains the compile-ready project arranged for the MetaTrader Data Folder.


Using Renko-bar Regression with Error Correction Using Renko-bar Regression with Error Correction
The article presents a regression-based approach to forecasting Renko bars using CatBoost: the model estimates the logarithmic return of the next bar and the uncertainty of the forecast. We will examine a cascade of residual models with OOF validation using TimeSeriesSplit, shrinkage, and overall early stopping, as well as conditional bias correction. On EURUSD D1, a reduction in OOF-MAE was achieved, along with approximately 65% directional accuracy. A working script for MetaTrader 5 is provided that generates a signal, position size, SL, and TP in brick units.
Integrating News Sentiment to Avoid Strategy Disruption in MQL5 Integrating News Sentiment to Avoid Strategy Disruption in MQL5
We present a dynamic news‑sentiment filter for MQL5 Expert Advisors. It uses WebRequest to query a REST API, parses JSON sentiment and confidence, and gates entries and exits alongside Moving Average and RSI signals. You get an example EA, integration steps, timer‑based updates, and Strategy Tester workarounds. The outcome is fewer trades against dominant news flow and earlier risk reduction when sentiment flips.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
Neural Networks in Trading: Adaptive Perception of Market Dynamics (Encoder) Neural Networks in Trading: Adaptive Perception of Market Dynamics (Encoder)
The article presents the comprehensive architecture of the STE-FlowNet encoder, which combines stack memory, recurrent processing, and a correlation mechanism to extract hidden market dependencies. The article demonstrates how these modules are sequentially integrated into a single computational chain capable of performing comprehensive time-series analysis.