preview
The Repository Pattern in MQL5: Abstracting Trade History Access for Testable EA Logic

The Repository Pattern in MQL5: Abstracting Trade History Access for Testable EA Logic

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

Introduction

An Expert Advisor (EA) analytics module that directly calls HistorySelect(), HistoryDealGetDouble(), and HistoryDealGetInteger() inside its calculation methods introduces hidden dependencies on the MetaTrader 5 terminal state. For example, a method like CalculateWinRate() appears to be a pure function. In practice, it requires an active broker connection and a loaded account history. It also requires a prior HistorySelect() call with the correct date range. If any of these conditions are missing, the method returns incorrect results without raising a detectable error.

This tight coupling creates severe testing and maintenance limitations. An analytics module written this way cannot be executed in isolation. To unit-test CalculateWinRate(), you need a live terminal and an active connection. You also need an account with matching historical data. Verifying edge cases—such as an empty history—is impractical at scale. It forces the developer to either manually clear terminal data or inject conditional testing logic into production code.

The maintenance overhead compounds as the codebase grows:

  • API Changes: If a broker changes its deal classification scheme, every calculation method reading HistoryDealGetInteger() must be updated manually.
  • Portability Barriers: Porting the EA to a different broker or architecture requires disentangling the analytics code from the data access layer.
  • Code Duplication: New modules must either duplicate the data retrieval pattern or rely on the undocumented assumption that another module already initialized the history state.

Architectural comparison demonstrating

Figure 1: Applying the Dependency Inversion Principle. Analytics consumers now depend on the ITradeRepository abstraction rather than concrete data sources, enabling interchangeable live and mock repository implementations.

The left side shows the original tightly coupled design, where analytical components depend directly on the MQL5 History API. The right side shows the repository-based design, where all consumers depend only on the ITradeRepository  interface. This abstraction decouples analytics code from the underlying data source and allows live and mock repositories to be swapped without changing consumer logic.


The Repository Pattern as a Dependency Inversion Mechanism

The repository pattern introduces a contract layer between consumers and data sources. Consumers depend on an interface. The interface defines what data operations are available. Concrete implementations perform these operations by talking to their underlying data source. The consumer has no knowledge of, and no dependency on, the concrete implementation.

In MQL5, this structure is implemented using an abstract base class with pure virtual methods. Every analytics component receives a pointer to ITradeRepository rather than a pointer to any specific implementation. At construction time, the EA injects either CLiveTradeRepository or CMockTradeRepository into analytics components. The analytics code is identical in both cases. The outputs are identical when the mock data is constructed to match the live data. The underlying data access mechanism is completely different.

The STradeRecord Struct

Every repository implementation and every analytics consumer in this architecture shares one canonical data structure: STradeRecord. Defining it in a dedicated include file ensures that no component owns its own version of a trade record, and no implicit type conversions occur across the boundary between the data layer and the analytics layer.

//+------------------------------------------------------------------+
//|                                                  TradeRecord.mqh |
//| STradeRecord: canonical trade record struct shared across all    |
//| repository implementations and analytics consumers.              |
//+------------------------------------------------------------------+
#ifndef TRADERECORD_MQH
#define TRADERECORD_MQH
//+------------------------------------------------------------------+
//| STradeRecord                                                     |
//| Purpose: Core data structure representing a finalized trade deal |
//+------------------------------------------------------------------+
struct STradeRecord
  {
   ulong             ticket;        // Deal or order ticket identifier
   datetime          open_time;     // Trade open timestamp
   datetime          close_time;    // Trade close timestamp
   double            open_price;    // Entry price
   double            close_price;   // Exit price
   double            volume;        // Executed volume in lots
   double            profit;        // Net profit including swap and commission
   double            commission;    // Commission charged by broker
   double            swap;          // Swap charged or credited
   string            symbol;        // Trading instrument symbol
   int               direction;     // 1 = long, -1 = short
   string            comment;       // Order or deal comment
   //--- Constructor initializing all fields safely to default clear states
                     STradeRecord(void)
      :              ticket(0),
         open_time(0),
         close_time(0),
         open_price(0.0),
         close_price(0.0),
         volume(0.0),
         profit(0.0),
         commission(0.0),
         swap(0.0),
         symbol(""),
         direction(0),
         comment("")
     {
      //--- Empty structural body
     }
  };
#endif // TRADERECORD_MQH
//+------------------------------------------------------------------+

The constructor initializes all fields to safe defaults. This matters because GetClosedTrade() in both repository implementations returns a default-constructed STradeRecord on any invalid index or failed history selection. Callers receive a zeroed record rather than reading garbage memory.

The ITradeRepository Interface

ITradeRepository is the inversion point. It is an abstract base class with pure virtual methods. No consumer in this architecture holds a pointer to CLiveTradeRepository or CMockTradeRepository directly. Every consumer holds an ITradeRepository*. This is what makes the live-to-mock swap a single pointer reassignment rather than a codebase-wide refactor.

//+------------------------------------------------------------------+
//|                                           ITradeRepository.mqh   |
//| Abstract repository contract. All analytics consumers depend     |
//| exclusively on this interface, never on concrete                 |
//| implementations or the History API directly.                     |
//+------------------------------------------------------------------+
#ifndef ITRADE_REPOSITORY_MQH
#define ITRADE_REPOSITORY_MQH
#include "TradeRecord.mqh"
//+------------------------------------------------------------------+
//| ITradeRepository                                                 |
//| Purpose: Interface defining data access contracts for historical |
//|          and transactional trade tracking operations.            |
//+------------------------------------------------------------------+
class ITradeRepository
  {
public:
   //--- Interface methods for record retrieval and counting
   virtual int          GetTradeCount(void) = 0;
   virtual STradeRecord GetClosedTrade(int index) = 0;
   //--- Interface methods for metric calculations and diagnostic evaluation
   virtual double       GetDailyPnL(datetime date) = 0;
   virtual double       GetWinRate(void) = 0;
   virtual double       GetTotalProfit(void) = 0;
   virtual double       GetMaxDrawdown(void) = 0;
   virtual double       GetAverageTrade(void) = 0;
   //--- Interface metadata identity and memory lifecycle management
   virtual string       GetRepositoryType(void) = 0;
   virtual             ~ITradeRepository(void) {}
  };
#endif // ITRADE_REPOSITORY_MQH
//+------------------------------------------------------------------+

Seven virtual methods define what a repository can do. The = 0 syntax enforces the contract: any class that inherits ITradeRepository without implementing all seven methods cannot be compiled. GetRepositoryType() returns either "LIVE" or "MOCK" and is used by the equity curve panel's title bar to indicate which data source is active at render time.

Architectural Layers

Layer Responsibility
EA Logic Decision-making, component wiring, repository injection
Repository Interface (ITradeRepository) Abstraction contract; defines what operations exist
Concrete Repository Data retrieval from History API or mock dataset
Data Source History API, in-memory array, CSV file, or database

CLiveTradeRepository: Implementing the Contract Against the History API

CLiveTradeRepository implements ITradeRepository by querying the MetaTrader 5 History API on every method call. The constructor accepts a date range and magic number filter. The private SelectHistory() helper calls HistorySelect() to populate the terminal's deal cache before any enumeration begins. The private IsEntryDeal() helper filters out opening deals, retaining only DEAL_ENTRY_OUT and DEAL_ENTRY_INOUT entries that represent trade completions.

//+------------------------------------------------------------------+
//|                                       LiveTradeRepository.mqh    |
//| CLiveTradeRepository: implements ITradeRepository using the      |
//| MetaTrader 5 History API. Queries HistorySelect() on each        |
//| method call to reflect current terminal trade history state.     |
//+------------------------------------------------------------------+
#ifndef LIVETRADE_REPOSITORY_MQH
#define LIVETRADE_REPOSITORY_MQH

#include "ITradeRepository.mqh"

//+------------------------------------------------------------------+
//| CLiveTradeRepository                                             |
//| Purpose: Active trading terminal history engine adapter tracking |
//|          deal entries, commissions, and metrics dynamically.     |
//+------------------------------------------------------------------+
class CLiveTradeRepository : public ITradeRepository
  {
private:
   datetime          m_from_date;    // History query start date boundary
   datetime          m_to_date;      // History query end date boundary
   ulong             m_magic;        // Filter: only include deals with this magic number (0 = all)

   //--- Internal utility validation rules
   bool              SelectHistory(void);
   bool              IsEntryDeal(ulong ticket);

public:
   //--- Lifecycle management
                     CLiveTradeRepository(datetime from_date, datetime to_date, ulong magic);
                    ~CLiveTradeRepository(void) {}

   //--- Overridden calculation and data access methods
   virtual int          GetTradeCount(void);
   virtual STradeRecord GetClosedTrade(int index);
   virtual double       GetDailyPnL(datetime date);
   virtual double       GetWinRate(void);
   virtual double       GetTotalProfit(void);
   virtual double       GetMaxDrawdown(void);
   virtual double       GetAverageTrade(void);
   virtual string       GetRepositoryType(void);
  };

The constructor simply stores the three parameters that scope every subsequent query:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//| Purpose: Initializes the history adapter context boundaries      |
//+------------------------------------------------------------------+
CLiveTradeRepository::CLiveTradeRepository(datetime from_date,
      datetime to_date,
      ulong magic)
   : m_from_date(from_date),
     m_to_date(to_date),
     m_magic(magic)
  {
  }

SelectHistory() and IsEntryDeal() are the only two methods in the entire codebase that touch the History API directly. Every public method delegates its filtering logic through these two helpers, which means any future change to how history is selected or how entry deals are classified is a change to exactly two private methods, nowhere else:

//+------------------------------------------------------------------+
//| IsEntryDeal                                                      |
//| Purpose: Detects whether a historical deal record represents an  |
//|          a closing execution that completes the trade            |
//+------------------------------------------------------------------+
bool CLiveTradeRepository::IsEntryDeal(ulong ticket)
  {
//--- Retrieve structural transaction tracking type identifier
   long deal_entry = HistoryDealGetInteger(ticket, DEAL_ENTRY);

//--- DEAL_ENTRY_OUT (Close), DEAL_ENTRY_INOUT (Reversal) indicate trade completion
   return(deal_entry == DEAL_ENTRY_OUT || deal_entry == DEAL_ENTRY_INOUT);
  }

GetTradeCount() shows the pattern all public methods follow: validate the history selection first, then enumerate with magic number and entry-type filtering applied on every iteration:

//+------------------------------------------------------------------+
//| GetTradeCount                                                    |
//| Purpose: Evaluates total number of qualifying closed loop deals  |
//+------------------------------------------------------------------+
int CLiveTradeRepository::GetTradeCount(void)
  {
//--- Synchronize cache storage lists
   if(!SelectHistory())
     {
      return(0);
     }

   int count = 0;
   int total = HistoryDealsTotal();

//--- Loop through the selection index to parse metadata matches
   for(int i = 0; i < total; i++)
     {
      ulong ticket = HistoryDealGetTicket(i);
      if(ticket == 0)
        {
         continue;
        }

      //--- Validate magic number ownership constraints if required
      if(m_magic > 0 && (ulong)HistoryDealGetInteger(ticket, DEAL_MAGIC) != m_magic)
        {
         continue;
        }

      //--- Filter non-closing transaction fragments
      if(!IsEntryDeal(ticket))
        {
         continue;
        }
      count++;
     }

   return(count);
  }

GetClosedTrade() maps a virtual positional index to a deal in the filtered sequence and hydrates an STradeRecord. Consumers such as CEquityCurvePanel call this method in a sequential loop from index 0 to GetTradeCount() - 1 without any knowledge that the underlying data comes from the terminal's deal cache:

//+------------------------------------------------------------------+
//| GetClosedTrade                                                   |
//| Purpose: Hydrates and builds a canonical structural trade model  |
//|          matching a virtual tracking positional index reference  |
//+------------------------------------------------------------------+
STradeRecord CLiveTradeRepository::GetClosedTrade(int index)
  {
   STradeRecord record;

//--- Check active collection environment state
   if(!SelectHistory())
     {
      return(record);
     }

   int total     = HistoryDealsTotal();
   int match_idx = 0;

//--- Traverse target tracking database
   for(int i = 0; i < total; i++)
     {
      ulong ticket = HistoryDealGetTicket(i);
      if(ticket == 0)
        {
         continue;
        }
      if(m_magic > 0 && (ulong)HistoryDealGetInteger(ticket, DEAL_MAGIC) != m_magic)
        {
         continue;
        }
      if(!IsEntryDeal(ticket))
        {
         continue;
        }

      //--- Extract and copy target values upon index verification match
      if(match_idx == index)
        {
         record.ticket      = ticket;
         record.close_time  = (datetime)HistoryDealGetInteger(ticket, DEAL_TIME);
         record.close_price = HistoryDealGetDouble(ticket, DEAL_PRICE);
         record.volume      = HistoryDealGetDouble(ticket, DEAL_VOLUME);
         record.profit      = HistoryDealGetDouble(ticket, DEAL_PROFIT);
         record.commission  = HistoryDealGetDouble(ticket, DEAL_COMMISSION);
         record.swap        = HistoryDealGetDouble(ticket, DEAL_SWAP);
         record.symbol      = HistoryDealGetString(ticket, DEAL_SYMBOL);
         record.comment     = HistoryDealGetString(ticket, DEAL_COMMENT);

         //--- Map system enum layouts to direction standards (1=Long, -1=Short)
         long deal_type   = HistoryDealGetInteger(ticket, DEAL_TYPE);
         record.direction = (deal_type == DEAL_TYPE_SELL) ? 1 : -1;
         return(record);
        }

      match_idx++;
     }

   return(record);
  }

