| //+------------------------------------------------------------------+
|
| //| ServiceStateWriter.mq5 |
|
| //| Менеджер терминалов MetaTrader 5, ч. 4 |
|
| //| |
|
| //| Сервис периодически записывает состояние своего экземпляра |
|
| //| терминала и подключённого торгового счёта в общую базу SQLite, |
|
| //| расположенную в папке Common. Веб-приложение менеджера читает |
|
| //| эти данные из базы, не обращаясь к терминалам напрямую. |
|
| //+------------------------------------------------------------------+
|
| #property service
|
| #property version "1.02"
|
| #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; // Счётчик пропущенных записей
|
|
|
| //+------------------------------------------------------------------+
|
| //| FindLast: поиск последнего вхождения подстроки |
|
| //+------------------------------------------------------------------+
|
| 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;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| InstanceFolder: имя папки экземпляра терминала |
|
| //+------------------------------------------------------------------+
|
| string InstanceFolder()
|
| {
|
| string path = TerminalInfoString(TERMINAL_DATA_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;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| EnsureSchema: создание таблиц базы данных |
|
| //+------------------------------------------------------------------+
|
| 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;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| OpenDatabase: открытие базы и настройка соединения |
|
| //+------------------------------------------------------------------+
|
| 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;
|
| }
|
|
|
| //--- Таймаут занятости - свойство соединения, задаём при каждом открытии
|
| if(!DatabaseExecute(g_db, StringFormat("PRAGMA busy_timeout=%d", InpBusyTimeout)))
|
| PrintFormat("Не удалось задать busy_timeout, код ошибки: %d", GetLastError());
|
|
|
| //--- Режим WAL запоминается в заголовке файла базы, но повторная
|
| //--- установка безвредна и страхует от создания базы другим клиентом
|
| if(!DatabaseExecute(g_db, "PRAGMA journal_mode=WAL"))
|
| PrintFormat("Не удалось включить режим WAL, код ошибки: %d", GetLastError());
|
|
|
| if(!EnsureSchema(g_db))
|
| {
|
| DatabaseClose(g_db);
|
| g_db = INVALID_HANDLE;
|
| return false;
|
| }
|
|
|
| return true;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| RegisterTerminal: регистрация терминала в базе |
|
| //+------------------------------------------------------------------+
|
| int RegisterTerminal()
|
| {
|
| const string folder = InstanceFolder();
|
| const string path = TerminalInfoString(TERMINAL_DATA_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_DATABASE_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;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| StoreState: запись текущего состояния |
|
| //+------------------------------------------------------------------+
|
| 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_DATABASE_NO_MORE_DATA (5126) соответствует завершению выборки и ошибкой не является
|
| if(error != 0 && error != ERR_DATABASE_NO_MORE_DATA)
|
| {
|
| DatabaseTransactionRollback(g_db);
|
| return false;
|
| }
|
|
|
| if(!DatabaseTransactionCommit(g_db))
|
| return false;
|
|
|
| return true;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| OnStart: точка входа сервиса |
|
| //+------------------------------------------------------------------+
|
| void OnStart()
|
| {
|
| PrintFormat("Запуск сервиса для экземпляра '%s'", InstanceFolder());
|
| PrintFormat("База сервиса: %s\\Files\\%s",
|
| TerminalInfoString(TERMINAL_COMMONDATA_PATH), InpDatabase);
|
|
|
| //--- Разброс интервала, чтобы записи разных сервисов не совпадали по фазе
|
| MathSrand((int)TimeLocal() + (int)GetTickCount());
|
|
|
| const int interval_ms = InpIntervalSec * 1000;
|
|
|
| while(!IsStopped())
|
| {
|
| //--- 1. Подключаемся к базе, если ещё не подключены
|
| if(g_db == INVALID_HANDLE)
|
| {
|
| if(!OpenDatabase())
|
| {
|
| Sleep(interval_ms);
|
| continue;
|
| }
|
| Print("База открыта, таблицы terminals и states готовы");
|
| g_terminal_id = -1;
|
| }
|
|
|
| //--- 2. Регистрируем терминал, если ещё не зарегистрирован
|
| if(g_terminal_id < 0)
|
| {
|
| g_terminal_id = RegisterTerminal();
|
| if(g_terminal_id < 0)
|
| {
|
| Sleep(interval_ms);
|
| continue;
|
| }
|
| }
|
|
|
| //--- 3. Записываем текущее состояние
|
| if(!StoreState())
|
| {
|
| g_skipped++;
|
| if(g_skipped % 10 == 0)
|
| PrintFormat("Пропущено записей: %d, последняя ошибка: %d",
|
| g_skipped, GetLastError());
|
| }
|
|
|
| //--- 4. Ждём до следующей итерации, добавляя небольшой случайный разброс
|
| 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);
|
| }
|
| //+------------------------------------------------------------------+
|