Паттерн "Репозиторий" в MQL5: Абстрагирование доступа к торговой истории для обеспечения тестируемости логики советника
Введение
Аналитический модуль советника (EA), который напрямую вызывает функции HistorySelect(), HistoryDealGetDouble() и HistoryDealGetInteger() внутри своих методов расчёта, создаёт скрытые зависимости от состояния терминала MetaTrader 5. Например, такой метод, как CalculateWinRate(), выглядит как чистая функция. На практике для работы метода требуются активное соединение с брокером и загруженная история счета. Кроме того, требуется предварительный вызов функции HistorySelect() с указанием правильного диапазона дат. Если какое-либо из этих условий не выполняется, метод возвращает некорректные результаты, не генерируя при этом обнаруживаемой ошибки.
Такая сильная связанность создает серьезные ограничения при тестировании и сопровождении. Аналитический модуль, написанный таким образом, невозможно запустить изолированно. Для модульного тестирования метода CalculateWinRate() нужны работающий терминал и активное подключение. Также понадобится торговый счет с соответствующими историческими данными. Проверять граничные случаи, например пустую историю, в больших масштабах практически неосуществимо. Это вынуждает разработчика либо вручную очищать данные терминала, либо встраивать условную логику для тестирования в рабочий код.
Затраты на сопровождение нарастают по мере роста кодовой базы:
- Изменения в API: если брокер изменяет свою схему классификации сделок, каждый метод расчета, использующий функцию HistoryDealGetInteger(), необходимо обновить вручную.
- Препятствия для переноса: перенос советника к другому брокеру или на другую архитектуру требует отделения аналитического кода от слоя доступа к данным.
- Дублирование кода: новые модули должны либо дублировать схему извлечения данных, либо опираться на недокументированное предположение о том, что другой модуль уже инициализировал состояние истории.

Рисунок 1: Применение принципа инверсии зависимостей. Аналитические компоненты-потребители теперь зависят от абстракции ITradeRepository, а не от конкретных источников данных, что позволяет использовать взаимозаменяемые реализации рабочего и тестового репозиториев.
Слева показана исходная архитектура с сильной связанностью, в которой аналитические компоненты напрямую зависят от API торговой истории MQL5. Справа показана архитектура на основе репозитория, в которой все потребители зависят только от интерфейса ITradeRepository. Эта абстракция устраняет связанность аналитического кода с базовым источником данных и позволяет заменять рабочий репозиторий тестовым и наоборот без изменения логики потребителей.
Паттерн "Репозиторий" как механизм инверсии зависимостей
Паттерн "Репозиторий" вводит слой контрактов между потребителями и источниками данных. Потребители зависят от интерфейса. Интерфейс определяет, какие операции с данными доступны. Конкретные реализации выполняют эти операции посредством взаимодействия со своим базовым источником данных. Потребитель не знает о конкретной реализации и не зависит от неё.
В MQL5 эта структура реализована с помощью абстрактного базового класса с чисто виртуальными методами. Каждый аналитический компонент получает указатель на ITradeRepository, а не указатель на какую-либо конкретную реализацию. При создании аналитических компонентов советник внедряет в них либо CLiveTradeRepository, либо CMockTradeRepository. Аналитический код в обоих случаях одинаков. Результаты одинаковы, если тестовые данные сформированы так, чтобы совпадать с данными рабочего репозитория. Механизм доступа к исходным данным совершенно иной.
Структура STradeRecord
Каждая реализация репозитория и каждый аналитический компонент в этой архитектуре используют единую каноническую структуру данных: STradeRecord. Определение этой структуры в отдельном подключаемом файле гарантирует, что ни один компонент не будет иметь собственной версии записи о сделке и что на границе между слоем данных и слоем аналитики не будет неявных преобразований типов.
//+------------------------------------------------------------------+ //| 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 //+------------------------------------------------------------------+
Конструктор инициализирует все поля безопасными значениями по умолчанию. Это важно, поскольку метод GetClosedTrade() в обеих реализациях репозитория возвращает объект STradeRecord, созданный конструктором по умолчанию, при недопустимом индексе или неудачной выборке истории. Вызывающий код получает обнуленную запись вместо чтения мусорных данных из памяти.
Интерфейс ITradeRepository
ITradeRepository — точка инверсии зависимостей. Это абстрактный базовый класс с чисто виртуальными методами. Ни один потребитель в этой архитектуре не хранит указатель непосредственно на CLiveTradeRepository или CMockTradeRepository. Каждый потребитель хранит указатель типа ITradeRepository*. Именно благодаря этому замена рабочего репозитория тестовым сводится к одному переназначению указателя, а не к рефакторингу всей кодовой базы.
//+------------------------------------------------------------------+ //| 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 //+------------------------------------------------------------------+
Семь виртуальных методов определяют функциональные возможности репозитория. Синтаксис = 0 обеспечивает соблюдение контракта: любой класс, наследующий ITradeRepository, но не реализующий все семь методов, не может быть скомпилирован. Метод GetRepositoryType() возвращает "LIVE" или "MOCK" и используется в строке заголовка панели кривой средств, чтобы показать, какой источник данных активен в момент отрисовки.
Архитектурные слои
| Слой | Зона ответственности |
|---|---|
| Логика советника | Принятие решений, связывание компонентов, внедрение репозитория |
| Интерфейс репозитория (ITradeRepository) | Контракт абстракции; определяет доступные операции |
| Конкретная реализация репозитория | Извлечение данных из API торговой истории или тестового набора данных |
| Источник данных | API торговой истории, массив в памяти, файл CSV или база данных |
CLiveTradeRepository: реализация контракта с использованием API торговой истории
CLiveTradeRepository реализует интерфейс ITradeRepository, обращаясь к API торговой истории MetaTrader 5 при каждом вызове метода. Конструктор принимает диапазон дат и фильтр по магическому номеру. Закрытый вспомогательный метод SelectHistory() вызывает HistorySelect(), чтобы заполнить кэш сделок терминала до начала перебора. Закрытый вспомогательный метод IsEntryDeal() исключает открывающие сделки, оставляя только записи DEAL_ENTRY_OUT и DEAL_ENTRY_INOUT, соответствующие завершению сделок.
//+------------------------------------------------------------------+ //| 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); };
Конструктор просто сохраняет три параметра, которые определяют область действия каждого последующего запроса:
//+------------------------------------------------------------------+ //| 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() и IsEntryDeal() — единственные два метода во всей кодовой базе, которые напрямую взаимодействуют с API торговой истории. Каждый открытый метод делегирует логику фильтрации этим двум вспомогательным методам, поэтому любые будущие изменения в выборе истории или классификации сделок по типу входа/выхода затронут ровно два закрытых метода и никакой другой код:
//+------------------------------------------------------------------+ //| 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() демонстрирует общую схему работы всех открытых методов: сначала проверяется успешность выборки истории, затем выполняется перебор с фильтрацией по магическому номеру и типу входа/выхода на каждой итерации:
//+------------------------------------------------------------------+ //| 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() сопоставляет виртуальный порядковый индекс со сделкой в отфильтрованной последовательности и заполняет структуру STradeRecord. Такие потребители, как CEquityCurvePanel, вызывают этот метод в последовательном цикле с индексом от 0 до GetTradeCount() - 1, не зная, что исходные данные поступают из кэша сделок терминала:
//+------------------------------------------------------------------+ //| 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); }
Остальные методы расчета показателей — GetDailyPnL(), GetWinRate(), GetTotalProfit(), GetMaxDrawdown() и GetAverageTrade() — используют ту же схему перебора. При расчёте чистого результата каждый из них учитывает прибыль, комиссию и своп, чтобы отразить брокерские издержки. Метод GetAverageTrade() делегирует вычисления методам GetTradeCount() и GetTotalProfit(), вместо того чтобы перебирать историю в третий раз:
//+------------------------------------------------------------------+ //| 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
Методы интерфейса репозитория
| Метод | Тип возвращаемого значения | Назначение |
|---|---|---|
| GetTradeCount() | int | Возвращает общее количество закрытых сделок в наборе данных |
| GetClosedTrade(int index) | STradeRecord | Возвращает запись о сделке по указанному индексу |
| GetDailyPnL(datetime date) | double | Возвращает чистую прибыль/убыток по сделкам, закрытым в указанный день |
| GetWinRate() | double | Возвращает долю прибыльных сделок в процентах |
| GetTotalProfit() | double | Возвращает сумму прибылей по всем сделкам |
| GetMaxDrawdown() | double | Возвращает максимальную просадку средств от пика до последующего минимума в наборе данных |
| GetAverageTrade() | double | Возвращает среднюю прибыль на сделку |
Реализации репозиториев
| Репозиторий | Источник данных | Основной сценарий использования |
|---|---|---|
| CLiveTradeRepository | API торговой истории (HistorySelect, HistoryDealGetDouble) | Советник для эксплуатации на реальном или демонстрационном счете |
| CMockTradeRepository | Массив STradeRecord в памяти с жестко заданными данными | Офлайн-тестирование, конвейеры непрерывной интеграции, проверка параметров |
| Будущий репозиторий на основе CSV-файлов | Файловая система через FileOpen / FileReadString | Воспроизведение результатов бэктестинга, межсессионный анализ |
| Будущий репозиторий на основе базы данных | Внешнее хранилище через WebRequest | Облачная аналитика, агрегация данных по нескольким счетам |
CMockTradeRepository: реализация контракта с использованием массива в памяти
CMockTradeRepository реализует тот же интерфейс без единого вызова API торговой истории. Его хранилище данных представляет собой фиксированный массив STradeRecord, который заполняется при создании объекта методом BuildDataset(). Каждый вызов метода тестового репозитория возвращает один и тот же результат при одинаковых входных данных на любом компьютере, в любом сеансе и у любого брокера. Именно это делает его полезным для тестирования.
//+------------------------------------------------------------------+ //| 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() формирует набор данных из 48 сделок, включающий 29 прибыльных и 19 убыточных. Значения прибыли, торговые символы и временные метки детерминированы. К каждой сделке применяется комиссия в размере -0,70, чтобы показатели отражали результат с учётом издержек, а не валовую прибыль. Набор данных сформирован таким образом, что доля прибыльных сделок составляет примерно 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); } }
Поскольку тестовый репозиторий владеет данными, хранящимися в обычном массиве, работа метода GetClosedTrade() сводится к проверке границ индекса и прямому чтению из массива. Нет ни перебора элементов, ни вызовов API, ни возможности изменения результата между вызовами:
//+------------------------------------------------------------------+ //| 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]); }
Методы расчета показателей выполняют перебор непосредственно по массиву m_trades[]. Метод GetWinRate() использует ту же логику расчета результата с учетом издержек, что и реализация рабочего репозитория (прибыль плюс комиссия плюс своп), поэтому оба репозитория выдают сопоставимые значения при одинаковых наборах данных:
//+------------------------------------------------------------------+ //| 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 //+------------------------------------------------------------------+
Независимость модулей-потребителей
Каждый аналитический модуль-потребитель в этой архитектуре зависит только от ITradeRepository*. Модуль управления рисками использует этот интерфейс для получения результатов недавних сделок, необходимых для расчета размера позиции. Аналитический модуль использует этот интерфейс для расчета доли прибыльных сделок, средней прибыли на сделку и просадки. Панель кривой средств использует этот интерфейс для последовательного перебора всех сделок и построения кривой накопленной прибыли. Ни один из этих модулей не содержит ни одного вызова функций API торговой истории.
Модули-потребители
| Модуль | Зависит от | Используемые операции с данными |
|---|---|---|
| CAnalyticsEngine | ITradeRepository* | GetWinRate(), GetTotalProfit(), GetAverageTrade(), GetMaxDrawdown() |
| CEquityCurvePanel | ITradeRepository* | GetTradeCount(), GetClosedTrade() |
| Модуль управления рисками (уровень советника) | ITradeRepository* | GetDailyPnL(), GetWinRate() |
| Модуль расчета размера позиции | ITradeRepository* | GetAverageTrade(), GetMaxDrawdown() |
CAnalyticsEngine: использование интерфейса
CAnalyticsEngine содержит единственное поле типа ITradeRepository*. Его конструктор принимает указатель, передаваемый извне, поэтому вызывающая сторона решает, будет ли аналитика работать с данными рабочего или тестового репозитория. Сам аналитический движок в этом выборе не участвует. Метод RunAnalysis() вызывает шесть методов репозитория и кэширует результаты. Метод PrintReport() форматирует эти кэшированные значения и выводит их в журнал терминала:
//+------------------------------------------------------------------+ //| 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() не содержит ветвлений по типу репозитория. Он вызывает одни и те же шесть методов независимо от того, на какую реализацию ссылается указатель — на CLiveTradeRepository или на CMockTradeRepository. Таблица виртуальных методов обеспечивает диспетчеризацию вызовов во время выполнения. Именно на этом и основана архитектура: аналитический код, который полностью независим от источника данных.
Модель владения данными
CLiveTradeRepository не хранит записи о сделках между вызовами методов. При каждом вызове метода он обращается к API торговой истории и вычисляет результаты на основе текущего состояния терминала. Это означает, что его результаты отражают состояние счета на момент вызова, что подходит для эксплуатации, но делает результаты повторных вызовов недетерминированными, если во время работы советника закрываются сделки.
CMockTradeRepository владеет своими торговыми данными, храня их внутри объекта в виде фиксированного массива структур STradeRecord. Массив заполняется при создании объекта и не изменяется в течение всего времени работы советника. Каждый вызов метода тестового репозитория возвращает один и тот же результат при одинаковых входных данных, что и является определением детерминированного источника данных.
Именно это различие служит архитектурным обоснованием для данного интерфейса. Потребителю не нужно знать, получает ли он снимок данных реального торгового счета или фиксированный массив из тестового набора данных. Он получает указатель ITradeRepository* и вызывает через него методы. Контракт гарантирует, что сигнатуры методов будут одинаковыми в обоих случаях.