The remaining metric methods — GetDailyPnL(), GetWinRate(), GetTotalProfit(), GetMaxDrawdown(), and GetAverageTrade() — follow the same enumeration pattern. Each includes profit, commission, and swap in its net calculation to ensure that broker costs are always accounted for. GetAverageTrade() delegates to both GetTradeCount() and GetTotalProfit() rather than re-enumerating the history a third time:

//+------------------------------------------------------------------+
//| GetWinRate                                                       |
//| Purpose: Calculates percentage ratio of net profitable trades    |
//+------------------------------------------------------------------+
double CLiveTradeRepository::GetWinRate(void)
  {
   if(!SelectHistory())
     {
      return(0.0);
     }

   int total = HistoryDealsTotal();
   int wins  = 0;
   int count = 0;

   for(int i = 0; i < total; i++)
     {
      ulong ticket = HistoryDealGetTicket(i);
      if(ticket == 0)
        {
         continue;
        }
      if(m_magic > 0 && (ulong)HistoryDealGetInteger(ticket, DEAL_MAGIC) != m_magic)
        {
         continue;
        }
      if(!IsEntryDeal(ticket))
        {
         continue;
        }

      //--- Calculate complete returns by factoring charges and adjustments
      double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT) +
                      HistoryDealGetDouble(ticket, DEAL_COMMISSION) +
                      HistoryDealGetDouble(ticket, DEAL_SWAP);
      if(profit > 0.0)
        {
         wins++;
        }
      count++;
     }

//--- Protect against division by zero errors
   if(count == 0)
     {
      return(0.0);
     }
   return((wins / (double)count) * 100.0);
  }

//+------------------------------------------------------------------+
//| GetTotalProfit                                                   |
//| Purpose: Accumulates overall lifetime transaction metrics        |
//+------------------------------------------------------------------+
double CLiveTradeRepository::GetTotalProfit(void)
  {
   if(!SelectHistory())
     {
      return(0.0);
     }

   int    total  = HistoryDealsTotal();
   double result = 0.0;

   for(int i = 0; i < total; i++)
     {
      ulong ticket = HistoryDealGetTicket(i);
      if(ticket == 0)
        {
         continue;
        }
      if(m_magic > 0 && (ulong)HistoryDealGetInteger(ticket, DEAL_MAGIC) != m_magic)
        {
         continue;
        }
      if(!IsEntryDeal(ticket))
        {
         continue;
        }

      //--- Continuously increment master balances with comprehensive tracking values
      result += HistoryDealGetDouble(ticket, DEAL_PROFIT);
      result += HistoryDealGetDouble(ticket, DEAL_COMMISSION);
      result += HistoryDealGetDouble(ticket, DEAL_SWAP);
     }

   return(result);
  }

//+------------------------------------------------------------------+
//| GetMaxDrawdown                                                   |
//| Purpose: Tracks systemic structural peak reductions to measure   |
//|          maximum depth relative to history curves                |
//+------------------------------------------------------------------+
double CLiveTradeRepository::GetMaxDrawdown(void)
  {
   if(!SelectHistory())
     {
      return(0.0);
     }

   int    total  = HistoryDealsTotal();
   double equity = 0.0;
   double peak   = 0.0;
   double max_dd = 0.0;

   for(int i = 0; i < total; i++)
     {
      ulong ticket = HistoryDealGetTicket(i);
      if(ticket == 0)
        {
         continue;
        }
      if(m_magic > 0 && (ulong)HistoryDealGetInteger(ticket, DEAL_MAGIC) != m_magic)
        {
         continue;
        }
      if(!IsEntryDeal(ticket))
        {
         continue;
        }

      //--- Calculate current incremental closed balance level points
      equity += HistoryDealGetDouble(ticket, DEAL_PROFIT) +
                HistoryDealGetDouble(ticket, DEAL_COMMISSION) +
                HistoryDealGetDouble(ticket, DEAL_SWAP);

      //--- Update maximum running curve historical peak metrics
      if(equity > peak)
        {
         peak = equity;
        }

      //--- Evaluate distance drops between peak caps and raw floors
      double dd = peak - equity;
      if(dd > max_dd)
        {
         max_dd = dd;
        }
     }

   return(max_dd);
  }

//+------------------------------------------------------------------+
//| GetAverageTrade                                                  |
//| Purpose: Determines statistical math expectation value levels     |
//+------------------------------------------------------------------+
double CLiveTradeRepository::GetAverageTrade(void)
  {
   int count = GetTradeCount();
   if(count == 0)
     {
      return(0.0);
     }

//--- Mathematical mean logic processing standard returns
   return(GetTotalProfit() / count);
  }

//+------------------------------------------------------------------+
//| GetRepositoryType                                                |
//| Purpose: Explicit indicator identifying implementation lineage   |
//+------------------------------------------------------------------+
string CLiveTradeRepository::GetRepositoryType(void)
  {
   return("LIVE");
  }

#endif // LIVETRADE_REPOSITORY_MQH

Repository Interface Methods

Method Return Type Purpose
GetTradeCount() int Returns total number of closed trades in dataset
GetClosedTrade(int index) STradeRecord Returns trade record at specified index
GetDailyPnL(datetime date) double Returns net profit/loss for trades closed on the given date
GetWinRate() double Returns percentage of trades with positive profit
GetTotalProfit() double Returns sum of all trade profits
GetMaxDrawdown() double Returns maximum peak-to-trough equity drawdown in dataset
GetAverageTrade() double Returns mean profit per trade

Repository Implementations

