bifurcado de antekov/mt5-manager
//+------------------------------------------------------------------+
| |
//| 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);
| |
}
| |
//+------------------------------------------------------------------+
|