Рисунок 2: Запрос доли прибыльных сделок без зависимости от источника данных. CAnalyticsEngine вызывает метод GetWinRate() через интерфейс ITradeRepository и получает результат. Всё, что находится за интерфейсом (рабочий репозиторий и API торговой истории, через который он читает данные), скрыто от аналитического движка, поэтому источник данных можно заменить без изменения вызывающего кода.

Рисунок 3: Запрос доли прибыльных сделок у тестового репозитория. Тот же CAnalyticsEngine выполняет тот же вызов GetWinRate() через ITradeRepository, но здесь CMockTradeRepository возвращает данные из массива в памяти. Здесь не требуется ни подключение терминала к брокеру, ни брокерский счёт, ни вызов API торговой истории, а скомпилированный код аналитического движка остается таким же, как в случае на рисунке 2. Изменилась лишь реализация, скрытая за интерфейсом.
Накладные расходы, связанные с полиморфной диспетчеризацией
Каждый вызов метода через ITradeRepository* требует обращения к таблице виртуальных методов: считывается адрес таблицы виртуальных методов объекта, на который указывает указатель, находится запись нужного метода, и выполнение переходит к функции, реализующей этот метод. Это требует двух операций чтения из памяти и одного непрямого перехода, что, как правило, занимает от трёх до пяти наносекунд при "разогретом" кэше.
Для аналитических вычислений, выполняющихся один раз в функции OnInit() или один раз при закрытии каждого бара, эти накладные расходы с точки зрения архитектуры не имеют значения. Для модуля управления рисками, который вызывает метод GetWinRate() на каждом тике волатильного инструмента, накладные расходы накапливаются. При частоте 100 тиков в секунду и затратах на диспетчеризацию в 5 наносекунд общие накладные расходы составляют 500 нс процессорного времени в секунду (~5e-7 с/с), что ниже уровня шума измерений для любого советника на MQL5.
Более значительные затраты связаны с самой абстракцией вызова метода. Цикл с прямыми вызовами HistoryDealGetDouble() вычисляет долю прибыльных сделок за один проход по списку сделок. Паттерн "Репозиторий" добавляет один уровень косвенности при каждом вызове метода. Методы GetWinRate(), GetTotalProfit() и GetMaxDrawdown() независимо друг от друга обходят список сделок. В результате рабочий репозиторий выполняет три обхода, тогда как в реализации с прямыми вызовами API их можно было бы объединить в один. Это архитектурный компромисс: преимущества устранения связанности оправдывают накладные расходы на обходы во всех сценариях аналитических вычислений, кроме наиболее чувствительных к задержкам.
Тестируемость и детерминированное моделирование граничных случаев
Основная ценность тестового репозитория — не производительность, а возможность контролировать данные. С помощью CMockTradeRepository разработчик может создать в коде любой набор данных о сделках и убедиться, что аналитический модуль правильно его обрабатывает, ещё до того, как советник начнёт работать на реальном счёте. Следующие сценарии сложно или невозможно надёжно воспроизвести на реальном счёте, но с тестовым репозиторием это сделать легко.
Пустая история торговых операций позволяет проверить, способен ли аналитический модуль обрабатывать наборы данных с нулевым количеством записей без ошибок деления на ноль. Набор данных, состоящий только из убыточных сделок, позволяет проверить, что расчёт доли прибыльных сделок возвращает ноль, а не отрицательное значение. Набор данных, в котором каждая сделка приносит одинаковую прибыль, позволяет проверить граничные случаи при расчёте средней прибыли на сделку. Набор данных с одной сделкой, результат которой резко отличается от остальных, позволяет проверить, зависит ли расчёт просадки от порядка сделок в наборе.
Каждый из этих сценариев реализуется одним вызовом конструктора со своим набором данных, жёстко заданным в коде. Не требуется ни брокер, ни торговый счет, ни канал рыночных данных. Результаты полностью воспроизводимы на разных компьютерах, платформах и в разное время.
Визуализация кривой средств на основе данных репозитория
Класс CEquityCurvePanel последовательно перебирает все сделки из репозитория и с помощью CCanvas строит линию накопленной прибыли. Поскольку класс зависит только от ITradeRepository*, он одинаково строит график независимо от того, поступают ли данные с реального счёта или из тестового набора данных. Это демонстрируется в функции OnInit() советника, где кривая средств строится на основе тестовых данных еще до того, как потребуется подключение к брокеру.

Рисунок 4: Панель кривой средств, отрисованная с помощью CCanvas исключительно по данным тестового репозитория.
CEquityCurvePanel: отображение кривой
CEquityCurvePanel использует CCanvas для построения кривой накопленной прибыли на графике. Его метод Render() принимает указатель типа ITradeRepository*, последовательно перебирает все сделки с помощью GetClosedTrade(), накапливает текущий результат сделок и передает полученный массив в DrawCurve(). Поскольку панель зависит только от интерфейса, она одинаково отображает данные как рабочего, так и тестового репозитория. В строке заголовка отображается надпись "Equity Curve — LIVE" или "Equity Curve — MOCK" в зависимости от результата вызова 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() напрямую вызывает методы GetWinRate(), GetAverageTrade() и GetMaxDrawdown() репозитория для заполнения сводной строки в нижней части панели. Здесь не требуются никакие значения из кэша CAnalyticsEngine. Панель самодостаточна:
//+----------------------------------------------------------------------+ //| 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)); }
Удобство сопровождения в долгосрочной перспективе
Преимущество паттерна "Репозиторий" с точки зрения удобства сопровождения можно оценить при смене источника данных. Чтобы добавить репозиторий на основе CSV, реализуйте интерфейс ITradeRepository в новом классе, который считывает данные из файла. Аналитический движок, панель кривой средств, модуль управления рисками и модуль расчета размера позиции продолжают работать без каких-либо изменений. Советник выбирает новый репозиторий при создании объектов: для этого достаточно изменить одну операцию присваивания указателя.
Без паттерна "Репозиторий" добавление источника данных в формате CSV потребовало бы изменения каждого прямого вызова API торговой истории во всем аналитическом коде, проверки того, что логика разбора CSV обрабатывает все граничные случаи, за которые ранее отвечал API торговой истории, а также повторного тестирования каждой аналитической функции с новым источником данных. Количество участков кода, требующих изменения, пропорционально числу мест прямого вызова API. При использовании паттерна "Репозиторий" изменения затрагивают ровно один компонент — новый класс конкретной реализации.
Объединение всех компонентов в советнике
В файле RepositoryPatternEA.mq5 создаются экземпляры всех компонентов и устанавливаются связи между ними. Оба репозитория создаются в динамической памяти в функции OnInit(). Активный репозиторий выбирается одним присваиванием указателю в зависимости от значения входного параметра inp_use_mock_repository. И аналитический модуль, и панель кривой средств получают только указатель типа ITradeRepository*. Когда параметр inp_run_both_repositories имеет значение true, советник запускает один и тот же аналитический код для обоих репозиториев и выводит оба набора результатов, демонстрируя, что формат вывода остается одинаковым независимо от источника данных:
//+------------------------------------------------------------------+ //| 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); }
Вызов g_panel.Render(g_mock_repo) в последней строке перед оператором return наглядно демонстрирует, что панель кривой средств не требует подключения к брокеру. Панель отрисовывается исключительно на основе набора данных в памяти при инициализации — до получения каких-либо рыночных данных и до первого вызова OnTick().
Функция OnDeinit() явно освобождает все динамически выделенные области памяти и обнуляет все указатели. Порядок имеет значение: сначала удаляется панель, поскольку она содержит ресурс CCanvas. Затем удаляется аналитический модуль. В последнюю очередь удаляются два объекта конкретных реализаций репозитория. Указателю на интерфейс g_repository присваивается значение null, но удаление через него не выполняется, поскольку он указывает на память, которой владеет один из двух указателей на конкретные реализации репозитория и которая уже освобождена через этот указатель:
//+------------------------------------------------------------------+ //| 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); }
Заключение
Прямые вызовы API торговой истории MetaTrader 5 привязывают аналитические компоненты к текущему состоянию терминала. Такая сильная связанность препятствует изолированному тестированию, затрудняет сопровождение и вынуждает компоненты полагаться на неявные допущения о состоянии терминала.
Паттерн "Репозиторий" решает эти проблемы, делая зависимости от данных явными и позволяя внедрять их через указатель ITradeRepository*. Аналитический уровень просто запрашивает данные. Для него совершенно неважно, поступают ли эти данные с работающего сервера брокера или из тестового набора данных.
Такое изменение архитектуры предполагает определенные компромиссы:
- Издержки: при каждом вызове метода возникают накладные расходы на диспетчеризацию через таблицу виртуальных методов, требуется сопровождать класс интерфейса наряду с его реализациями, а также могут потребоваться многократные обходы списка.
- Преимущества: такой подход позволяет тестировать аналитику автономно, без подключения к брокеру, упрощает проверку граничных случаев в контролируемых условиях и позволяет легко менять источник данных одним присваиванием указателю.
Для советника, предназначенного для длительной эксплуатации в рабочей среде, эти минимальные накладные расходы при выполнении и усложнение структуры — вполне оправданная плата за всестороннюю тестируемость и структурную гибкость.
Программы, использованные в статье:
| # | Название | Тип | Описание |
|---|---|---|---|
| 1 | TradeRecord.mqh | Подключаемый файл | Структура STradeRecord, определяющая каноническую запись о сделке, используемую всеми реализациями репозитория и всеми потребителями |
| 2 | ITradeRepository.mqh | Подключаемый файл | Абстрактный базовый класс, определяющий контракт репозитория с чисто виртуальными методами для всех операций доступа к данным |
| 3 | LiveTradeRepository.mqh | Подключаемый файл | CLiveTradeRepository, реализующий контракт с помощью функции HistorySelect(), перебора сделок и расчета прибыли и убытка по данным терминала |
| 4 | MockTradeRepository.mqh | Подключаемый файл | CMockTradeRepository, реализующий контракт с помощью заданного в коде массива STradeRecord в памяти для детерминированного автономного тестирования |
| 5 | AnalyticsEngine.mqh | Подключаемый файл | CAnalyticsEngine, рассчитывающий долю прибыльных сделок, общую прибыль, среднюю прибыль на сделку и максимальную просадку исключительно через ITradeRepository* |
| 6 | EquityCurvePanel.mqh | Подключаемый файл | CEquityCurvePanel, отображающий с помощью CCanvas кривую средств по накопленному результату сделок на основе данных из репозитория |
| 7 | RepositoryPatternEA.mq5 | Демонстрационный советник | Демонстрационный советник, который в функции OnInit() подключает обе реализации репозитория к одному и тому же аналитическому модулю, выводит совпадающие результаты и строит кривую средств по тестовым данным |
| 8 | Repository_Pattern.zip | Архив в формате ZIP | ZIP-архив, содержащий все прикрепленные файлы и их пути относительно корневой папки терминала. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22958
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.