Память между перезапусками: Сохранение состояния советника с помощью двоичных файлов в MQL5
Введение
Обеспечение непрерывности состояния при перезапуске терминала имеет решающее значение для советников, отслеживающих показатели по нескольким сделкам. К примерам относятся ограничения количества сделок в день и схемы последовательного изменения объёма позиций. Поскольку стандартные переменные MQL5 хранятся исключительно в оперативной памяти, при каждом перезапуске терминала внутренние счетчики советника сбрасываются до значений по умолчанию. Это создаёт уязвимости в работе советника: при инициализации он может нарушить заданные ограничения риска или неверно рассчитать объёмы лотов. Стандартное обходное решение — использование глобальных переменных терминала — по своей структуре не подходит для реализации сложной логики. Эти переменные предназначены исключительно для хранения отдельных значений типа double, не имеют иерархической структуры данных и легко удаляются при переустановке терминала или обслуживании каталогов.
В данной статье представлена программная основа для сериализации состояния с использованием встроенных операций MQL5 с двоичными файлами. Она хранит текущее состояние советника в одной структуре, записывает его при ключевых событиях и повторно загружает при инициализации. Полученный подключаемый файл PersistenceManager.mqh предоставляет автономный механизм с низкими накладными расходами, который обеспечивает сохранение рабочего состояния советника при перезапусках, обновлениях платформы и смене профилей без зависимости от внешних баз данных или ненадёжных конфигураций терминала.
Раздел 1: Что советнику нужно помнить
Состояние и данные о позициях
Существует важное различие между данными о позициях и состоянием советника. Данные о позициях, включая открытые сделки, номера тикетов, цены входа и уровни стоп-лосса, уже хранятся на сервере брокера и всегда доступны советнику через стандартные функции работы с позициями и историей. Эти данные не нужно сохранять локально, поскольку они уже хранятся у вашего брокера.
Состояние советника — это другое. Речь идёт о переменных внутренней логики, которые советник хранит в памяти между тиками: счётчиках, флагах, множителях и учётных значениях, которые советник формирует самостоятельно и которые существуют только в работающем процессе. Когда этот процесс завершается, состояние теряется. Эти данные не хранятся ни у брокера, ни где-либо ещё, откуда MetaTrader мог бы их получить. Именно для сохранения этого состояния и предназначен файл PersistenceManager.mqh.
Типичные переменные состояния, которые стоит сохранять
Конкретные переменные, которые необходимо сохранять, зависят от логики советника, однако в большинстве стратегий, требующих сохранения состояния, повторяются определённые шаблоны. В приведенном ниже списке перечислены наиболее распространенные из них:
- Счётчик сделок за день: показывает, сколько сделок было открыто сегодня. Счётчик необходимо сбрасывать в начале каждого нового торгового дня, но его значение должно сохраняться при перезапусках в течение дня.
- Счётчики прибыльных и убыточных сделок подряд: используются в стратегиях с масштабированием объёма лота, мартингейле, антимартингейле и стратегиях, основанных на сериях.
- Текущий множитель лота: действующий коэффициент расчёта объёма позиции, определяемый на основе серии сделок или модели риска.
- Последнее состояние сигнала: направление последнего сигнала, по которому действовал советник; используется для предотвращения двойного входа по одному и тому же сигналу после перезапуска.
- Флаг частичного закрытия: указывает, фиксировал ли советник уже часть прибыли по текущей открытой сделке, и предотвращает повторную фиксацию после перезапуска.
- Максимальное значение средств на счёте за сессию: наибольшее значение средств, достигнутое в ходе текущей торговой сессии. Оно используется для расчёта скользящих лимитов просадки.
- Время последнего обработанного бара: указывает, на каком баре советник в последний раз выполнил действие, и предотвращает повторное срабатывание логики входа в рынок на том же баре после перезапуска.
Не все эти переменные нужны каждому советнику. Главное — состав структуры можно гибко менять. Включайте только те поля, которые фактически используются в данной стратегии, и добавляйте новые по мере развития логики. Подход с использованием двоичных файлов позволяет работать со структурами с произвольным составом и расположением полей без изменения кода чтения и записи.
Почему глобальные переменные терминала не справляются с задачей
В качестве решения для сохранения состояния часто предлагают использовать глобальные переменные терминала MQL5, доступ к которым осуществляется с помощью функций GlobalVariableSet() и GlobalVariableGet(). Они сохраняют данные при перезапусках терминала, просты в использовании и отлично подходят для хранения одного значения типа double. Проблема возникает, когда стратегии необходимо сохранять набор взаимосвязанных значений, представляющих единое состояние.
В отличие от современных языков программирования, в которых для группировки данных используются "объекты", глобальные переменные терминала MQL5 находятся в едином плоском пространстве имён. Это означает, что все советники на вашем терминале используют одну и ту же область хранения. Если два разных советника используют одно и то же имя переменной, например Total_Trades, они будут перезаписывать данные друг друга. Такой конфликт имён может привести к некорректной работе вашей стратегии, поскольку она считывает данные, предназначенные для совершенно другой программы. Чтобы этого избежать, разработчики вынуждены использовать длинные уникальные префиксы для каждой переменной, что затрудняет сопровождение кода.
В таблице 1 ниже сравниваются возможности сохранения состояния с помощью глобальных переменных терминала и двоичных файлов:
| Характеристика | Глобальная переменная терминала | Двоичный файл (FileWriteStruct) |
|---|---|---|
| Сохраняет данные при перезапусках терминала | Да (данные хранятся в терминале) | Да (данные хранятся на диске) |
| Сохраняет данные при переустановке терминала | Нет — данные удаляются при чистой установке | Да — файл остаётся на диске |
| Доступно для просмотра вне MetaTrader 5 | Нет | Да — с помощью любого шестнадцатеричного редактора или скрипта |
| Совместное использование в разных терминалах | Да, на одном и том же компьютере | Да — флаг FILE_COMMON |
| Управление версиями и форматом | Нет — только значение | Да — поле version в структуре |
| Хранит сложные структуры | Нет — только тип double | Да — любая структура фиксированного размера |
| Можно просматривать или создавать резервные копии | Только через пользовательский интерфейс терминала MetaTrader 5 | Да — стандартное копирование файлов |
| Риск конфликта имен | Высокий — общее пространство имён | Низкий — имя файла содержит название советника и обозначение торгового инструмента |
С практической точки зрения глобальные переменные терминала удаляются при переустановке терминала или его переносе на другой компьютер. Если трейдер переключается между настольным компьютером и VPS или переустанавливает MetaTrader 5 после сбоя в работе системы, состояние советника может быть сброшено без предупреждения. Файл, записанный на диск, сохраняется при всех этих событиях, поскольку его можно копировать, создавать резервные копии и перемещать, как и любой другой файл на вашем компьютере.
Раздел 2: Функции работы с двоичными файлами в MQL5
Режим FILE_BIN и сериализация структур
Файловая система MQL5 поддерживает три режима: CSV — для текста с разделителями-запятыми, TXT — для простого текста и BIN — для необработанных двоичных данных. Именно двоичный режим делает сохранение состояния структур простым и удобным. При записи структуры в файл, открытый в режиме FILE_BIN, с помощью FileWriteStruct() MQL5 копирует в файл её точное байтовое представление без форматирования, разбора или преобразования. При чтении с помощью FileReadStruct() те же байты копируются обратно в переменную того же структурного типа. Эта операция выполняется быстро, без потерь данных и подходит для любой структуры, содержащей только элементы фиксированного размера.
Основное ограничение заключается в том, что структура должна содержать только простые типы данных: целочисленные типы, тип double, логический тип, тип datetime, перечисления и массивы элементов типа char фиксированной длины. Динамические массивы, строки и указатели не поддерживаются, поскольку их представление в памяти включает адреса памяти, которые теряют смысл при перезапуске процесса. Используйте в структуре состояния только типы-значения, и её сериализация и десериализация всегда будут проходить корректно.
Основные функции работы с файлами
Цикл работы с файлами в MQL5 опирается на пять основных функций, обеспечивающих проверку наличия файлов, передачу данных и завершение работы с файлами:
- FileOpen(): Открывает существующий файл или создаёт новый, возвращая уникальный дескриптор файла.
- FileWriteStruct(): Записывает содержимое структуры в открытый файл.
- FileReadStruct(): Считывает структурированные данные из файла.
- FileClose(): Закрывает дескриптор файла и записывает на диск все оставшиеся в буфере данные.
- FileIsExist(): Проверяет наличие указанного файла перед попыткой его чтения, что позволяет избежать ошибок, возникающих при попытке открыть несуществующие файлы.
//--- Open file for writing (creates if not present, overwrites if it exists) int writeHandle = FileOpen("MyState.bin", FILE_WRITE | FILE_BIN | FILE_COMMON); //--- Open file for reading int readHandle = FileOpen("MyState.bin", FILE_READ | FILE_BIN | FILE_COMMON); //--- Write a structure to disk (returns bytes written; should equal sizeof(struct)) uint bytesWritten = FileWriteStruct(writeHandle, myStateStruct); //--- Read a structure from disk (returns bytes read) uint bytesRead = FileReadStruct(readHandle, myStateStruct); //--- Close file handles after operation FileClose(writeHandle); //--- Check if file exists prior to structural reads bool exists = FileIsExist("MyState.bin", FILE_COMMON); //+------------------------------------------------------------------+
По умолчанию операции с файлами в MQL5 выполняются в локальной папке данных терминала, отдельной для каждой установки терминала. FILE_COMMON указывает на общую папку, расположенную по адресу MetaQuotes/Terminal/Common/Files, доступ к которой открыт из любого терминала MetaTrader 5, установленного на том же компьютере. Для большинства конфигураций с одним компьютером оба варианта подходят. FILE_COMMON — более безопасный вариант по умолчанию, поскольку общая папка остаётся доступной, даже если трейдер переключается между терминалами разных брокеров.
Управление версиями файла состояния
Одна из практических проблем возникает при обновлении советника, когда в структуру состояния добавляются новые поля или изменяются существующие. Если файл на диске был записан с использованием более старой версии структуры, а советник пытается прочитать его в структуру новой версии, расположение байтов не будет соответствовать ожидаемому, и считанные значения окажутся некорректными. Это происходит незаметно: сообщение об ошибке не выводится, а советник просто загружает бессмысленные данные.
Чтобы исправить это, добавьте поле version в начало структуры. Каждый раз, когда меняется состав и расположение полей структуры, номер версии увеличивается на единицу. При загрузке советник сначала считывает поле версии и сравнивает его с ожидаемой версией. Если они совпадают, чтение продолжается. В противном случае файл считается устаревшим и удаляется, а советник начинает работу со значениями по умолчанию. Это всего лишь одна проверка целого числа, практически не требующая вычислительных затрат.
//+------------------------------------------------------------------+ //| Version tracking definition for serialization safety | //| Note: Increment STATE_VERSION whenever the struct layout changes | //+------------------------------------------------------------------+ #define STATE_VERSION 1 //+------------------------------------------------------------------+ //| Structure storing EA persistent runtime state | //| Note: The version field must remain the FIRST member of struct | //+------------------------------------------------------------------+ struct EAState { int version; // Struct version validation check datetime lastSaveTime; // Timestamp of last serialization datetime lastBarTime; // Last active operational bar time int dailyTradeCount; // Cumulative daily trade count int lossStreak; // Active consecutive loss sequence int winStreak; // Active consecutive win sequence double currentLotMult; // Volume scaling factor tracking double sessionHighEq; // Balance apex marker for reference bool partialClosed; // Position slice distribution flat int lastSignal; // Active trade gate signal mapping char reserved[64]; // Padded bytes for future schema }; //+------------------------------------------------------------------+
Поле reserved[64] предоставляет 64 резервных байта для добавления будущих полей без изменения размера структуры. Важно отметить, что перед записью на диск всю структуру необходимо инициализировать нулями с помощью функции ZeroMemory(). Это гарантирует, что все резервные байты будут обнулены. Когда впоследствии в этой резервной области будут размещены новые переменные, при чтении старых файлов состояния они будут корректно получать нулевые значения по умолчанию, обеспечивая беспроблемную обратную совместимость. Когда резервного пространства перестанет хватать для расширения структуры, достаточно обновить STATE_VERSION и отказаться от старого формата файла.
Раздел 3: Подключаемый файл PersistenceManager
Проектные решения
Подключаемый файл построен на основе двух функций: SaveState() и LoadState(). Обе функции по своей идее являются универсальными и работают со структурой EAState, определённой разработчиком. Имя файла формируется автоматически на основе названия советника и торгового инструмента, поэтому один и тот же подключаемый файл можно использовать с любыми советниками без конфликтов имён.
Функция SaveState() открывает файл, записывает структуру и закрывает файл. Перед записью функция устанавливает значения полей version и lastSaveTime, благодаря чему они всегда остаются актуальными, и вызывающему коду не нужно самостоятельно управлять ими. Функция LoadState() проверяет наличие файла, при его наличии открывает его, считывает структуру, проверяет версию и в случае любой ошибки возвращает структуру по умолчанию. Вызывающему коду никогда не нужно напрямую обрабатывать ошибки, связанные с файлами.
Полный текст файла PersistenceManager.mqh
//+------------------------------------------------------------------+ //| PersistenceManager.mqh | //+------------------------------------------------------------------+ #property strict #define STATE_VERSION 1 //+------------------------------------------------------------------+ //| Structure storing EA persistent runtime state | //+------------------------------------------------------------------+ struct EAState { int version; datetime lastSaveTime; datetime lastBarTime; int dailyTradeCount; int lossStreak; int winStreak; double currentLotMult; double sessionHighEq; bool partialClosed; int lastSignal; char reserved[64]; }; //+------------------------------------------------------------------+ //| Generates the unique state file path for the specific instance | //+------------------------------------------------------------------+ string GetStateFilePath() { return(MQLInfoString(MQL_PROGRAM_NAME) + "_" + _Symbol + "_state.bin"); } //+------------------------------------------------------------------+ //| Returns an EAState structure populated with baseline values | //+------------------------------------------------------------------+ EAState DefaultState() { EAState s; ZeroMemory(s); s.version = STATE_VERSION; s.lastSaveTime = 0; s.lastBarTime = 0; s.dailyTradeCount = 0; s.lossStreak = 0; s.winStreak = 0; s.currentLotMult = 1.0; s.sessionHighEq = AccountInfoDouble(ACCOUNT_EQUITY); s.partialClosed = false; s.lastSignal = 0; return(s); } //+------------------------------------------------------------------+ //| Serializes and saves the current EAState to disk | //+------------------------------------------------------------------+ bool SaveState(EAState &state) { state.version = STATE_VERSION; state.lastSaveTime = TimeCurrent(); string path = GetStateFilePath(); int handle = FileOpen(path, FILE_WRITE | FILE_BIN | FILE_COMMON); if(handle == INVALID_HANDLE) { Print("PersistenceManager: SaveState failed. Error: ", GetLastError()); return(false); } uint written = FileWriteStruct(handle, state); FileClose(handle); if(written != sizeof(EAState)) { Print("PersistenceManager: SaveState structural mismatch."); return(false); } return(true); } //+------------------------------------------------------------------+ //| Deserializes and loads the historical EAState from disk | //+------------------------------------------------------------------+ bool LoadState(EAState &state) { //--- Clear the global memory container completely before filling it ZeroMemory(state); string path = GetStateFilePath(); if(!FileIsExist(path, FILE_COMMON)) { Print("PersistenceManager: No state file found. Using defaults."); state = DefaultState(); return(false); } int handle = FileOpen(path, FILE_READ | FILE_BIN | FILE_COMMON); if(handle == INVALID_HANDLE) { Print("PersistenceManager: LoadState failed. Error: ", GetLastError()); state = DefaultState(); return(false); } EAState loaded; ZeroMemory(loaded); uint bytesRead = FileReadStruct(handle, loaded); FileClose(handle); if(loaded.version != STATE_VERSION) { Print("PersistenceManager: Version mismatch. Resetting to defaults."); FileDelete(path, FILE_COMMON); state = DefaultState(); return(false); } state = loaded; PrintFormat("PersistenceManager: State loaded. Last saved Server Time: %s", TimeToString(state.lastSaveTime, TIME_DATE | TIME_MINUTES)); return(true); } //+------------------------------------------------------------------+ //| Permanently purges the stored bin file from disk | //+------------------------------------------------------------------+ void DeleteStateFile() { string path = GetStateFilePath(); if(FileIsExist(path, FILE_COMMON)) { FileDelete(path, FILE_COMMON); Print("PersistenceManager: State file deleted."); } } //+------------------------------------------------------------------+
Раздел 4: Шаблон интеграции и пограничные случаи
Интеграция в четыре строки
Чтобы добавить сохранение состояния в существующий советник, необходимо внести изменения в трёх местах: добавить одну директиву #include в начале кода, две строки в OnInit() и одну строку в OnDeinit(). Кроме того, функция SaveState() вызывается в любой точке логики советника, где происходит изменение состояния, как правило, после открытия сделки, после закрытия сделки и после сброса счётчика сделок за день. Сама переменная состояния объявлена глобально в файле советника.
//+------------------------------------------------------------------+ //| EA Integration | //+------------------------------------------------------------------+ #include <PersistenceManager.mqh> //--- Global state instance mapping EAState g_state; //--- Input parameters input bool InpResetState = false; // Wipe saved state on initialization //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- 1. Purge active state tracking if manual reset is requested if(InpResetState) DeleteStateFile(); //--- 2. Retrieve state parameters from file framework LoadState(g_state); //--- 3. Verify calendar integrity for rolling profile sessions MqlDateTime savedDt, todayDt; TimeToStruct(g_state.lastSaveTime, savedDt); TimeToStruct(TimeCurrent(), todayDt); if(savedDt.day != todayDt.day || savedDt.mon != todayDt.mon) { g_state.dailyTradeCount = 0; Print("PersistenceManager: New trading day detected. Daily counter reset."); } return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Commit active profile footprint to stable file cache SaveState(g_state); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- 1. Operational threshold restrictions if(g_state.dailyTradeCount >= 3) return; //--- 2. Mathematical volume lot scaling calculation double lots = 0.1 * g_state.currentLotMult; } //+------------------------------------------------------------------+
Проверка смены даты в функции OnInit() — небольшая, но важная деталь. Если советник перезапускается в новый торговый день, счётчик сделок за день в сохранённом файле по-прежнему содержит вчерашнее значение. Сравнение календарной даты в метке времени сохранения с текущей датой позволяет выявить эту ситуацию и сбросить счётчик сделок за день до того, как советник начнёт открывать сделки. Без этой проверки файл, сохраненный поздно вечером в четверг со значением dailyTradeCount = 3, заблокировал бы все сделки в пятницу.
Несколько графиков, один советник
Когда один и тот же советник запущен одновременно на нескольких графиках, например на EURUSD H1 и EURUSD H4, оба экземпляра вызывают функцию GetStateFilePath() и получают одинаковое имя файла, поскольку название советника и торговый инструмент у них совпадают. Это приводит к состоянию гонки: два экземпляра могут перезаписывать данные друг друга в общем файле состояния.
Чтобы решить эту проблему, необходимо включить таймфрейм графика в имя файла. Замените функцию GetStateFilePath() в файле PersistenceManager.mqh приведённой ниже версией, которая добавляет таймфрейм к имени файла. Тогда каждый экземпляр будет читать данные из своего отдельного файла и записывать их в него без риска конфликтов.
//+------------------------------------------------------------------+ //| Generates a timeframe-unique path for multi-chart deployments | //| Note: Prevents state collisions when running on multiple periods | //+------------------------------------------------------------------+ string GetStateFilePath() { string eaName = MQLInfoString(MQL_PROGRAM_NAME); string symbol = _Symbol; string period = EnumToString(_Period); return(eaName + "_" + symbol + "_" + period + "_state.bin"); }
Если советники работают с разными торговыми инструментами, например EURUSD и GBPUSD, на отдельных графиках, имя файла по умолчанию уже содержит обозначение торгового инструмента, поэтому конфликтов не возникает. Суффикс, обозначающий таймфрейм, требуется только в том случае, если одна и та же комбинация торгового инструмента и советника запущена одновременно на нескольких графиках.
Жизненный цикл сохранения состояния
Знать, когда вызывать функцию SaveState(), не менее важно, чем знать, как её реализовать. Вызывать эту функцию на каждом тике нерационально и нет необходимости. При каждом вызове файл записывается на диск, что создаёт накладные расходы на ввод-вывод. Если вызывать эту функцию слишком редко, при сбое могут быть утеряны последние изменения состояния. Правильный подход — событийно-ориентированный: сохраняйте состояние всякий раз, когда происходит какое-либо значимое изменение.
| Событие советника | Действие | Примечания |
|---|---|---|
| OnInit() | LoadState() | Считывает файл, если он существует. Если файл не найден или версия не совпадает, используется структура со значениями по умолчанию. |
| Сделка открыта | SaveState() | Сохраняет обновлённое значение счётчика сделок за день, текущий множитель лота и последний сигнал. |
| Сделка закрыта | SaveState() | Обновить счётчики прибыльных и убыточных сделок подряд, максимальное значение средств на счёте за сессию и флаги частичного закрытия. |
| Обнаружен новый день | SaveState() | Перед сохранением сбросьте суточные счетчики внутри структуры. |
| OnDeinit() | SaveState() | Заключительное сохранение при корректном завершении работы. Охватывает ручное удаление советника с графика и закрытие терминала. |
| Входной параметр-флаг сброса = true | DeleteStateFile() + значения по умолчанию | Трейдеры устанавливают входной параметр-флаг, чтобы при следующей инициализации принудительно установить "чистое" состояние. Файл обнаружен, структура сброшена. |
Таблица 2 — Когда вызывать SaveState() в течение жизненного цикла советника. Каждая строка соответствует событию, которое изменяет состояние и требует его немедленной записи на диск.
Особенно важна функция OnDeinit(), расположенная внизу таблицы. Когда советник удаляется с графика вручную или терминал нормально завершает работу, перед завершением процесса вызывается функция OnDeinit(). Сохранение состояния в этом обработчике гарантирует фиксацию итоговых значений и при штатной остановке советника, а не только при аварийном завершении его работы. В случае сбоев и отключений электропитания для восстановления используется состояние, сохранённое последним вызовом SaveState() при последнем торговом событии.
Раздел 5: Практический пример — сохранение данных при перезапуске в середине сессии
Демонстрационный советник
В приведённом ниже советнике все эти элементы объединены в полноценный рабочий пример. Он отслеживает три переменные состояния, сохраняемые между перезапусками: счётчик сделок за день, счётчик убыточных сделок подряд и текущий множитель лота, рассчитанный на основе длины этой серии. Логика входа в рынок представляет собой базовую заглушку, поэтому основное внимание уделяется поведению системы сохранения состояния, а не самой стратегии.
//+------------------------------------------------------------------+ //| PersistenceDemo.mq5 | //+------------------------------------------------------------------+ #property strict #include <PersistenceManager.mqh> #include <Trade\Trade.mqh> //--- Input parameters input int InpMaxDailyTrades = 3; input double InpBaseLots = 0.1; input double InpLotMultiplier = 1.5; input bool InpResetState = false; //--- Global variables EAState g_state; CTrade g_trade; datetime g_lastBarTime = 0; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { if(InpResetState) DeleteStateFile(); //--- Load historical data into memory state LoadState(g_state); //--- Validate daytime logic only if a historical state exists if(g_state.lastSaveTime > 0) { MqlDateTime sdt, tdt; TimeToStruct(g_state.lastSaveTime, sdt); TimeToStruct(TimeCurrent(), tdt); if(sdt.day != tdt.day || sdt.mon != tdt.mon) { g_state.dailyTradeCount = 0; Print("New trading day detected - daily counter reset."); SaveState(g_state); } } else { //--- For fresh profiles, ensure structural sizing begins at baseline g_state.currentLotMult = 1.0; } Print("=== STATE ON LOAD ==="); PrintFormat("Daily trades so far : %d", g_state.dailyTradeCount); PrintFormat("Loss streak : %d", g_state.lossStreak); PrintFormat("Current lot mult : %.2f", g_state.currentLotMult); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Serialize active data footprint on parameter tweaks or extraction SaveState(g_state); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { datetime currentBar = iTime(_Symbol, PERIOD_CURRENT, 0); if(currentBar == g_lastBarTime) return; g_lastBarTime = currentBar; if(g_state.dailyTradeCount >= InpMaxDailyTrades) { PrintFormat("Daily trade limit reached (%d/%d).", g_state.dailyTradeCount, InpMaxDailyTrades); return; } if(PositionsTotal() > 0) return; double lots = NormalizeDouble(InpBaseLots * g_state.currentLotMult, 2); lots = MathMax(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN)); bool buySignal = (g_state.lastSignal <= 0); double sl, tp; double atr = iATR(_Symbol, PERIOD_CURRENT, 14); if(atr == 0) return; if(buySignal) { sl = SymbolInfoDouble(_Symbol, SYMBOL_BID) - atr * 1.5; tp = SymbolInfoDouble(_Symbol, SYMBOL_BID) + atr * 2.0; if(g_trade.Buy(lots, _Symbol, 0, sl, tp, "PersistDemo")) { g_state.lastSignal = 1; PrintFormat("BUY signal sent to server. Lots: %.2f", lots); } } else { sl = SymbolInfoDouble(_Symbol, SYMBOL_ASK) + atr * 1.5; tp = SymbolInfoDouble(_Symbol, SYMBOL_ASK) - atr * 2.0; if(g_trade.Sell(lots, _Symbol, 0, sl, tp, "PersistDemo")) { g_state.dailyTradeCount++; g_state.lastSignal = -1; SaveState(g_state); PrintFormat("SELL opened. Daily count: %d | Lots: %.2f", g_state.dailyTradeCount, lots); } } } //+------------------------------------------------------------------+ //| TradeTransaction function | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction &trans, const MqlTradeRequest &req, const MqlTradeResult &res) { //--- Isolate processing strictly to confirmed deal additions if(trans.type != TRADE_TRANSACTION_DEAL_ADD) return; ulong dealTicket = trans.deal; if(!HistoryDealSelect(dealTicket)) return; //--- Restrict validation context exclusively to this specific ticker if(HistoryDealGetString(dealTicket, DEAL_SYMBOL) != _Symbol) return; long dealEntry = HistoryDealGetInteger(dealTicket, DEAL_ENTRY); //--- 1. TRACK NEWLY OPENED TRADES (Daily Counter) if(dealEntry == DEAL_ENTRY_IN) { g_state.dailyTradeCount++; PrintFormat("New trade detected on account. Daily count updated to: %d", g_state.dailyTradeCount); SaveState(g_state); return; } //--- 2. TRACK CLOSED TRADES (Win/Loss Streaks) if(dealEntry == DEAL_ENTRY_OUT) { double profit = HistoryDealGetDouble(dealTicket, DEAL_PROFIT); double swap = HistoryDealGetDouble(dealTicket, DEAL_SWAP); double commission = HistoryDealGetDouble(dealTicket, DEAL_COMMISSION); double netProfit = profit + swap + commission; if(netProfit < 0) { g_state.lossStreak++; g_state.winStreak = 0; g_state.currentLotMult *= InpLotMultiplier; PrintFormat("Loss recorded. Streak: %d | Next lot mult: %.2f", g_state.lossStreak, g_state.currentLotMult); } else if(netProfit > 0) { g_state.winStreak++; g_state.lossStreak = 0; g_state.currentLotMult = 1.0; PrintFormat("Win recorded. Streak: %d | Lot mult reset to 1.0", g_state.winStreak); } SaveState(g_state); } } //+------------------------------------------------------------------+
Функция OnTradeTransaction(), расположенная внизу, играет ключевую роль в обновлении счётчика убыточных сделок подряд и множителя лота. Она срабатывает при каждом добавлении сделки в историю счёта, в том числе при открытии и закрытии любой позиции. Проверка на TRADE_TRANSACTION_DEAL_ADD в сочетании с проверкой на DEAL_ENTRY_OUT позволяет обрабатывать только сделки закрытия позиций. После обновления серии сделок функция SaveState() вызывается немедленно, чтобы обновлённые значения сохранились при любом последующем перезапуске.
Наблюдение за поведением системы при перезапуске
Чтобы увидеть, как работает сохранение состояния, подключите советник к любому графику, установив InpResetState = false. Откройте две или три сделки и запишите значения счётчика сделок за день и множителя лота из журнала "Эксперты". Затем удалите советник с графика и снова подключите его к графику. При повторном подключении советника к графику в журнале "Эксперты" появится сообщение STATE ON LOAD с сохранёнными значениями: счётчик сделок за день, счётчик убыточных сделок подряд и множитель лота останутся без изменений. Ничего не пропало.

