//+------------------------------------------------------------------+ //| ServiceStateWriter.mq5 | //| Менеджер терминалов MetaTrader 5, ч. 4 | //| | //| Сервис периодически записывает состояние своего экземпляра | //| терминала и подключённого торгового счёта в общую базу SQLite, | //| расположенную в папке Common. Веб-приложение менеджера читает | //| эти данные из базы, не обращаясь к терминалам напрямую. | //+------------------------------------------------------------------+ #property service #property version "1.00" #property description "Запись состояния терминала в общую базу SQLite" //--- Входные параметры экземпляра сервиса input string InpDatabase = "mt5-manager.sqlite"; // Имя файла базы данных (папка Common) input int InpIntervalSec = 2; // Интервал записи, секунд input int InpJitterMs = 1000; // Разброс интервала, мс (0 - выключить) input int InpBusyTimeout = 3000; // Таймаут занятости базы, мс //--- Глобальные переменные int g_db = INVALID_HANDLE; // Хэндл базы данных int g_terminal_id = -1; // Идентификатор записи этого терминала int g_skipped = 0; // Счётчик пропущенных записей // DatabaseRead() на запросе без результата (INSERT/UPDATE) завершается // этим кодом - это конец выборки, а не ошибка. Проверено на практике. #define ERR_DB_NO_MORE_DATA 5126 //+------------------------------------------------------------------+ //| Ищет последнее вхождение подстроки (в MQL5 нет StringFindLast) | //+------------------------------------------------------------------+ int FindLast(const string text, const string what) { int pos = -1; int next = StringFind(text, what, 0); while(next >= 0) { pos = next; next = StringFind(text, what, next + 1); } return pos; } //+------------------------------------------------------------------+ //| Возвращает имя папки экземпляра терминала | //| В portable-режиме это последний компонент пути запуска, | //| например "MetaTrader5.1" для C:/MT5/MetaTrader5.1 | //+------------------------------------------------------------------+ string InstanceFolder() { string path = TerminalInfoString(TERMINAL_PATH); StringReplace(path, "\\", "/"); // Отсекаем завершающий слеш, если он есть int len = StringLen(path); if(len > 0 && StringGetCharacter(path, len - 1) == '/') path = StringSubstr(path, 0, len - 1); int pos = FindLast(path, "/"); return (pos >= 0) ? StringSubstr(path, pos + 1) : path; } //+------------------------------------------------------------------+ //| Выполняет PRAGMA, возвращающий строку результата | //| DatabaseExecute() на таких запросах может вернуть ошибку, | //| поэтому используем подготовленный запрос с чтением результата | //+------------------------------------------------------------------+ bool ExecutePragma(const int db, const string sql) { int request = DatabasePrepare(db, sql); if(request == INVALID_HANDLE) { PrintFormat("Не удалось подготовить запрос '%s', код ошибки: %d", sql, GetLastError()); return false; } // Забираем возвращённую строку, если она есть DatabaseRead(request); DatabaseFinalize(request); return true; } //+------------------------------------------------------------------+ //| Создаёт таблицы, если их ещё нет | //+------------------------------------------------------------------+ bool EnsureSchema(const int db) { const string sql_terminals = "CREATE TABLE IF NOT EXISTS terminals (" " id INTEGER PRIMARY KEY," " folder TEXT NOT NULL UNIQUE," " path TEXT," " company TEXT," " build INTEGER," " first_seen INTEGER," " last_seen INTEGER)"; const string sql_states = "CREATE TABLE IF NOT EXISTS states (" " terminal_id INTEGER PRIMARY KEY REFERENCES terminals(id)," " updated_at INTEGER NOT NULL," " connected INTEGER," " ping_last INTEGER," " login INTEGER," " server TEXT," " currency TEXT," " leverage INTEGER," " trade_mode INTEGER," " trade_allowed INTEGER," " balance REAL," " equity REAL," " profit REAL," " margin REAL," " margin_free REAL," " margin_level REAL," " credit REAL," " positions INTEGER," " company TEXT," " holder_name TEXT," " error_code INTEGER DEFAULT 0," " error_text TEXT)"; if(!DatabaseExecute(db, sql_terminals)) { PrintFormat("Не удалось создать таблицу terminals, код ошибки: %d", GetLastError()); return false; } if(!DatabaseExecute(db, sql_states)) { PrintFormat("Не удалось создать таблицу states, код ошибки: %d", GetLastError()); return false; } return true; } //+------------------------------------------------------------------+ //| Открывает базу и подготавливает соединение | //+------------------------------------------------------------------+ bool OpenDatabase() { g_db = DatabaseOpen(InpDatabase, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | DATABASE_OPEN_COMMON); if(g_db == INVALID_HANDLE) { PrintFormat("Не удалось открыть базу '%s', код ошибки: %d", InpDatabase, GetLastError()); return false; } // Таймаут занятости - свойство соединения, задаём при каждом открытии ExecutePragma(g_db, StringFormat("PRAGMA busy_timeout=%d", InpBusyTimeout)); // Режим WAL запоминается в заголовке файла базы, но повторная // установка безвредна и страхует от создания базы другим клиентом ExecutePragma(g_db, "PRAGMA journal_mode=WAL"); if(!EnsureSchema(g_db)) { DatabaseClose(g_db); g_db = INVALID_HANDLE; return false; } return true; } //+------------------------------------------------------------------+ //| Регистрирует терминал в таблице terminals и возвращает его id | //+------------------------------------------------------------------+ int RegisterTerminal() { const string folder = InstanceFolder(); const string path = TerminalInfoString(TERMINAL_PATH); const string company = TerminalInfoString(TERMINAL_COMPANY); const int build = (int)TerminalInfoInteger(TERMINAL_BUILD); // TimeGMT(), не TimeLocal(): последняя возвращает цифры настенных часов, // выданные за UTC-эпоху (смещение на часовой пояс машины), из-за чего // разъезжается с временем на стороне Python. TimeGMT() - настоящий UTC, // и при этом не зависит от времени сервера брокера, как TimeCurrent(). const long now = (long)TimeGMT(); const string sql = "INSERT INTO terminals (folder, path, company, build, first_seen, last_seen) " "VALUES (?1, ?2, ?3, ?4, ?5, ?5) " "ON CONFLICT(folder) DO UPDATE SET " " path = excluded.path," " company = excluded.company," " build = excluded.build," " last_seen = excluded.last_seen"; int request = DatabasePrepare(g_db, sql); if(request == INVALID_HANDLE) { PrintFormat("Не удалось подготовить регистрацию терминала, код ошибки: %d", GetLastError()); return -1; } DatabaseBind(request, 0, folder); DatabaseBind(request, 1, path); DatabaseBind(request, 2, company); DatabaseBind(request, 3, build); DatabaseBind(request, 4, now); ResetLastError(); DatabaseRead(request); const int error = GetLastError(); DatabaseFinalize(request); if(error != 0 && error != ERR_DB_NO_MORE_DATA) { PrintFormat("Не удалось зарегистрировать терминал '%s', код ошибки: %d", folder, error); return -1; } // Забираем присвоенный идентификатор int id = -1; request = DatabasePrepare(g_db, "SELECT id FROM terminals WHERE folder = ?1"); if(request != INVALID_HANDLE) { DatabaseBind(request, 0, folder); if(DatabaseRead(request)) DatabaseColumnInteger(request, 0, id); DatabaseFinalize(request); } if(id < 0) PrintFormat("Не удалось получить идентификатор терминала '%s'", folder); else PrintFormat("Терминал '%s' зарегистрирован с id=%d", folder, id); return id; } //+------------------------------------------------------------------+ //| Собирает состояние и записывает его в таблицу states | //+------------------------------------------------------------------+ bool StoreState() { //--- Состояние терминала const bool connected = (bool)TerminalInfoInteger(TERMINAL_CONNECTED); const int ping_last = (int)TerminalInfoInteger(TERMINAL_PING_LAST); //--- Состояние торгового счёта const long login = AccountInfoInteger(ACCOUNT_LOGIN); const string server = AccountInfoString(ACCOUNT_SERVER); const string currency = AccountInfoString(ACCOUNT_CURRENCY); const int leverage = (int)AccountInfoInteger(ACCOUNT_LEVERAGE); const int trade_mode = (int)AccountInfoInteger(ACCOUNT_TRADE_MODE); const int trade_allowed = (int)AccountInfoInteger(ACCOUNT_TRADE_ALLOWED); const double balance = AccountInfoDouble(ACCOUNT_BALANCE); const double equity = AccountInfoDouble(ACCOUNT_EQUITY); const double profit = AccountInfoDouble(ACCOUNT_PROFIT); const double margin = AccountInfoDouble(ACCOUNT_MARGIN); const double margin_free = AccountInfoDouble(ACCOUNT_MARGIN_FREE); const double margin_level = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL); const double credit = AccountInfoDouble(ACCOUNT_CREDIT); const int positions = PositionsTotal(); const long updated_at = (long)TimeGMT(); //--- Данные о держателе счёта, для отображения в интерфейсе const string account_company = AccountInfoString(ACCOUNT_COMPANY); const string holder_name = AccountInfoString(ACCOUNT_NAME); //--- Признак готовности данных. 1/-2 продолжают ряд кодов менеджера // (-1 - "Manager: Terminal not found"), префикс в тексте показывает // источник жалобы const int error_code = connected ? 1 : -2; const string error_text = connected ? "Success" : "Service: No connection to trade server"; const string sql = "INSERT INTO states (" " terminal_id, updated_at, connected, ping_last, login, server, currency," " leverage, trade_mode, trade_allowed, balance, equity, profit, margin," " margin_free, margin_level, credit, positions, company, holder_name," " error_code, error_text) " "VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20,?21,?22) " "ON CONFLICT(terminal_id) DO UPDATE SET " " updated_at = excluded.updated_at," " connected = excluded.connected," " ping_last = excluded.ping_last," " login = excluded.login," " server = excluded.server," " currency = excluded.currency," " leverage = excluded.leverage," " trade_mode = excluded.trade_mode," " trade_allowed = excluded.trade_allowed," " balance = excluded.balance," " equity = excluded.equity," " profit = excluded.profit," " margin = excluded.margin," " margin_free = excluded.margin_free," " margin_level = excluded.margin_level," " credit = excluded.credit," " positions = excluded.positions," " company = excluded.company," " holder_name = excluded.holder_name," " error_code = excluded.error_code," " error_text = excluded.error_text"; int request = DatabasePrepare(g_db, sql); if(request == INVALID_HANDLE) return false; DatabaseBind(request, 0, g_terminal_id); DatabaseBind(request, 1, updated_at); DatabaseBind(request, 2, connected ? 1 : 0); DatabaseBind(request, 3, ping_last); DatabaseBind(request, 4, login); DatabaseBind(request, 5, server); DatabaseBind(request, 6, currency); DatabaseBind(request, 7, leverage); DatabaseBind(request, 8, trade_mode); DatabaseBind(request, 9, trade_allowed); DatabaseBind(request, 10, balance); DatabaseBind(request, 11, equity); DatabaseBind(request, 12, profit); DatabaseBind(request, 13, margin); DatabaseBind(request, 14, margin_free); DatabaseBind(request, 15, margin_level); DatabaseBind(request, 16, credit); DatabaseBind(request, 17, positions); DatabaseBind(request, 18, account_company); DatabaseBind(request, 19, holder_name); DatabaseBind(request, 20, error_code); DatabaseBind(request, 21, error_text); ResetLastError(); // Оборачиваем запись в явную транзакцию: без неё каждая операция // становится отдельной транзакцией со сбросом на диск if(!DatabaseTransactionBegin(g_db)) { DatabaseFinalize(request); return false; } DatabaseRead(request); const int error = GetLastError(); DatabaseFinalize(request); // Код ERR_DB_NO_MORE_DATA соответствует завершению выборки и ошибкой не является if(error != 0 && error != ERR_DB_NO_MORE_DATA) { DatabaseTransactionRollback(g_db); return false; } if(!DatabaseTransactionCommit(g_db)) return false; return true; } //+------------------------------------------------------------------+ //| Точка входа сервиса | //+------------------------------------------------------------------+ void OnStart() { PrintFormat("Запуск сервиса для экземпляра '%s'", InstanceFolder()); // Разброс интервала, чтобы записи разных сервисов не совпадали по фазе MathSrand((int)TimeLocal() + (int)GetTickCount()); const int interval_ms = InpIntervalSec * 1000; while(!IsStopped()) { // Подключение к базе - при неудаче пробуем снова на следующем витке if(g_db == INVALID_HANDLE) { if(!OpenDatabase()) { Sleep(interval_ms); continue; } g_terminal_id = -1; } // Регистрация терминала - тоже с повтором при неудаче if(g_terminal_id < 0) { g_terminal_id = RegisterTerminal(); if(g_terminal_id < 0) { Sleep(interval_ms); continue; } } // Основная работа if(!StoreState()) { g_skipped++; if(g_skipped % 10 == 0) PrintFormat("Пропущено записей: %d, последняя ошибка: %d", g_skipped, GetLastError()); } // Пауза с разбросом int pause = interval_ms; if(InpJitterMs > 0) pause += (int)(MathRand() % InpJitterMs); Sleep(pause); } if(g_db != INVALID_HANDLE) DatabaseClose(g_db); PrintFormat("Остановка сервиса, всего пропущено записей: %d", g_skipped); } //+------------------------------------------------------------------+