The Repository Pattern in MQL5: Abstracting Trade History Access for Testable EA Logic
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.

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.

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.

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.
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. |
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
MQL5 Wizard Techniques you should know (Part 96): Using Wavelet Thresholding and LSTM Network in a Custom Money Management Class
Competitive Learning Algorithm (CLA)
Graph Theory: Network Flow of Commodities (Ford-Fulkerson Algorithm), Used as a Liquidity-Capacity Engine
Gaussian Processes in Machine Learning (Part 1): Classification Model in MQL5
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use