Рис. 1. Журнал "Эксперты" для PersistenceDemo (GBPUSD, H4), демонстрирующий восстановление состояния при двух инициализациях. При первой загрузке файл не найден, поэтому используются значения по умолчанию. После регистрации двух сделок и одного убытка — в результате чего счётчик убыточных сделок подряд увеличивается до 1, а множитель лота — до 1,50 — советник проходит повторную инициализацию. При повторной загрузке двоичный файл успешно считывается, восстанавливая значения счётчика сделок за день (2), счётчика убыточных сделок подряд (1) и множителя лота (1,50)
Проверка логики смены дня
Поскольку система сохранения состояния использует время сервера брокера (TimeCurrent()), изменение времени на локальном компьютере не вызовет сброс при смене дня во время тестирования на реальном графике. Вместо этого можно имитировать смену суток, временно скорректировав загруженные данные о состоянии непосредственно в функции OnInit().
Чтобы немедленно принудительно проверить логику сброса:
- Добавьте эту временную строку с вычислением сразу после LoadState(g_state); в функции OnInit():
// TESTING ONLY: Subtract 25 hours to simulate a file saved yesterday g_state.lastSaveTime = g_state.lastSaveTime - 90000;
- Скомпилируйте советник и подключите его к графику демосчёта, работающему в реальном времени, затем отключите советник от графика.
- Снова подключите советник к графику.
При повторном подключении советника к графику он обработает отметку времени с более ранней датой, что приведёт к срабатыванию условия "Обнаружен новый торговый день" и сбросу счётчика сделок за день до нуля. Убедившись по журналу "Эксперты", что сброс выполнен, удалите временную тестовую строку и перекомпилируйте рабочий код.
Совместимость с тестером стратегий: файл PersistenceManager.mqh работает в тестере стратегий, но ведет себя несколько иначе. В тестере при каждом новом запуске функция OnInit() вызывается заново, поэтому функция LoadState() будет находить те данные, которые были записаны функцией OnDeinit() предыдущего запуска. Это может привести к тому, что последовательные бэктесты будут наследовать состояние друг от друга — в частности, значение счётчика сделок за день, если даты тестирования приходятся на один и тот же календарный месяц. Перед первым запуском бэктеста после внесения изменений в советник всегда устанавливайте значение InpResetState = true или вручную удаляйте файл состояния из общей папки между запусками. После этого оставьте значение InpResetState равным false, чтобы проверить поведение при восстановлении между сеансами тестирования.
Раздел 6: Расширение и адаптация шаблона
Добавление новых полей состояния
Добавление новой переменной в структуру состояния состоит из двух этапов:
- Добавьте поле в EAState.
- Уменьшите размер массива reserved[] ровно на то количество байтов, которое вы только что добавили для новой переменной. Благодаря этому общий размер структуры остается таким же, как и в старой версии.
Если новое поле помещается в существующую зарезервированную область, увеличивать значение STATE_VERSION не требуется, поскольку размер файла остаётся неизменным. Это сохраняет обратную совместимость, позволяя системе беспрепятственно считывать файлы более ранних версий. Поскольку в старом файле эти резервные байты были заполнены нулями с помощью функции ZeroMemory(), значение нового поля при чтении будет корректно определено как 0. Если обновлённая логика требует, чтобы для этого нового поля в старых файлах было задано ненулевое значение по умолчанию, необходимо явно присвоить его сразу после того, как функция LoadState() вернёт значение true.
Если новое поле не помещается в резервные байты — то есть размер структуры необходимо увеличить, — увеличьте значение STATE_VERSION. При следующем вызове LoadState() на компьютере со старым файлом функция обнаружит несоответствие версий, удалит устаревший файл и начнёт работу заново, используя новые значения по умолчанию, заданные функцией DefaultState(). Такой разовый сброс неизбежен при кардинальном изменении состава и расположения полей структуры.
Использование шаблона для пользовательских индикаторов
Сохранение состояния в двоичных файлах предназначено не только для советников: пользовательским индикаторам также часто требуется сохранять данные между сессиями. Например, индикатору может потребоваться запомнить нанесённые пользователем уровни поддержки и сопротивления или самые высокие и самые низкие точки разворота, рассчитанные за последние N дней.
Здесь применяется тот же шаблон PersistenceManager. Чтобы реализовать это:
- Определите структуру, специально предназначенную для состояния индикатора.
- Подключите к своему коду файл, реализующий сохранение состояния.
- Вызовите функцию LoadState() внутри функции OnInit().
- Вызовите функцию SaveState() внутри OnCalculate() после обработки последнего бара.
Следует учитывать одно отличие: у индикаторов нет гарантии вызова OnDeinit() при каждом удалении. Иногда терминал может удалить индикатор без вызова функции OnDeinit(), если он выполняет очистку при завершении работы. Для индикаторов более надёжный подход — вызывать SaveState() при каждом вызове OnCalculate(), если количество баров изменилось, а не только при деинициализации. При выполнении один раз на бар затраты на файловый ввод-вывод незначительны.
Резервное копирование и перенос файлов состояния
Поскольку файлы состояния хранятся в стандартной папке на диске, ими легко управлять. Чтобы создать резервную копию состояния запущенного советника, скопируйте файл .bin из папки MetaQuotes/Terminal/Common/Files/ в любое место. Для переноса данных на новый компьютер или переустановленный терминал скопируйте файл в ту же папку на целевом устройстве. При следующем вызове OnInit() советник загрузит данные из этого файла, как будто ничего не изменилось.