Repository Backing Source Primary Use Case
CLiveTradeRepository History API (HistorySelect, HistoryDealGetDouble) Production EA on live or demo account
CMockTradeRepository Hardcoded in-memory STradeRecord array Offline testing, CI pipelines, parameter validation
Future CSV Repository File system via FileOpen / FileReadString Backtesting result replay, cross-session analysis
Future Database Repository External storage via WebRequest Cloud-based analytics, multi-account aggregation

CMockTradeRepository: Implementing the Contract Against an In-Memory Array

CMockTradeRepository implements the identical interface without a single History API call. Its backing store is a fixed STradeRecord array populated at construction time by BuildDataset(). Every method call against the mock repository produces the same result for the same input on every machine, every session, and every broker. This is what makes it useful for testing.

//+------------------------------------------------------------------+
//|                                       MockTradeRepository.mqh    |
//| CMockTradeRepository: implements ITradeRepository via a          |
//| hardcoded in-memory STradeRecord array. Produces deterministic   |
//| results independent of terminal state or broker connection.      |
//+------------------------------------------------------------------+
#ifndef MOCKTRADE_REPOSITORY_MQH
#define MOCKTRADE_REPOSITORY_MQH

#include "ITradeRepository.mqh"

//+------------------------------------------------------------------+
//| CMockTradeRepository                                             |
//| Purpose: Mock database provider serving a static historical deal |
//|          matrix for isolated strategic analysis testing.         |
//+------------------------------------------------------------------+
class CMockTradeRepository : public ITradeRepository
  {
private:
   STradeRecord      m_trades[];     // In-memory trade dataset
   int               m_count;        // Number of records in dataset

   void              BuildDataset(void);

public:
   //--- Lifecycle Management
                     CMockTradeRepository(void);
                    ~CMockTradeRepository(void) {}

   //--- Interface Implementations
   virtual int          GetTradeCount(void);
   virtual STradeRecord GetClosedTrade(int index);
   virtual double       GetDailyPnL(datetime date);
   virtual double       GetWinRate(void);
   virtual double       GetTotalProfit(void);
   virtual double       GetMaxDrawdown(void);
   virtual double       GetAverageTrade(void);
   virtual string       GetRepositoryType(void);
  };

BuildDataset() constructs a 48-trade dataset with 29 winners and 19 losers. The profit values, symbols, and timestamps are all deterministic. A commission of -0.70 is applied to every trade to ensure that the metrics reflect real net-of-cost values rather than gross profit. The dataset is designed so that the win rate computes to approximately 60.42%:

//+-------------------------------------------------------------------+
//| BuildDataset                                                      |
//| Purpose: Constructs a fixed 48-trade dataset matching structural  |
//|          sample criteria (Win Rate 60.42%, Count: 48)             |
//+-------------------------------------------------------------------+
void CMockTradeRepository::BuildDataset(void)
  {
   //--- Profit values: 29 winners, 19 losers, net 344.20 on a base day
   double profits[] =
     {
      18.50,  -12.30,  25.70,  -8.90,  31.20,  -15.40,  22.10,  -9.80,
      27.30,   14.60, -11.20,  19.80, -22.50,  35.40,  -13.70,  28.90,
       8.40,  -17.60,  23.50,  16.20, -10.30,  29.70,  -14.80,  21.40,
      12.90,  -19.20,  33.60,   7.80, -11.90,  26.10,  -16.50,  18.70,
      24.30,  -13.10,  30.80,   9.20, -20.40,  22.90,  -8.60,   15.40,
      28.50,  -12.70,  19.30,  -9.40,  34.20,  -17.80,  11.60,  25.90
     };

   string symbols[] =
     {
      "EURUSD", "GBPUSD", "USDJPY", "AUDUSD", "USDCAD", "EURUSD", "GBPUSD", "USDJPY",
      "EURUSD", "AUDUSD", "USDCAD", "GBPUSD", "EURUSD", "USDJPY", "AUDUSD", "EURUSD",
      "GBPUSD", "USDCAD", "EURUSD", "USDJPY", "GBPUSD", "EURUSD", "AUDUSD", "USDCAD",
      "USDJPY", "EURUSD", "GBPUSD", "AUDUSD", "USDCAD", "EURUSD", "USDJPY", "GBPUSD",
      "EURUSD", "AUDUSD", "USDCAD", "USDJPY", "EURUSD", "GBPUSD", "AUDUSD", "EURUSD",
      "USDJPY", "USDCAD", "GBPUSD", "EURUSD", "AUDUSD", "USDJPY", "EURUSD", "GBPUSD"
     };

   //--- Configure internal array dimensions
   m_count = ArraySize(profits);
   ArrayResize(m_trades, m_count);

   //--- Base date reference: Midnight of 2024-01-15 
   datetime base_day = D'2024.01.15 00:00';

   //--- Synthesize comprehensive historical metadata loops
   for(int i = 0; i < m_count; i++)
     {
      m_trades[i].ticket      = (ulong)(100001 + i);
      m_trades[i].open_time   = base_day + (i * 600);
      m_trades[i].close_time  = base_day + (i * 600) + 300;
      m_trades[i].open_price  = 1.08000 + (i * 0.00010);
      m_trades[i].close_price = m_trades[i].open_price + (profits[i] > 0 ? 0.00050 : -0.00050);
      m_trades[i].volume      = 0.10;
      m_trades[i].profit      = profits[i];
      m_trades[i].commission  = -0.70;
      m_trades[i].swap        = 0.0;
      m_trades[i].symbol      = symbols[i];
      m_trades[i].direction   = (profits[i] > 0 && i % 2 == 0) ? 1 : -1;
      m_trades[i].comment     = "Mock trade " + IntegerToString(i + 1);
     }
  }

Because the mock repository owns its data as a plain array, GetClosedTrade() is an index bounds check and a direct array read. There is no enumeration, no API call, and no possibility of the result changing between calls:

//+------------------------------------------------------------------+
//| GetClosedTrade                                                   |
//+------------------------------------------------------------------+
STradeRecord CMockTradeRepository::GetClosedTrade(int index)
  {
   //--- Validate boundaries to safeguard against array out-of-range errors
   if(index < 0 || index >= m_count)
     {
      STradeRecord empty_record;
      return(empty_record);
     }
     
   return(m_trades[index]);
  }

The metric methods iterate m_trades[] directly. GetWinRate() applies the same net-of-cost logic as the live implementation — profit plus commission plus swap — so that the two repositories produce comparable values when fed matching datasets:

//+------------------------------------------------------------------+
//| GetWinRate                                                       |
//+------------------------------------------------------------------+
double CMockTradeRepository::GetWinRate(void)
  {
   if(m_count == 0)
     {
      return(0.0);
     }

   int wins = 0;
   for(int i = 0; i < m_count; i++)
     {
      //--- Evaluate trade result net of operational overhead costs
      double net = m_trades[i].profit + 
                   m_trades[i].commission + 
                   m_trades[i].swap;
      if(net > 0.0)
        {
         wins++;
        }
     }

   return((wins / (double)m_count) * 100.0);
  }

//+------------------------------------------------------------------+
//| GetTotalProfit                                                   |
//+------------------------------------------------------------------+
double CMockTradeRepository::GetTotalProfit(void)
  {
   double total = 0.0;
   
   for(int i = 0; i < m_count; i++)
     {
      total += m_trades[i].profit + 
               m_trades[i].commission + 
               m_trades[i].swap;
     }
     
   return(total);
  }

//+------------------------------------------------------------------+
//| GetMaxDrawdown                                                   |
//| Purpose: Processes the continuous data grid to locate the widest |
//|          peak-to-trough drop valley in the cumulative equity path|
//+------------------------------------------------------------------+
double CMockTradeRepository::GetMaxDrawdown(void)
  {
   double equity = 0.0;
   double peak   = 0.0;
   double max_dd = 0.0;

   for(int i = 0; i < m_count; i++)
     {
      equity += m_trades[i].profit + 
                m_trades[i].commission + 
                m_trades[i].swap;

      if(equity > peak)
        {
         peak = equity;
        }
        
      double dd = peak - equity;
      if(dd > max_dd)
        {
         max_dd = dd;
        }
     }

   return(max_dd);
  }

//+------------------------------------------------------------------+
//| GetAverageTrade                                                  |
//+------------------------------------------------------------------+
double CMockTradeRepository::GetAverageTrade(void)
  {
   if(m_count == 0)
     {
      return(0.0);
     }
     
   return(GetTotalProfit() / m_count);
  }

//+------------------------------------------------------------------+
//| GetRepositoryType                                                |
//+------------------------------------------------------------------+
string CMockTradeRepository::GetRepositoryType(void)
  {
   return("MOCK");
  }

#endif // MOCKTRADE_REPOSITORY_MQH
//+------------------------------------------------------------------+


Consumer Module Independence

Every analytics consumer in this architecture depends only on ITradeRepository*. The risk manager uses it to retrieve recent trade outcomes for position sizing. The analytics engine uses it to compute win rate, average trade, and drawdown. The equity curve panel uses it to iterate all trades in sequence and plot the running profit curve. None of these modules contain a single call to any History API function.

Consumer Modules

Module Depends On Data Operations Used
CAnalyticsEngine ITradeRepository* GetWinRate(), GetTotalProfit(), GetAverageTrade(), GetMaxDrawdown()
CEquityCurvePanel ITradeRepository* GetTradeCount(), GetClosedTrade()
Risk Manager (EA-level) ITradeRepository* GetDailyPnL(), GetWinRate()
Position Sizing Module ITradeRepository* GetAverageTrade(), GetMaxDrawdown()

CAnalyticsEngine: Consuming the Interface

CAnalyticsEngine holds a single ITradeRepository* member. Its constructor accepts a pointer injected from outside, which means the caller decides whether analytics runs against live or mock data. The engine itself has no opinion on this. RunAnalysis() calls six repository methods and caches the results. PrintReport() formats those cached values to the terminal log:

//+------------------------------------------------------------------+
//|                                              AnalyticsEngine.mqh |
//| CAnalyticsEngine: computes win rate, total profit, average       |
//| trade, and max drawdown exclusively through ITradeRepository*.   |
//| Contains no direct History API calls.                            |
//+------------------------------------------------------------------+
#ifndef ANALYTICSENGINE_MQH
#define ANALYTICSENGINE_MQH

#include "ITradeRepository.mqh"

//+------------------------------------------------------------------+
//| CAnalyticsEngine                                                 |
//| Purpose: Decoupled statistical processing engine evaluating      |
//|          metrics provided by an abstract historical interface.   |
//+------------------------------------------------------------------+
class CAnalyticsEngine
  {
private:
   ITradeRepository *m_repository;   // Non-owned data layer resource pointer

   double            m_win_rate;     // Cached value for last calculated win rate
   double            m_total_profit; // Cached value for last calculated net profit
   double            m_avg_trade;    // Cached value for last calculated average payout expectation
   double            m_max_drawdown; // Cached value for last calculated absolute maximum drawdown depth
   double            m_daily_pnl;    // Cached value for last calculated net day profit/loss
   int               m_trade_count;  // Cached value for last calculated closed trade loop count

public:
   //--- Lifecycle Management
                     CAnalyticsEngine(ITradeRepository *repository);
                    ~CAnalyticsEngine(void) {}

   //--- Processing Routines
   void              RunAnalysis(datetime daily_pnl_date);
   void              PrintReport(void);

   //--- State Constant Accessors
   double            GetWinRate(void)     const;
   double            GetTotalProfit(void) const;
   double            GetAvgTrade(void)    const;
   double            GetMaxDrawdown(void) const;
   double            GetDailyPnL(void)    const;
   int               GetTradeCount(void)  const;
  };

//+------------------------------------------------------------------+
//| Constructor                                                      |
//| Purpose: Injects database dependency and normalizes state values |
//+------------------------------------------------------------------+
CAnalyticsEngine::CAnalyticsEngine(ITradeRepository *repository)
   : m_repository(repository),
     m_win_rate(0.0),
     m_total_profit(0.0),
     m_avg_trade(0.0),
     m_max_drawdown(0.0),
     m_daily_pnl(0.0),
     m_trade_count(0)
  {
  }

//+------------------------------------------------------------------+
//| RunAnalysis                                                      |
//| Purpose: Polls and caches metric values across the repository    |
//+------------------------------------------------------------------+
void CAnalyticsEngine::RunAnalysis(datetime daily_pnl_date)
  {
   //--- Validate data layer reference to prevent access violations
   if(m_repository == NULL)
     {
      Print("[CAnalyticsEngine] Repository pointer is null. Analysis aborted.");
      return;
     }

   //--- Sequentially pull evaluation parameters safely from abstraction layer
   m_trade_count  = m_repository.GetTradeCount();
   m_win_rate     = m_repository.GetWinRate();
   m_total_profit = m_repository.GetTotalProfit();
   m_avg_trade    = m_repository.GetAverageTrade();
   m_max_drawdown = m_repository.GetMaxDrawdown();
   m_daily_pnl    = m_repository.GetDailyPnL(daily_pnl_date);
  }

//+-------------------------------------------------------------------------+
//| PrintReport                                                             |
//| Purpose: Formats metric results cleanly onto terminal diagnostic output |
//+-------------------------------------------------------------------------+
void CAnalyticsEngine::PrintReport(void)
  {
   //--- Dynamically isolate driver type name signature details
   string repo_type = (m_repository != NULL) ? m_repository.GetRepositoryType() : "UNKNOWN";

   Print("[INFO] Repository Type = " + repo_type);
   Print("[INFO] Running Analytics...");
   Print("");
   Print("Daily PnL    = " + DoubleToString(m_daily_pnl, 2));
   Print("Win Rate     = " + DoubleToString(m_win_rate, 2) + "%");
   Print("Trade Count  = " + IntegerToString(m_trade_count));
   Print("Total Profit = " + DoubleToString(m_total_profit, 2));
   Print("Avg Trade    = " + DoubleToString(m_avg_trade, 2));
   Print("Max Drawdown = " + DoubleToString(m_max_drawdown, 2));
   Print("");
  }

//+------------------------------------------------------------------+
//| GetWinRate                                                       |
//| Purpose: Returns the cached win rate percentage metric           |
//+------------------------------------------------------------------+
double CAnalyticsEngine::GetWinRate(void) const
  {
   return(m_win_rate);
  }

//+------------------------------------------------------------------+
//| GetTotalProfit                                                   |
//| Purpose: Returns the cached historical net profit value    |
//+------------------------------------------------------------------+
double CAnalyticsEngine::GetTotalProfit(void) const
  {
   return(m_total_profit);
  }

//+------------------------------------------------------------------+
//| GetAvgTrade                                                      |
//| Purpose: Returns the cached average profit math expectation value|
//+------------------------------------------------------------------+
double CAnalyticsEngine::GetAvgTrade(void) const
  {
   return(m_avg_trade);
  }

//+------------------------------------------------------------------+
//| GetMaxDrawdown                                                   |
//| Purpose: Returns the cached maximum absolute curve drawdown value|
//+------------------------------------------------------------------+
double CAnalyticsEngine::GetMaxDrawdown(void) const
  {
   return(m_max_drawdown);
  }

//+------------------------------------------------------------------+
//| GetDailyPnL                                                      |
//| Purpose: Returns the cached daily profit/loss for analyzed date  |
//+------------------------------------------------------------------+
double CAnalyticsEngine::GetDailyPnL(void) const
  {
   return(m_daily_pnl);
  }

//+------------------------------------------------------------------+
//| GetTradeCount                                                    |
//| Purpose: Returns the cached total qualifying closed trade count  |
//+------------------------------------------------------------------+
int CAnalyticsEngine::GetTradeCount(void) const
  {
   return(m_trade_count);
  }

#endif // ANALYTICSENGINE_MQH
//+------------------------------------------------------------------+

RunAnalysis() contains no branching on repository type. It calls the same six methods regardless of whether the pointer resolves to CLiveTradeRepository or CMockTradeRepository. The vtable handles dispatch at runtime. This is the point the architecture is built around: analytics code that is entirely agnostic to its data source.


Data Ownership Model

CLiveTradeRepository does not store trade records persistently. It queries the History API on each method call and computes results from the live terminal state. This means its results reflect the account's current state at the moment of the call, which is appropriate for production use but makes the output non-deterministic across calls if trades close during the EA's session.

CMockTradeRepository owns its trade data internally as a fixed array of STradeRecord structures. The array is populated at construction time and does not change during the EA's lifetime. Every method call against the mock repository returns the same result for the same input, which is the definition of a deterministic data source.

This distinction is the architectural justification for the interface. The consumer does not need to know whether it is receiving a snapshot from a live account or a fixed array from a test dataset. It receives an ITradeRepository* and calls methods on it. The contract guarantees the same method signatures in both cases.

Decoupled Win-Rate Request

Figure 2: Decoupled Win-Rate Request. CAnalyticsEngine calls GetWinRate() on the ITradeRepository interface and receives the result. Everything behind the interface — the live repository and the History API it reads — is hidden from the engine, so the data source can be swapped without changing the caller.

Mock Win-Rate Request

Figure 3: Mock Win-Rate Request. The same CAnalyticsEngine makes the same GetWinRate() call through ITradeRepository, but here CMockTradeRepository returns data from an in-memory array. No terminal connection, broker account, or History API call is involved — and the engine's compiled code is identical to Figure 2. Only the implementation behind the interface changed.


Polymorphic Dispatch Overhead

Each method call through ITradeRepository* involves a vtable lookup: the pointer's vtable address is read, the correct method entry is located, and execution branches to the implementing function. This entails two memory reads and one indirect branch, typically completing in three to five nanoseconds on a warm cache.

For analytics computations that run once in OnInit() or once per bar close, this overhead is architecturally irrelevant. For a risk manager that calls GetWinRate() on every tick of a volatile instrument, the overhead accumulates. At one hundred ticks per second with a five-nanosecond dispatch cost, the total overhead is 500 ns of CPU time per second (~5e-7 s/s), which is below the measurement noise floor for any MQL5 EA.

The more significant cost is the method call abstraction itself. A direct HistoryDealGetDouble() loop computes the win rate in one pass over the deal list. The repository pattern adds one level of indirection per method call. GetWinRate(), GetTotalProfit(), and GetMaxDrawdown() each iterate the trade list independently. As a result, the live repository performs three traversals, while a direct implementation could combine them into one. This is a design trade-off: the decoupling benefit justifies the traversal overhead for all but the most latency-critical analytics paths.


Testability and Deterministic Edge-Case Simulation

The mock repository's primary value is not performance but controllability. With CMockTradeRepository, the developer can construct any trade dataset in code and verify that the analytics engine handles it correctly before the EA ever executes on a live account. The following scenarios, which are difficult or impossible to reproduce reliably with a live account, become trivial with a mock repository.

An empty trade history tests whether the analytics engine handles zero-count datasets without division-by-zero errors. A dataset with all losing trades verifies that the win rate returns zero rather than a negative value. A dataset where every trade has identical profit tests the edge cases of average trade computation. A dataset with a single extreme outlier trade tests whether drawdown calculation is sensitive to trade ordering.

Each of these scenarios is a single constructor call with a different hardcoded dataset. No broker, no account, no market data feed is required. The outputs are fully reproducible across machines, platforms, and time.


Equity Curve Rendering from Repository Data

The CEquityCurvePanel class iterates all trades from the repository in sequence and plots a running cumulative profit line using CCanvas. Because it depends only on ITradeRepository*, it renders identically whether the data comes from a live account or a mock dataset. This is demonstrated in the EA's OnInit(), where the equity curve is rendered from mock data before any connection to a broker is required.


Figure 4: CCanvas-rendered equity curve panel generated entirely from mock repository data.

Figure 4: CCanvas-rendered equity curve panel generated entirely from mock repository data.


CEquityCurvePanel: Rendering the Curve

CEquityCurvePanel uses CCanvas to draw a cumulative profit curve on the chart. Its Render() method takes an ITradeRepository*, iterates all trades via GetClosedTrade() in sequence, accumulates a running equity value, and passes the resulting array to DrawCurve(). Because the panel depends only on the interface, it renders identically from live or mock data. The title bar writes "Equity Curve — LIVE" or "Equity Curve — MOCK" based on GetRepositoryType():

//+------------------------------------------------------------------------+
//| Render                                                                 |
//| Purpose: Updates internal buffers and prompts real-time redraw updates |
//+------------------------------------------------------------------------+
void CEquityCurvePanel::Render(ITradeRepository *repo)
  {
   if(!m_initialized || repo == NULL)
     {
      return;
     }

   int count = repo.GetTradeCount();
   if(count == 0)
     {
      return;
     }

   double equity[];
   ArrayResize(equity, count);

//--- Parse total net returns over individual asset classes
   double running = 0.0;
   for(int i = 0; i < count; i++)
     {
      STradeRecord rec = repo.GetClosedTrade(i);
      running      += rec.profit + rec.commission + rec.swap;
      equity[i]     = running;
     }

//--- DrawCurve calls DrawGrid internally which draws the title bar
//--- background band. After DrawCurve returns, repo is in scope so
//    the title text can be written on top of the band correctly.
   DrawCurve(equity, count);

//--- Write title text after DrawCurve so repo pointer is available.
//--- GetRepositoryType() returns "LIVE" or "MOCK" depending on which
//--- concrete implementation was injected at construction time.
   string title = "Equity Curve — " + repo.GetRepositoryType();
   m_canvas.TextOut(8, 4, title, ColorToARGB(m_text_color, 255));

   DrawLabels(repo);

   m_canvas.Update();
  }

DrawLabels() calls GetWinRate(), GetAverageTrade(), and GetMaxDrawdown() directly on the repository to populate the summary bar at the bottom of the panel. No cached values from CAnalyticsEngine are needed here. The panel is self-contained:

//+----------------------------------------------------------------------+
//| DrawLabels                                                           |
//| Purpose: Prints core summary matrix indicators inside bottom margins |
//+----------------------------------------------------------------------+
void CEquityCurvePanel::DrawLabels(ITradeRepository *repo)
  {
   string label = "Win Rate: " + DoubleToString(repo.GetWinRate(), 2) + "%" +
                  "  |  Avg Trade: " + DoubleToString(repo.GetAverageTrade(), 2) +
                  "  |  Max DD: -" + DoubleToString(repo.GetMaxDrawdown(), 2);

   m_canvas.TextOut(8, m_height - 22, label, ColorToARGB(m_text_color, 200));
  }


Long-Term Maintainability

The repository pattern's maintainability advantage becomes measurable when the data source changes. To add a CSV-backed repository, implement ITradeRepository in a new class that reads from a file. The analytics engine, the equity curve panel, the risk manager, and the position sizing module all continue to work without modification. The EA selects the new repository at construction time by changing one pointer assignment.

Without the repository pattern, adding a CSV data source would require modifying every direct History API call throughout the analytics code, verifying that the CSV parsing logic handles all the edge cases that the History API was previously responsible for, and re-testing every analytics function against the new data source. The change surface is proportional to the number of direct API call sites. With the repository pattern, the change surface is exactly one: the new concrete implementation class.


Wiring Everything Together in the EA

RepositoryPatternEA.mq5 is where all components are instantiated and connected. Both repositories are allocated in OnInit(). The active repository is selected by a single pointer assignment based on the inp_use_mock_repository input. The analytics engine and the equity curve panel each receive only an ITradeRepository*. When inp_run_both_repositories is true, the EA runs the same analytics code against both repositories and prints both result sets, demonstrating that the output format is identical regardless of the data source:

//+------------------------------------------------------------------+
//| OnInit                                                           |
//| Purpose: Expert initialization function. Allocates repositories, |
//|          runs base benchmarking, and establishes panel views.    |
//+------------------------------------------------------------------+
int OnInit(void)
  {
   datetime now       = TimeCurrent();
   datetime from_date = now - (datetime)(inp_history_days * 86400);

   //--- Construct both repository implementations
   g_live_repo = new CLiveTradeRepository(from_date, now, inp_magic_filter);
   g_mock_repo = new CMockTradeRepository();

   if(CheckPointer(g_live_repo) != POINTER_DYNAMIC || CheckPointer(g_mock_repo) != POINTER_DYNAMIC)
     {
      Print("[RepositoryPatternEA] Failed to allocate repository instances.");
      return(INIT_FAILED);
     }

   //--- Select active repository based on input
   g_repository = inp_use_mock_repository ? (ITradeRepository *)g_mock_repo : (ITradeRepository *)g_live_repo;

   //--- Construct analytics engine bound to active repository
   g_analytics = new CAnalyticsEngine(g_repository);
   if(CheckPointer(g_analytics) != POINTER_DYNAMIC)
     {
      Print("[RepositoryPatternEA] Failed to allocate CAnalyticsEngine.");
      return(INIT_FAILED);
     }

   //--- Run analytics on the active repository
   datetime analysis_date = now - (now % 86400);
   g_analytics.RunAnalysis(analysis_date);

   if(inp_enable_repository_logs)
     {
      g_analytics.PrintReport();
     }

   //--- Optionally run both repositories with identical analytics code
   if(inp_run_both_repositories)
     {
      Print("=== LIVE REPOSITORY RESULTS ===");
      RunAndPrintAnalytics(g_live_repo, analysis_date);

      Print("=== MOCK REPOSITORY RESULTS ===");
      RunAndPrintAnalytics(g_mock_repo, analysis_date);
     }

   //--- Construct and render equity curve panel from mock repository
   //--- This demonstrates that the panel operates without terminal history
   g_panel = new CEquityCurvePanel(0, inp_panel_x, inp_panel_y, inp_panel_width, inp_panel_height);

   if(CheckPointer(g_panel) != POINTER_DYNAMIC)
     {
      Print("[RepositoryPatternEA] Failed to allocate CEquityCurvePanel.");
      return(INIT_FAILED);
     }

   if(!g_panel.Create())
     {
      Print("[RepositoryPatternEA] Failed to create equity curve canvas.");
      return(INIT_FAILED);
     }

   //--- Render equity curve from mock data: no broker connection required
   g_panel.Render(g_mock_repo);

   PrintFormat("[RepositoryPatternEA] Initialized. Active repository: %s | Lookback: %s days | Magic filter: %s",
               g_repository.GetRepositoryType(),
               IntegerToString(inp_history_days),
               (inp_magic_filter == 0) ? "ALL" : IntegerToString((int)inp_magic_filter));

   return(INIT_SUCCEEDED);
  }

The g_panel.Render(g_mock_repo) call on the last line before the return is the concrete demonstration that the equity curve panel requires no broker connection. It renders entirely from the in-memory dataset at initialization time, before any market data is received and before OnTick() is ever called.

OnDeinit() explicitly deletes every dynamic allocation and nulls every pointer. The order matters: the panel is deleted first because it holds a CCanvas resource. The analytics engine is deleted next. The two concrete repositories are deleted last. The interface pointer g_repository is set to null but not deleted because it points to memory already owned and freed by one of the two concrete repository pointers:

//+------------------------------------------------------------------+
//| OnDeinit                                                         |
//| Purpose: Expert deinitialization function. Performs orderly state|
//|          cleanup and releases dynamic pointer memory trees.      |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   //--- Destroy allocated dynamic instances explicitly to prevent leaks
   if(CheckPointer(g_panel) == POINTER_DYNAMIC)
     {
      delete g_panel;
      g_panel = NULL;
     }
   if(CheckPointer(g_analytics) == POINTER_DYNAMIC)
     {
      delete g_analytics;
      g_analytics = NULL;
     }
   if(CheckPointer(g_live_repo) == POINTER_DYNAMIC)
     {
      delete g_live_repo;
      g_live_repo = NULL;
     }
   if(CheckPointer(g_mock_repo) == POINTER_DYNAMIC)
     {
      delete g_mock_repo;
      g_mock_repo = NULL;
     }

   g_repository = NULL;

   PrintFormat("[RepositoryPatternEA] Deinitialized. Reason code: %d.", reason);
  }


Conclusion

Direct calls to the MetaTrader 5 History API couple analytics components to live terminal states. This tight coupling blocks isolated testing, complicates maintenance, and forces components to rely on unstated assumptions about terminal state.

The repository pattern resolves these issues by making data dependencies explicit and injectable via an ITradeRepository* pointer. The analytics layer simply requests data. It remains completely indifferent to whether that data originates from a live broker server or a mock dataset.

This architectural shift introduces specific trade-offs:

  • The Costs: It adds vtable dispatch overhead per method call, requires maintaining an interface class alongside its implementations, and may result in multiple list traversals.
  • The Benefits: It enables offline analytics testing without a broker connection, simplifies edge-case verification in controlled environments, and allows seamless data source swapping via a single pointer assignment.

For a production Expert Advisor designed for long-term deployment, these minimal runtime and structural overheads are a highly favorable exchange for comprehensive testability and structural flexibility.


Programs used in the article:

# Name Type Description
1 TradeRecord.mqh Include File STradeRecord struct defining the canonical trade record used across all repository implementations and consumers
2 ITradeRepository.mqh Include File Abstract base class defining the repository contract with pure virtual methods for all data access operations
3 LiveTradeRepository.mqh Include File CLiveTradeRepository implementing the contract via HistorySelect(), deal enumeration, and live profit/loss calculation
4 MockTradeRepository.mqh Include File CMockTradeRepository implementing the contract via a hardcoded in-memory STradeRecord array for deterministic offline testing
5 AnalyticsEngine.mqh Include File CAnalyticsEngine computing win rate, total profit, average trade, and max drawdown exclusively through ITradeRepository*
6 EquityCurvePanel.mqh Include File CEquityCurvePanel rendering a cumulative equity curve using CCanvas from repository trade data
7 RepositoryPatternEA.mq5 Demo EA Demonstration EA wiring both repository implementations to the same analytics engine, printing matching outputs, and rendering an equity curve from mock data in OnInit()
8 Repository_Pattern.zip Zip Archive Zip archive containing all the attached files and their paths relative to the terminal's root folder.
MQL5 Wizard Techniques you should know (Part 96): Using Wavelet Thresholding and LSTM Network in a Custom Money Management Class MQL5 Wizard Techniques you should know (Part 96): Using Wavelet Thresholding and LSTM Network in a Custom Money Management Class
In this article we consider a custom MQL5 Wizard class that processes Money Management. Our custom class is labelled ‘CMoneyWaveletLSTM’, and is developed by combining the Wavelet Thresholding algorithm with an LSTM network. As has been the case throughout these series, the developed model is testable with MQL5 Wizard-Assembled Expert Advisors that can be tuned with different trailing stops and entry Signals classes. We maintain our entry Signal, as in past articles as the built-in 'Envelopes' class and the RSI class.
Competitive Learning Algorithm (CLA) Competitive Learning Algorithm (CLA)
The article presents the Competitive Learning Algorithm (CLA), a new metaheuristic optimization method based on simulating the educational process. The algorithm organizes the population of solutions into classes with students and teachers, where agents learn through three mechanisms: following the best in the class, using personal experience, and sharing knowledge between classes.
Graph Theory: Network Flow of Commodities (Ford-Fulkerson Algorithm), Used as a Liquidity-Capacity Engine Graph Theory: Network Flow of Commodities (Ford-Fulkerson Algorithm), Used as a Liquidity-Capacity Engine
The article presents an MQL5 Expert Advisor that adapts the Ford–Fulkerson max-flow method into a liquidity-capacity filter. Market structures—Swing Highs/Lows, Fair Value Gaps, Order Blocks, and Liquidity Pools—form a directed graph with edge capacities from volume, price reaction, distance, and structure quality. Maximum flow qualifies ICT setups, filters weak paths, and drives dynamic position sizing for a consistent, two-stage decision process.
Gaussian Processes in Machine Learning (Part 1): Classification Model in MQL5 Gaussian Processes in Machine Learning (Part 1): Classification Model in MQL5
The article considers the classification model of Gaussian processes. We will start by studying its theoretical principles moving on to the practical development of the GP library in MQL5.