Рис. 2. Файл состояния PersistenceDemo_EURUSD_state.bin, расположенный в папке общих файлов песочницы MetaQuotes. Размер файла составляет ровно 117 байт — столько занимает сериализованная структура данных состояния. Как и любой обычный файл, его можно копировать, резервировать или переносить на другой компьютер; при следующей инициализации советника терминал автоматически найдёт файл и прочитает содержащиеся в нём данные
Для трейдеров, которые запускают советники как на локальном компьютере, так и на облачном сервере, синхронизация папки состояния с помощью любого сервиса синхронизации файлов позволяет обоим экземплярам отслеживать активность друг друга. Это простой, но эффективный способ обеспечить учёт сделки, открытой на локальном компьютере в течение дня, в серии убыточных сделок и счётчике сделок за день, когда советник на сервере возобновит работу следующим утром.
Заключение
Проблема сброса состояния — скрытая уязвимость при разработке на MQL5. Поскольку эта проблема не вызывает ошибок времени выполнения и не проявляется при стандартном тестировании стратегий на исторических данных, она, как правило, остаётся незамеченной до перезапуска терминала или перезагрузки советника в условиях реальной торговли. При таких обычных событиях советник инициализируется со значениями по умолчанию, полностью теряет связь с историей своей работы и выполняет логику стратегии так, будто торговая сессия только что началась.
Реализация PersistenceManager.mqh решает эту проблему за счет внедрения автономной программной основы сериализации. Объединяя переменные, хранящиеся в оперативной памяти, в единую структуру данных и записывая её в двоичный файл, подключаемый файл обеспечивает сохранность данных между запусками при минимальном объёме дополнительного кода. Архитектура предусматривает управление версиями файлов для предотвращения повреждения данных при изменении структуры, разграничивает имена файлов во избежание конфликтов между несколькими экземплярами на разных графиках и полностью работает в среде MQL5 без внешних зависимостей.
Данный подход также учитывает неявные допущения об инициализации. Присваивание переменным, накапливающим значения, фиксированных значений по умолчанию предполагает запуск с нуля, что в реальной торговле часто не соответствует действительности. Переход к модели с сохранением состояния обеспечивает сохранность внутренней памяти советника между сессиями, приводя его состояние при запуске в соответствие с фактическими результатами предыдущей работы.
Программы, использованные в статье:
| # | Название | Тип | Описание |
|---|---|---|---|
| 1 | PersistenceManager.mqh | Подключаемый файл | Полный подключаемый файл, содержащий структуру EAState, а также функции GetStateFilePath(), DefaultState(), SaveState(), LoadState() и DeleteStateFile(). Поместите файл в папку MQL5/Include/. |
| 2 | PersistenceDemo.mq5 | Демонстрационный советник | Демонстрационный советник, в котором все функции сохранения состояния интегрированы с ограничениями количества сделок в день, отслеживанием серий убыточных сделок и масштабированием объёма лота. Подключите советника к любому графику, чтобы понаблюдать за восстановлением его состояния после перезапуска. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22277
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
От новичка до эксперта: Усиление стратегий ликвидности за счет структурного подтверждения на нескольких таймфреймах
Пошаговая реализация локальной системы стоп-лоссов на MQL5
От новичка до эксперта: Выявление переворотов зон ликвидности на языке MQL5
Пакетный подход к разработке на языке MQL5 с помощью KnitPkg
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования