mt5-manager/docs/articles/part-04/publish/article4-site.html
485 lines
122 KiB
HTML
<p><b>Содержание</b></p> <ul> <li><a href="/ru/articles/20150/#intro">Введение</a></li> <li><a href="/ru/articles/20150/#current_approach">Чем не устраивает текущий подход</a></li> <li><a href="/ru/articles/20150/#plan">Намечаем путь</a></li> <li><a href="/ru/articles/20150/#service_type">Сервис как тип MQL5-программы</a></li> <li><a href="/ru/articles/20150/#db_schema">Схема базы данных</a></li> <li><a href="/ru/articles/20150/#implementation">Реализация сервиса ServiceStateWriter.mq5</a></li> <li><a href="/ru/articles/20150/#first_run">Что показал первый запуск</a></li> <li><a href="/ru/articles/20150/#python_side">Обновление Python-стороны</a></li> <li><a href="/ru/articles/20150/#compatibility">Совместимость с уже написанным интерфейсом</a></li> <li><a href="/ru/articles/20150/#deploy">Развёртывание через config/services.ini</a></li> <li><a href="/ru/articles/20150/#testing">Тестирование</a></li> <li><a href="/ru/articles/20150/#conclusion">Заключение</a></li> <li><a href="/ru/articles/20150/#archive">Содержание архива</a></li> </ul> <h3 id="intro">Введение</h3> <p>В <a href="/ru/articles/19804" target="_blank">первой части</a> мы определили, что хотим получить: удобный инструмент для управления несколькими экземплярами MetaTrader 5, которого для этой платформы раньше попросту не существовало. Во <a href="/ru/articles/19852" target="_blank">второй</a> мы научились запускать и останавливать произвольное количество терминалов через веб-интерфейс на FastAPI, а в <a href="/ru/articles/19946" target="_blank">третьей</a> добавили конфигурацию и получение данных о счёте, после чего карточки терминалов на странице стали показывать баланс, прибыль, средства и маржу.</p> <p>Способ получения этих данных мы тогда выбрали самый прямолинейный: веб-сервер сам подключался к каждому терминалу через модуль <i>MetaTrader5</i> для Python, вызывал функции <i>mt5.terminal_info()</i> и <i>mt5.account_info()</i>, получая от них информацию о терминале и о торговом счёте, и сразу же отключался. Для первого шага этого вполне достаточно, но у такого подхода есть предел, и в этой части нам как раз предстоит с ним разобраться.</p> <h3 id="current_approach">Чем не устраивает текущий подход</h3> <p>Прежде чем что-то менять, давайте разберёмся, чем нас не устраивает нынешний способ получения данных. Дело в том, что функции библиотеки MetaTrader 5 могут одновременно работать только с одним терминалом, а именно с тем, соединение с которым было установлено вызовом <i>mt5.initialize()</i>. Поэтому опрос разных терминалов идёт строго по очереди, а между итерациями приходится вызывать функцию <i>mt5.shutdown()</i>. Для четырёх терминалов это почти незаметно, но если их станет пятнадцать, цикл опроса, скорее всего, перестанет укладывать<D182>
<span class="preprocessor">#property </span><span class="macro">service</span>
<span class="comment">//+------------------------------------------------------------------+</span>
<span class="comment">//| 2. OnStart: единственная точка входа&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |</span>
<span class="comment">//+------------------------------------------------------------------+</span>
<span class="keyword">void</span> <span class="functions">OnStart</span>()
&nbsp;&nbsp;{
<span class="comment">//--- 3. Работаем, пока терминал или пользователь не попросят остановиться</span>
&nbsp;&nbsp; <span class="keyword">while</span>(!<span class="functions">IsStopped</span>())
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">//--- Здесь выполняется полезная работа</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// ...</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">//--- 4. Делаем паузу до следующей итерации</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">Sleep</span>(<span class="number">1000</span>);
&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;}
</pre><button type="button" class="copy-code-btn c-b8amxo" title="Скопировать в буфер обмена"><svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4.5H9
C9.39782 4.5 9.77936 4.65804 10.0607 4.93934
C10.342 5.22064 10.5 5.60218 10.5 6V14
C10.5 14.3978 10.342 14.7794 10.0607 15.0607
C9.77936 15.342 9.39782 15.5 9 15.5H4
C3.60218 15.5 3.22064 15.342 2.93934 15.0607
C2.65804 14.7794 2.5 14.3978 2.5 14V6
C2.5 5.60218 2.65804 5.22064 2.93934 4.93934
C3.22064 4.65804 3.60218 4.5 4 4.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.5H11L14.5 5V11
C14.5 11.3978 14.342 11.7794 14.0607 12.0607
C13.7794 12.342 13.3978 12.5 13 12.5H7
C6.60218 12.5 6.22064 12.342 5.93934 12.0607
C5.65804 11.7794 5.5 11.3978 5.5 11V3
C5.5 2.60218 5.65804 2.22064 5.93934 1.93934
C6.22064 1.65804 6.60218 1.5 7 1.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M11 1.5V3.5C11 3.89782 11.158 4.27936 11.4393 4.56066C11.7206 4.84196 12.1022 5 12.5 5H14.5L11 1.5Z" fill="#fff" stroke="#A1A1A1" stroke-linejoin="bevel"></path></svg><!----></button></div> <p>Главное отличие сервиса от советников, скриптов и индикаторов состоит в том, что он не привязан к графику. Те программы запускаются на графике, а экземпляр сервиса создаётся прямо из Навигатора командой «Добавить сервис» в контекстном меню. Из этого следует несколько полезных для нас вещей. Во-первых, судя по учебнику, сервис продолжает работать при переключении торгового счёта, тогда как советники и индикаторы при смене счёта перезагружаются вместе с графиками. Во-вторых, если на момент остановки терминала сервис был запущен, то после следующего запуска терминала он загрузится сам, без нашего участия. Для программы, которая должна непрерывно снимать показания с терминала, это почти то, что нужно.</p> <p>Есть и ещё одно отличие, связанное с многопоточностью. В документации по запуску программ модель выполнения индикаторов противопоставлена всем остальным: для индикаторов выделяется один общий поток на все индикаторы на одном символе, поэтому зациклившийся индикатор останавливает работу остальных. Скрипты, советники и сервисы устроены иначе, и у каждого запущенного экземпляра есть собственный поток. Так что сервис, работающий в бесконечном цикле (а это для него штатный режим), не мешает другим программам, в том числе таким же сервисам в других терминалах.</p> <h4>Ограничения</h4> <p>У сервисов, как и у любых программ без графика, есть и свои ограничения. Для них недоступны функции таймера <i>EventSetTimer()</i>, <i>EventSetMillisecondTimer()</i> и <i>EventKillTimer()</i>, поэтому единственный способ сделать работу периодической — это вызывать <i>Sleep()</i> внутри цикла. Недоступна также функция <i>ExpertRemove()</i>, а вместе с ней и функции работы с буферами индикаторов, вроде <i>SetIndexBuffer()</i>. Это вполне логично, раз у сервиса нет графика, на котором всё это могло бы отображаться.</p> <p>Кроме того, сервис сам не получает никаких событий. Он может лишь отправлять пользовательские события графикам с помощью <i>EventChartCustom()</i>, но в нашей задаче нам это не понадобится.</p> <h4>Для чего сервисы применяются</h4> <p>Сервис — это фоновый поставщик или потребитель данных, работающий сам по себе, без интерфейса и <20>
&nbsp;&nbsp;&nbsp;&nbsp;id&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; INTEGER PRIMARY KEY,
&nbsp;&nbsp;&nbsp;&nbsp;folder&nbsp;&nbsp;&nbsp;&nbsp; TEXT NOT <span class="macro">NULL</span> UNIQUE,&nbsp;&nbsp;-- ключ связи с config.json
&nbsp;&nbsp;&nbsp;&nbsp;path&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; TEXT,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-- полный путь, для диагностики
&nbsp;&nbsp;&nbsp;&nbsp;company&nbsp;&nbsp;&nbsp;&nbsp;TEXT,
&nbsp;&nbsp;&nbsp;&nbsp;build&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;INTEGER,
&nbsp;&nbsp;&nbsp;&nbsp;first_seen INTEGER,
&nbsp;&nbsp;&nbsp;&nbsp;last_seen&nbsp;&nbsp;INTEGER
);
CREATE TABLE IF NOT EXISTS states (
&nbsp;&nbsp;&nbsp;&nbsp;terminal_id&nbsp;&nbsp; INTEGER PRIMARY KEY REFERENCES terminals(id),
&nbsp;&nbsp;&nbsp;&nbsp;updated_at&nbsp;&nbsp;&nbsp;&nbsp;INTEGER NOT <span class="macro">NULL</span>,
&nbsp;&nbsp;&nbsp;&nbsp;connected&nbsp;&nbsp;&nbsp;&nbsp; INTEGER,
&nbsp;&nbsp;&nbsp;&nbsp;ping_last&nbsp;&nbsp;&nbsp;&nbsp; INTEGER,
&nbsp;&nbsp;&nbsp;&nbsp;login&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; INTEGER,
&nbsp;&nbsp;&nbsp;&nbsp;server&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;TEXT,
&nbsp;&nbsp;&nbsp;&nbsp;currency&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;TEXT,
&nbsp;&nbsp;&nbsp;&nbsp;leverage&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;INTEGER,
&nbsp;&nbsp;&nbsp;&nbsp;trade_mode&nbsp;&nbsp;&nbsp;&nbsp;INTEGER,
&nbsp;&nbsp;&nbsp;&nbsp;trade_allowed INTEGER,
&nbsp;&nbsp;&nbsp;&nbsp;balance&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; REAL,
&nbsp;&nbsp;&nbsp;&nbsp;equity&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;REAL,
&nbsp;&nbsp;&nbsp;&nbsp;profit&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;REAL,
&nbsp;&nbsp;&nbsp;&nbsp;margin&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;REAL,
&nbsp;&nbsp;&nbsp;&nbsp;margin_free&nbsp;&nbsp; REAL,
&nbsp;&nbsp;&nbsp;&nbsp;margin_level&nbsp;&nbsp;REAL,
&nbsp;&nbsp;&nbsp;&nbsp;credit&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;REAL,
&nbsp;&nbsp;&nbsp;&nbsp;positions&nbsp;&nbsp;&nbsp;&nbsp; INTEGER,
&nbsp;&nbsp;&nbsp;&nbsp;company&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; TEXT,
&nbsp;&nbsp;&nbsp;&nbsp;holder_name&nbsp;&nbsp; TEXT,
&nbsp;&nbsp;&nbsp;&nbsp;error_code&nbsp;&nbsp;&nbsp;&nbsp;INTEGER DEFAULT <span class="number">0</span>,
&nbsp;&nbsp;&nbsp;&nbsp;error_text&nbsp;&nbsp;&nbsp;&nbsp;TEXT
);</pre><button type="button" class="copy-code-btn c-b8amxo" title="Скопировать в буфер обмена"><svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4.5H9
C9.39782 4.5 9.77936 4.65804 10.0607 4.93934
C10.342 5.22064 10.5 5.60218 10.5 6V14
C10.5 14.3978 10.342 14.7794 10.0607 15.0607
C9.77936 15.342 9.39782 15.5 9 15.5H4
C3.60218 15.5 3.22064 15.342 2.93934 15.0607
C2.65804 14.7794 2.5 14.3978 2.5 14V6
C2.5 5.60218 2.65804 5.22064 2.93934 4.93934
C3.22064 4.65804 3.60218 4.5 4 4.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.5H11L14.5 5V11
C14.5 11.3978 14.342 11.7794 14.0607 12.0607
C13.7794 12.342 13.3978 12.5 13 12.5H7
C6.60218 12.5 6.22064 12.342 5.93934 12.0607
C5.65804 11.7794 5.5 11.3978 5.5 11V3
C5.5 2.60218 5.65804 2.22064 5.93934 1.93934
C6.22064 1.65804 6.60218 1.5 7 1.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M11 1.5V3.5C11 3.89782 11.158 4.27936 11.4393 4.56066C11.7206 4.84196 12.1022 5 12.5 5H14.5L11 1.5Z" fill="#fff" stroke="#A1A1A1" stroke-linejoin="bevel"></path></svg><!----></button></div> <p>Несколько решений в этой схеме стоит пояснить отдельно.</p> <p>Начнём с того, что записи в таблице <i>terminals</i> не удаляются, даже когда все терминалы выключены. Она хранит то, что меняется редко, поэтому список известных экземпляров виден в базе всегда, а не только пока работает сервис. Размер базы при этом не растёт со временем: сейчас в ней лежит только текущее состояние, по одной строке на терминал в каждой таблице. Историю показателей и журнал операций мы сознательно отложили до следующих частей, под них потребуется отдельная таблица. По той же причине мы не стали добавлять индексы сверх первичных ключей: на таких объёмах пользы от них вряд ли будет много.</p> <p>Кроме того, в схеме нет полей <i>pid</i> и <i>status</i>, и это сделано намеренно. Сервис не может достоверно знать, что терминал остановлен: если терминал закрывают, сервис прерывается вместе с ним. Поэтому эта часть информации остаётся за <i>psutil</i> на стороне Python, а не за базой.</p> <p>Время мы храним в виде целого числа секунд Unix, а не строки. Это решение оказалось не таким простым, как выглядит на бумаге, поэтому мы ещё вернёмся к нему. Отдельное поле <i>margin_level</i> понадобилось потому, что при нулевой марже отношение <i>equity</i> к <i>margin</i> не определено, и лучше один раз обработать такую особенность в MQL5, чем в JavaScript на клиенте.</p> <h3 id="implementation">Реализация сервиса ServiceStateWriter.mq5</h3> <p>Весь код сервиса находится в одном файле <i>ServiceStateWriter.mq5</i>. У него есть четыре входных параметра: имя файла базы данных, интервал записи в секундах, разброс этого интервала в миллисекундах (зачем он нужен, станет понятно чуть позже) и таймаут занятости базы. Значения по умолчанию нас вполне устраивают, поэтому менять их пока не придётся.</p> <p>Разберём сервис в том порядке, в котором он работает. Сначала он определит, в каком экземпляре терминала запущен, затем откроет базу и зарегистрирует в ней терминал, а после этого перейдёт к основному циклу, где состояние записывается раз за разом.</p> <h4>Определение имени экземпляра</h4> <p>Первым делом сервису нужно понять, в каком именно экземпляре терминала он работает: имя экземпляра служит ключом с
<span class="comment">//| FindLast: поиск последнего вхождения подстроки&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |</span>
<span class="comment">//+------------------------------------------------------------------+</span>
<span class="keyword">int</span> FindLast(<span class="keyword">const</span> <span class="keyword">string</span> text, <span class="keyword">const</span> <span class="keyword">string</span> what)
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">int</span> pos = -<span class="number">1</span>;
&nbsp;&nbsp; <span class="keyword">int</span> next = <span class="functions">StringFind</span>(text, what, <span class="number">0</span>);
&nbsp;&nbsp; <span class="keyword">while</span>(next &gt;= <span class="number">0</span>)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pos = next;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;next = <span class="functions">StringFind</span>(text, what, next + <span class="number">1</span>);
&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp; <span class="keyword">return</span> pos;
&nbsp;&nbsp;}
</pre><button type="button" class="copy-code-btn c-b8amxo" title="Скопировать в буфер обмена"><svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4.5H9
C9.39782 4.5 9.77936 4.65804 10.0607 4.93934
C10.342 5.22064 10.5 5.60218 10.5 6V14
C10.5 14.3978 10.342 14.7794 10.0607 15.0607
C9.77936 15.342 9.39782 15.5 9 15.5H4
C3.60218 15.5 3.22064 15.342 2.93934 15.0607
C2.65804 14.7794 2.5 14.3978 2.5 14V6
C2.5 5.60218 2.65804 5.22064 2.93934 4.93934
C3.22064 4.65804 3.60218 4.5 4 4.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.5H11L14.5 5V11
C14.5 11.3978 14.342 11.7794 14.0607 12.0607
C13.7794 12.342 13.3978 12.5 13 12.5H7
C6.60218 12.5 6.22064 12.342 5.93934 12.0607
C5.65804 11.7794 5.5 11.3978 5.5 11V3
C5.5 2.60218 5.65804 2.22064 5.93934 1.93934
C6.22064 1.65804 6.60218 1.5 7 1.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M11 1.5V3.5C11 3.89782 11.158 4.27936 11.4393 4.56066C11.7206 4.84196 12.1022 5 12.5 5H14.5L11 1.5Z" fill="#fff" stroke="#A1A1A1" stroke-linejoin="bevel"></path></svg><!----></button></div> <p>С её помощью выделим из этого пути последний компонент:</p> <div class="code-box"><pre class="code" data-enhanced="true"><span class="comment">//+------------------------------------------------------------------+</span>
<span class="comment">//| InstanceFolder: имя папки экземпляра терминала&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |</span>
<span class="comment">//+------------------------------------------------------------------+</span>
<span class="keyword">string</span> InstanceFolder()
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">string</span> path = <span class="functions">TerminalInfoString</span>(<span class="macro">TERMINAL_DATA_PATH</span>);
&nbsp;&nbsp; <span class="functions">StringReplace</span>(path, <span class="string">"\\"</span>, <span class="string">"/"</span>);
&nbsp;&nbsp; <span class="keyword">int</span> len = <span class="functions">StringLen</span>(path);
&nbsp;&nbsp; <span class="keyword">if</span>(len &gt; <span class="number">0</span> &amp;&amp; <span class="functions">StringGetCharacter</span>(path, len - <span class="number">1</span>) == <span class="string">'/'</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;path = <span class="functions">StringSubstr</span>(path, <span class="number">0</span>, len - <span class="number">1</span>);
&nbsp;&nbsp; <span class="keyword">int</span> pos = FindLast(path, <span class="string">"/"</span>);
&nbsp;&nbsp; <span class="keyword">return</span> (pos &gt;= <span class="number">0</span>) ? <span class="functions">StringSubstr</span>(path, pos + <span class="number">1</span>) : path;
&nbsp;&nbsp;}
</pre><button type="button" class="copy-code-btn c-b8amxo" title="Скопировать в буфер обмена"><svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4.5H9
C9.39782 4.5 9.77936 4.65804 10.0607 4.93934
C10.342 5.22064 10.5 5.60218 10.5 6V14
C10.5 14.3978 10.342 14.7794 10.0607 15.0607
C9.77936 15.342 9.39782 15.5 9 15.5H4
C3.60218 15.5 3.22064 15.342 2.93934 15.0607
C2.65804 14.7794 2.5 14.3978 2.5 14V6
C2.5 5.60218 2.65804 5.22064 2.93934 4.93934
C3.22064 4.65804 3.60218 4.5 4 4.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.5H11L14.5 5V11
C14.5 11.3978 14.342 11.7794 14.0607 12.0607
C13.7794 12.342 13.3978 12.5 13 12.5H7
C6.60218 12.5 6.22064 12.342 5.93934 12.0607
C5.65804 11.7794 5.5 11.3978 5.5 11V3
C5.5 2.60218 5.65804 2.22064 5.93934 1.93934
C6.22064 1.65804 6.60218 1.5 7 1.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M11 1.5V3.5C11 3.89782 11.158 4.27936 11.4393 4.56066C11.7206 4.84196 12.1022 5 12.5 5H14.5L11 1.5Z" fill="#fff" stroke="#A1A1A1" stroke-linejoin="bevel"></path></svg><!----></button></div> <p>Здесь мы берём путь <i>TERMINAL_DATA_PATH</i>, то есть папку, в которой хранятся данные терминала. Это сделано намеренно: менеджер всегда запускает терминалы с флагом <i>/portable</i>, и в этом режиме папка данных совпадает с папкой самого терминала, а её последний компонент является именем экземпляра, по которому мы ищем терминал в <i>config.json</i>. Таким образом, мы вводим ограничение: сервис рассчитан на работу только в portable-режиме.</p> <p>Два вопроса мы пока оставим открытыми. Во-первых, Windows не различает регистр в путях, поэтому <i>MetaTrader5.1</i> и <i>Metatrader5.1</i> для неё одна и та же папка, хотя как строки они, конечно, различаются. Пока имя папки вписывается в <i>config.json</i> вручную, такое расхождение вполне возможно, но когда у нас появится автоматическое создание экземпляров, имя будет подставляться само. Во-вторых, непонятно, что делать, если сервис зарегистрировал в базе папку, которой нет в <i>config.json</i>. Этот вопрос мы отложим до следующих частей.</p> <h4>Открытие базы и PRAGMA</h4> <p>Все действия с базой данных мы пока оформим в виде отдельных функций. В дальнейшем их можно будет собрать в отдельный класс, как мы уже делали в другой серии статей, но на данном этапе так проще проследить, что и в каком порядке происходит.</p> <p>Базу открывает функция <i>OpenDatabase()</i>. Файл создаётся, если его ещё нет, и открывается на чтение и запись в общей папке <i>Common</i>, о чём говорят флаги <i>DATABASE_OPEN_READWRITE</i>, <i>DATABASE_OPEN_CREATE</i> и <i>DATABASE_OPEN_COMMON</i>. Затем с помощью <i>DatabaseExecute()</i> выполняются два PRAGMA (если что-то пойдёт не так, код ошибки просто попадёт в журнал), а функция <i>EnsureSchema()</i> создаёт описанные выше таблицы, если их там ещё нет:</p> <div class="code-box"><pre class="code" data-enhanced="true"><span class="comment">//+------------------------------------------------------------------+</span>
<span class="comment">//| OpenDatabase: открытие базы и настройка соединения&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |</span>
<span class="comment">//+------------------------------------------------------------------+</span>
<span class="keyword">bool</span> OpenDatabase()
&nbsp;&nbsp;{
&nbsp;&nbsp; g_db = <span class="functions">DatabaseOpen</span>(InpDatabase,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="macro">DATABASE_OPEN_READWRITE</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | <span class="macro">DATABASE_OPEN_CREATE</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | <span class="macro">DATABASE_OPEN_COMMON</span>);
&nbsp;&nbsp; <span class="keyword">if</span>(g_db == <span class="macro">INVALID_HANDLE</span>)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">PrintFormat</span>(<span class="string">"Не удалось открыть базу '%s', код ошибки: %d"</span>,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;InpDatabase, <span class="functions">GetLastError</span>());
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span> <span class="macro">false</span>;
&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp; <span class="keyword">if</span>(!<span class="functions">DatabaseExecute</span>(g_db, <span class="functions">StringFormat</span>(<span class="string">"PRAGMA busy_timeout=%d"</span>, InpBusyTimeout)))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">PrintFormat</span>(<span class="string">"Не удалось задать busy_timeout, код ошибки: %d"</span>, <span class="functions">GetLastError</span>());
&nbsp;&nbsp; <span class="keyword">if</span>(!<span class="functions">DatabaseExecute</span>(g_db, <span class="string">"PRAGMA journal_mode=WAL"</span>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">PrintFormat</span>(<span class="string">"Не удалось включить режим WAL, код ошибки: %d"</span>, <span class="functions">GetLastError</span>());
&nbsp;&nbsp; <span class="keyword">if</span>(!EnsureSchema(g_db))
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">DatabaseClose</span>(g_db);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;g_db = <span class="macro">INVALID_HANDLE</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span> <span class="macro">false</span>;
&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp; <span class="keyword">return</span> <span class="macro">true</span>;
&nbsp;&nbsp;}
</pre><button type="button" class="copy-code-btn c-b8amxo" title="Скопировать в буфер обмена"><svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4.5H9
C9.39782 4.5 9.77936 4.65804 10.0607 4.93934
C10.342 5.22064 10.5 5.60218 10.5 6V14
C10.5 14.3978 10.342 14.7794 10.0607 15.0607
C9.77936 15.342 9.39782 15.5 9 15.5H4
C3.60218 15.5 3.22064 15.342 2.93934 15.0607
C2.65804 14.7794 2.5 14.3978 2.5 14V6
C2.5 5.60218 2.65804 5.22064 2.93934 4.93934
C3.22064 4.65804 3.60218 4.5 4 4.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.5H11L14.5 5V11
C14.5 11.3978 14.342 11.7794 14.0607 12.0607
C13.7794 12.342 13.3978 12.5 13 12.5H7
C6.60218 12.5 6.22064 12.342 5.93934 12.0607
C5.65804 11.7794 5.5 11.3978 5.5 11V3
C5.5 2.60218 5.65804 2.22064 5.93934 1.93934
C6.22064 1.65804 6.60218 1.5 7 1.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M11 1.5V3.5C11 3.89782 11.158 4.27936 11.4393 4.56066C11.7206 4.84196 12.1022 5 12.5 5H14.5L11 1.5Z" fill="#fff" stroke="#A1A1A1" stroke-linejoin="bevel"></path></svg><!----></button></div> <p>Эти два PRAGMA нужны для того, чтобы несколько сервисов могли без проблем записывать данные в один и тот же файл. Первый из них, <i>journal_mode=WAL</i>, избавляет читателей и писателя от взаимных блокировок, поэтому веб-приложение сможет читать базу, пока сервисы в неё пишут. Это свойство самого файла: оно записывается в его заголовок один раз и сохраняется дальше, так что повторная установка при каждом запуске сервиса безвредна и просто подстраховывает на случай, если базу первым создаст другой процесс. Однако стоит иметь в виду, что WAL опирается на разделяемую память в пределах одной машины и не работает через сетевую папку. Поэтому, если распределённая схема из первой части когда-нибудь понадобится нам всерьёз, у каждого сервера должна быть своя база.</p> <p>Второй PRAGMA, <i>busy_timeout</i>, в отличие от <i>journal_mode</i>, относится не к файлу, а к соединению. Он не сохраняется, поэтому его нужно устанавливать заново при каждом открытии базы в каждом сервисе. Благодаря ему при попытке записи в занятую базу библиотека не сразу вернёт ошибку <i>SQLITE_BUSY</i>, а подождёт и повторит попытку. Режим WAL эту проблему не решает: писатель по-прежнему может быть только один, и сервисы просто становятся в очередь на запись, вместо того чтобы завершаться с ошибкой.</p> <h4>Регистрация терминала</h4> <p>Сведения о самом терминале (папка, путь, брокер и билд) меняются редко, поэтому записываем мы их в таблицу <i>terminals</i> не в основном цикле, а один раз при старте сервиса, и повторно, если базу пришлось переоткрывать. Функция <i>RegisterTerminal()</i> вставляет новую запись, а если запись об этом терминале уже есть, просто обновляет её:</p> <div class="code-box"><pre class="code" data-enhanced="true"><span class="comment">//+------------------------------------------------------------------+</span>
<span class="comment">//| RegisterTerminal: регистрация терминала в базе&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |</span>
<span class="comment">//+------------------------------------------------------------------+</span>
<span class="keyword">int</span> RegisterTerminal()
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">const</span> <span class="keyword">string</span> folder&nbsp;&nbsp;= InstanceFolder();
&nbsp;&nbsp; <span class="keyword">const</span> <span class="keyword">string</span> path&nbsp;&nbsp;&nbsp;&nbsp;= <span class="functions">TerminalInfoString</span>(<span class="macro">TERMINAL_DATA_PATH</span>);
&nbsp;&nbsp; <span class="keyword">const</span> <span class="keyword">string</span> company = <span class="functions">TerminalInfoString</span>(<span class="macro">TERMINAL_COMPANY</span>);
&nbsp;&nbsp; <span class="keyword">const</span> <span class="keyword">int</span>&nbsp;&nbsp;&nbsp;&nbsp;build&nbsp;&nbsp; = (<span class="keyword">int</span>)<span class="functions">TerminalInfoInteger</span>(<span class="macro">TERMINAL_BUILD</span>);
&nbsp;&nbsp; <span class="keyword">const</span> <span class="keyword">long</span>&nbsp;&nbsp; now&nbsp;&nbsp;&nbsp;&nbsp; = (<span class="keyword">long</span>)<span class="functions">TimeGMT</span>();
&nbsp;&nbsp; <span class="keyword">const</span> <span class="keyword">string</span> sql =
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="string">"INSERT INTO terminals (folder, path, company, build, first_seen, last_seen) "</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="string">"VALUES (?1, ?2, ?3, ?4, ?5, ?5) "</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="string">"ON CONFLICT(folder) DO UPDATE SET "</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="string">"&nbsp;&nbsp;path = excluded.path,"</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="string">"&nbsp;&nbsp;company = excluded.company,"</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="string">"&nbsp;&nbsp;build = excluded.build,"</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="string">"&nbsp;&nbsp;last_seen = excluded.last_seen"</span>;
&nbsp;&nbsp; <span class="comment">// ... DatabasePrepare / DatabaseBind / DatabaseRead, см. полный листинг</span>
&nbsp;&nbsp;}
</pre><button type="button" class="copy-code-btn c-b8amxo" title="Скопировать в буфер обмена"><svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4.5H9
C9.39782 4.5 9.77936 4.65804 10.0607 4.93934
C10.342 5.22064 10.5 5.60218 10.5 6V14
C10.5 14.3978 10.342 14.7794 10.0607 15.0607
C9.77936 15.342 9.39782 15.5 9 15.5H4
C3.60218 15.5 3.22064 15.342 2.93934 15.0607
C2.65804 14.7794 2.5 14.3978 2.5 14V6
C2.5 5.60218 2.65804 5.22064 2.93934 4.93934
C3.22064 4.65804 3.60218 4.5 4 4.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.5H11L14.5 5V11
C14.5 11.3978 14.342 11.7794 14.0607 12.0607
C13.7794 12.342 13.3978 12.5 13 12.5H7
C6.60218 12.5 6.22064 12.342 5.93934 12.0607
C5.65804 11.7794 5.5 11.3978 5.5 11V3
C5.5 2.60218 5.65804 2.22064 5.93934 1.93934
C6.22064 1.65804 6.60218 1.5 7 1.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M11 1.5V3.5C11 3.89782 11.158 4.27936 11.4393 4.56066C11.7206 4.84196 12.1022 5 12.5 5H14.5L11 1.5Z" fill="#fff" stroke="#A1A1A1" stroke-linejoin="bevel"></path></svg><!----></button></div> <p>В этой функции есть несколько моментов, на которых стоит остановиться. Во-первых, запрос <i>INSERT ... ON CONFLICT DO UPDATE</i> надёжнее классической связки «сначала <i>UPDATE</i>, а если ни одна строка не изменилась, то <i>INSERT</i>»: он не требует отдельно различать первый запуск и все последующие, и ему не мешает, если строку из базы удалили вручную.</p> <p>Во-вторых, идентификатор вставленной или обновлённой строки нельзя получить через <i>last_insert_rowid()</i>: если сработала ветка обновления, вставки не было, и функция вернёт устаревшее значение. Можно было бы добавить в запрос <i>RETURNING id</i>, но для этого нужен SQLite версии 3.35 и выше, а рисковать версией движка ради экономии одного запроса при старте не хочется. Поэтому идентификатор мы получаем отдельным запросом <i>SELECT</i> сразу после вставки.</p> <p>В-третьих, нумерация параметров в функции <i>DatabaseBind()</i> начинается с нуля, а плейсхолдеры <i>?1</i>, <i>?2</i> в самом запросе нумеруются с единицы. Поначалу это похоже на опечатку, но, судя по примеру в <a href="/ru/docs/database/databasebind" target="_blank">документации по <i>DatabaseBind()</i></a>, так и задумано: <i>?1</i> связывается вызовом <i>DatabaseBind(request, 0, ...)</i>. На этом легко споткнуться, если не знать заранее.</p> <h4>Запись состояния</h4> <p>Состояние терминала и торгового счёта собирает и записывает в таблицу <i>states</i> функция <i>StoreState()</i>. В её начале мы просто запрашиваем нужные показатели встроенными функциями MQL5:</p> <div class="code-box"><pre class="code" data-enhanced="true"><span class="comment">//--- Состояние терминала</span>
<span class="keyword">const</span> <span class="keyword">bool</span> connected = (<span class="keyword">bool</span>)<span class="functions">TerminalInfoInteger</span>(<span class="macro">TERMINAL_CONNECTED</span>);
<span class="keyword">const</span> <span class="keyword">int</span>&nbsp;&nbsp;ping_last = (<span class="keyword">int</span>)<span class="functions">TerminalInfoInteger</span>(<span class="macro">TERMINAL_PING_LAST</span>);
<span class="comment">//--- Состояние торгового счёта</span>
<span class="keyword">const</span> <span class="keyword">long</span>&nbsp;&nbsp; login&nbsp;&nbsp; = <span class="functions">AccountInfoInteger</span>(<span class="macro">ACCOUNT_LOGIN</span>);
<span class="keyword">const</span> <span class="keyword">double</span> balance = <span class="functions">AccountInfoDouble</span>(<span class="macro">ACCOUNT_BALANCE</span>);
<span class="keyword">const</span> <span class="keyword">double</span> equity&nbsp;&nbsp;= <span class="functions">AccountInfoDouble</span>(<span class="macro">ACCOUNT_EQUITY</span>);
<span class="comment">// ...</span>
<span class="keyword">const</span> <span class="keyword">int</span>&nbsp;&nbsp;positions&nbsp;&nbsp;= <span class="functions">PositionsTotal</span>();
<span class="keyword">const</span> <span class="keyword">long</span> updated_at = (<span class="keyword">long</span>)<span class="functions">TimeGMT</span>();
</pre><button type="button" class="copy-code-btn c-b8amxo" title="Скопировать в буфер обмена"><svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4.5H9
C9.39782 4.5 9.77936 4.65804 10.0607 4.93934
C10.342 5.22064 10.5 5.60218 10.5 6V14
C10.5 14.3978 10.342 14.7794 10.0607 15.0607
C9.77936 15.342 9.39782 15.5 9 15.5H4
C3.60218 15.5 3.22064 15.342 2.93934 15.0607
C2.65804 14.7794 2.5 14.3978 2.5 14V6
C2.5 5.60218 2.65804 5.22064 2.93934 4.93934
C3.22064 4.65804 3.60218 4.5 4 4.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.5H11L14.5 5V11
C14.5 11.3978 14.342 11.7794 14.0607 12.0607
C13.7794 12.342 13.3978 12.5 13 12.5H7
C6.60218 12.5 6.22064 12.342 5.93934 12.0607
C5.65804 11.7794 5.5 11.3978 5.5 11V3
C5.5 2.60218 5.65804 2.22064 5.93934 1.93934
C6.22064 1.65804 6.60218 1.5 7 1.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M11 1.5V3.5C11 3.89782 11.158 4.27936 11.4393 4.56066C11.7206 4.84196 12.1022 5 12.5 5H14.5L11 1.5Z" fill="#fff" stroke="#A1A1A1" stroke-linejoin="bevel"></path></svg><!----></button></div> <p>Затем, в зависимости от того, есть ли соединение с торговым сервером, выбираются код и текст статуса:</p> <div class="code-box"><pre class="code" data-enhanced="true"><span class="keyword">const</span> <span class="keyword">int</span>&nbsp;&nbsp;&nbsp;&nbsp;error_code = connected ? <span class="number">1</span> : -<span class="number">2</span>;
<span class="keyword">const</span> <span class="keyword">string</span> error_text = connected ? <span class="string">"Success"</span> : <span class="string">"Service: No connection to trade server"</span>;
</pre><button type="button" class="copy-code-btn c-b8amxo" title="Скопировать в буфер обмена"><svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4.5H9
C9.39782 4.5 9.77936 4.65804 10.0607 4.93934
C10.342 5.22064 10.5 5.60218 10.5 6V14
C10.5 14.3978 10.342 14.7794 10.0607 15.0607
C9.77936 15.342 9.39782 15.5 9 15.5H4
C3.60218 15.5 3.22064 15.342 2.93934 15.0607
C2.65804 14.7794 2.5 14.3978 2.5 14V6
C2.5 5.60218 2.65804 5.22064 2.93934 4.93934
C3.22064 4.65804 3.60218 4.5 4 4.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.5H11L14.5 5V11
C14.5 11.3978 14.342 11.7794 14.0607 12.0607
C13.7794 12.342 13.3978 12.5 13 12.5H7
C6.60218 12.5 6.22064 12.342 5.93934 12.0607
C5.65804 11.7794 5.5 11.3978 5.5 11V3
C5.5 2.60218 5.65804 2.22064 5.93934 1.93934
C6.22064 1.65804 6.60218 1.5 7 1.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M11 1.5V3.5C11 3.89782 11.158 4.27936 11.4393 4.56066C11.7206 4.84196 12.1022 5 12.5 5H14.5L11 1.5Z" fill="#fff" stroke="#A1A1A1" stroke-linejoin="bevel"></path></svg><!----></button></div> <p>Эти коды продолжают числовой ряд, который уже использовался на стороне менеджера (например, <i>-1</i> означает «Manager: Terminal not found»), а префикс в тексте сразу показывает, кто именно сообщает о проблеме: сервис или менеджер. Единицу для успешного состояния мы выбрали не случайно: функция <i>mt5.last_error()</i> в модуле <i>MetaTrader5</i> возвращает <i>RES_S_OK</i>, равный единице, а ноль у нас уже занят статусами, которые расставляются вручную. Сохранив это соглашение, мы можем не менять логику отображения статуса на фронтенде.</p> <p>После этого данные записываются так же, как и при регистрации терминала, запросом <i>INSERT ... ON CONFLICT(terminal_id) DO UPDATE</i>, и всегда в рамках явной транзакции:</p> <div class="code-box"><pre class="code" data-enhanced="true"><span class="functions">ResetLastError</span>();
<span class="keyword">if</span>(!<span class="functions">DatabaseTransactionBegin</span>(g_db))
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="functions">DatabaseFinalize</span>(request);
&nbsp;&nbsp; <span class="keyword">return</span> <span class="macro">false</span>;
&nbsp;&nbsp;}
<span class="functions">DatabaseRead</span>(request);
<span class="keyword">const</span> <span class="keyword">int</span> error = <span class="functions">GetLastError</span>();
<span class="functions">DatabaseFinalize</span>(request);
<span class="keyword">if</span>(error != <span class="number">0</span> &amp;&amp; error != <span class="macro">ERR_DATABASE_NO_MORE_DATA</span>)
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="functions">DatabaseTransactionRollback</span>(g_db);
&nbsp;&nbsp; <span class="keyword">return</span> <span class="macro">false</span>;
&nbsp;&nbsp;}
<span class="keyword">if</span>(!<span class="functions">DatabaseTransactionCommit</span>(g_db))
&nbsp;&nbsp; <span class="keyword">return</span> <span class="macro">false</span>;
</pre><button type="button" class="copy-code-btn c-b8amxo" title="Скопировать в буфер обмена"><svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4.5H9
C9.39782 4.5 9.77936 4.65804 10.0607 4.93934
C10.342 5.22064 10.5 5.60218 10.5 6V14
C10.5 14.3978 10.342 14.7794 10.0607 15.0607
C9.77936 15.342 9.39782 15.5 9 15.5H4
C3.60218 15.5 3.22064 15.342 2.93934 15.0607
C2.65804 14.7794 2.5 14.3978 2.5 14V6
C2.5 5.60218 2.65804 5.22064 2.93934 4.93934
C3.22064 4.65804 3.60218 4.5 4 4.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.5H11L14.5 5V11
C14.5 11.3978 14.342 11.7794 14.0607 12.0607
C13.7794 12.342 13.3978 12.5 13 12.5H7
C6.60218 12.5 6.22064 12.342 5.93934 12.0607
C5.65804 11.7794 5.5 11.3978 5.5 11V3
C5.5 2.60218 5.65804 2.22064 5.93934 1.93934
C6.22064 1.65804 6.60218 1.5 7 1.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M11 1.5V3.5C11 3.89782 11.158 4.27936 11.4393 4.56066C11.7206 4.84196 12.1022 5 12.5 5H14.5L11 1.5Z" fill="#fff" stroke="#A1A1A1" stroke-linejoin="bevel"></path></svg><!----></button></div> <p>Сейчас на каждой итерации у нас выполняется всего один оператор, поэтому транзакция здесь, по сути, не нужна: SQLite и сам обернёт одиночный <i>INSERT</i> в неявную транзакцию. Мы делаем её явной как заготовку на будущее, ведь в следующей части появится запись истории, и тогда за один цикл будет выполняться уже несколько операторов. Вот там транзакция принесёт пользу и в целостности данных, и в скорости. Насколько велика разница, показывает пример из <a href="/ru/docs/database/databasetransactionbegin" target="_blank">документации по <i>DatabaseTransactionBegin()</i></a>: вставка большого количества сделок внутри транзакции оказалась примерно в 500 раз быстрее, чем без неё.</p> <p>Отдельно стоит сказать про проверку результата. Функция <i>DatabaseRead()</i> на запросе, который не возвращает результата, а <i>INSERT</i> и <i>UPDATE</i> именно такие, завершается с кодом <i>ERR_DATABASE_NO_MORE_DATA</i> (5126). Согласно <a href="/ru/docs/database/databaseread" target="_blank">документации по <i>DatabaseRead()</i></a>, это не ошибка, а нормальное завершение, поэтому обе функции, которые пишут в базу, должны пропускать этот код, а не считать его неудачей. К чему приводит забытая проверка, мы увидим в разделе про первый запуск.</p> <h4>Основной цикл</h4> <p>Все отдельные части сервиса готовы, и теперь осталось собрать их вместе в функции <i>OnStart()</i>. Получилась она совсем небольшой, потому что почти вся работа вынесена в отдельные функции:</p> <div class="code-box"><pre class="code" data-enhanced="true"><span class="comment">//+------------------------------------------------------------------+</span>
<span class="comment">//| OnStart: точка входа сервиса&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |</span>
<span class="comment">//+------------------------------------------------------------------+</span>
<span class="keyword">void</span> <span class="functions">OnStart</span>()
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="functions">PrintFormat</span>(<span class="string">"Запуск сервиса для экземпляра '%s'"</span>, InstanceFolder());
<span class="comment">//--- Разброс интервала, чтобы записи разных сервисов не совпадали по фазе</span>
&nbsp;&nbsp; <span class="functions">MathSrand</span>((<span class="keyword">int</span>)<span class="functions">TimeLocal</span>() + (<span class="keyword">int</span>)<span class="functions">GetTickCount</span>());
&nbsp;&nbsp; <span class="keyword">const</span> <span class="keyword">int</span> interval_ms = InpIntervalSec * <span class="number">1000</span>;
&nbsp;&nbsp; <span class="keyword">while</span>(!<span class="functions">IsStopped</span>())
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">//--- 1. Подключаемся к базе, если ещё не подключены</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(g_db == <span class="macro">INVALID_HANDLE</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(!OpenDatabase())
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">Sleep</span>(interval_ms);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">continue</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; g_terminal_id = -<span class="number">1</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">//--- 2. Регистрируем терминал, если ещё не зарегистрирован</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(g_terminal_id &lt; <span class="number">0</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; g_terminal_id = RegisterTerminal();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(g_terminal_id &lt; <span class="number">0</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">Sleep</span>(interval_ms);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">continue</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">//--- 3. Записываем текущее состояние</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(!StoreState())
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; g_skipped++;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(g_skipped % <span class="number">10</span> == <span class="number">0</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">PrintFormat</span>(<span class="string">"Пропущено записей: %d, последняя ошибка: %d"</span>,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;g_skipped, <span class="functions">GetLastError</span>());
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">//--- 4. Ждём до следующей итерации, добавляя небольшой случайный разброс</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">int</span> pause = interval_ms;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(InpJitterMs &gt; <span class="number">0</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pause += (<span class="keyword">int</span>)(<span class="functions">MathRand</span>() % InpJitterMs);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">Sleep</span>(pause);
&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp; <span class="keyword">if</span>(g_db != <span class="macro">INVALID_HANDLE</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">DatabaseClose</span>(g_db);
&nbsp;&nbsp;}
</pre><button type="button" class="copy-code-btn c-b8amxo" title="Скопировать в буфер обмена"><svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4.5H9
C9.39782 4.5 9.77936 4.65804 10.0607 4.93934
C10.342 5.22064 10.5 5.60218 10.5 6V14
C10.5 14.3978 10.342 14.7794 10.0607 15.0607
C9.77936 15.342 9.39782 15.5 9 15.5H4
C3.60218 15.5 3.22064 15.342 2.93934 15.0607
C2.65804 14.7794 2.5 14.3978 2.5 14V6
C2.5 5.60218 2.65804 5.22064 2.93934 4.93934
C3.22064 4.65804 3.60218 4.5 4 4.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.5H11L14.5 5V11
C14.5 11.3978 14.342 11.7794 14.0607 12.0607
C13.7794 12.342 13.3978 12.5 13 12.5H7
C6.60218 12.5 6.22064 12.342 5.93934 12.0607
C5.65804 11.7794 5.5 11.3978 5.5 11V3
C5.5 2.60218 5.65804 2.22064 5.93934 1.93934
C6.22064 1.65804 6.60218 1.5 7 1.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M11 1.5V3.5C11 3.89782 11.158 4.27936 11.4393 4.56066C11.7206 4.84196 12.1022 5 12.5 5H14.5L11 1.5Z" fill="#fff" stroke="#A1A1A1" stroke-linejoin="bevel"></path></svg><!----></button></div> <p>Пройдёмся по циклу. На каждой итерации мы сначала убеждаемся, что база открыта, затем что терминал зарегистрирован, и только после этого записываем состояние. Если на одном из первых двух шагов что-то не получилось, мы не пытаемся продолжать, а просто ждём один интервал и пробуем ещё раз. Благодаря этому сервис довольно спокойно переживает ситуацию, когда база временно недоступна, и для этого ему не нужна никакая отдельная логика.</p> <p>В конце каждой итерации стоит пауза, и здесь мы позволили себе одну небольшую хитрость. К базовому интервалу добавляется случайная величина в пределах <i>InpJitterMs</i>. Дело в том, что все сервисы стартуют почти одновременно, вместе со своими терминалами, и без такой добавки их циклы <i>Sleep()</i> могли бы довольно долго оставаться синхронными, а значит, записи приходились бы на один и тот же момент. Небольшой разброс размазывает их по времени и, скорее всего, заметно уменьшает число конфликтов на запись, хотя специальных замеров мы не проводили.</p> <p>Ещё одно решение касается ошибок записи. Если <i>StoreState()</i> не удалось, например потому, что база в этот момент занята другим сервисом, мы просто считаем такой пропуск и идём дальше, не пытаясь повторить запись внутри той же итерации. Пока в базе хранится только текущее состояние, это вполне допустимо: свежие данные придут через пару секунд и всё равно перезапишут предыдущие. Когда мы дойдём до истории показателей, пропуски превратятся в дыры в ряду данных, и к этому месту придётся вернуться. Чтобы они хотя бы не оставались незамеченными, каждый десятый пропуск попадает в журнал вместе с кодом последней ошибки.</p> <p>Когда сервис останавливают, <i>IsStopped()</i> начинает возвращать <i>true</i>, цикл завершается, и мы закрываем базу. О том, как именно ведёт себя остановка и почему нам не стоит бояться длинных пауз, поговорим чуть ниже.</p> <h4>Остановка сервиса и грейс-период</h4> <p>Раз <i>OnStart()</i> у нас представляет собой бесконечный цикл <i>while(!IsStopped())</i> со <i>Sleep()</i> внутри, стоит посмотреть, что происходит, когда сервис останавливают, например вручную или при закрытии терминала.</p> <p><a href="/ru/docs/check/isst
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span> <span class="literal">None</span></pre><button type="button" class="copy-code-btn c-b8amxo" title="Скопировать в буфер обмена"><svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4.5H9
C9.39782 4.5 9.77936 4.65804 10.0607 4.93934
C10.342 5.22064 10.5 5.60218 10.5 6V14
C10.5 14.3978 10.342 14.7794 10.0607 15.0607
C9.77936 15.342 9.39782 15.5 9 15.5H4
C3.60218 15.5 3.22064 15.342 2.93934 15.0607
C2.65804 14.7794 2.5 14.3978 2.5 14V6
C2.5 5.60218 2.65804 5.22064 2.93934 4.93934
C3.22064 4.65804 3.60218 4.5 4 4.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.5H11L14.5 5V11
C14.5 11.3978 14.342 11.7794 14.0607 12.0607
C13.7794 12.342 13.3978 12.5 13 12.5H7
C6.60218 12.5 6.22064 12.342 5.93934 12.0607
C5.65804 11.7794 5.5 11.3978 5.5 11V3
C5.5 2.60218 5.65804 2.22064 5.93934 1.93934
C6.22064 1.65804 6.60218 1.5 7 1.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M11 1.5V3.5C11 3.89782 11.158 4.27936 11.4393 4.56066C11.7206 4.84196 12.1022 5 12.5 5H14.5L11 1.5Z" fill="#fff" stroke="#A1A1A1" stroke-linejoin="bevel"></path></svg><!----></button></div> <p>Мы рассчитывали, что так будут обрабатываться штатные ситуации, например отсутствие базы или её временная занятость, но <i>OperationalError</i> — довольно широкая категория. Любая ошибка превращалась в «данных пока нет», а интерфейс бесконечно показывал <i>starting</i>. Нашли мы проблему только тогда, когда выполнили упавший запрос вручную в SQLite:</p> <div class="code-box"><pre class="code" data-enhanced="true">sqlite3.OperationalError: no such column: s.company</pre><button type="button" class="copy-code-btn c-b8amxo" title="Скопировать в буфер обмена"><svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4.5H9
C9.39782 4.5 9.77936 4.65804 10.0607 4.93934
C10.342 5.22064 10.5 5.60218 10.5 6V14
C10.5 14.3978 10.342 14.7794 10.0607 15.0607
C9.77936 15.342 9.39782 15.5 9 15.5H4
C3.60218 15.5 3.22064 15.342 2.93934 15.0607
C2.65804 14.7794 2.5 14.3978 2.5 14V6
C2.5 5.60218 2.65804 5.22064 2.93934 4.93934
C3.22064 4.65804 3.60218 4.5 4 4.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.5H11L14.5 5V11
C14.5 11.3978 14.342 11.7794 14.0607 12.0607
C13.7794 12.342 13.3978 12.5 13 12.5H7
C6.60218 12.5 6.22064 12.342 5.93934 12.0607
C5.65804 11.7794 5.5 11.3978 5.5 11V3
C5.5 2.60218 5.65804 2.22064 5.93934 1.93934
C6.22064 1.65804 6.60218 1.5 7 1.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M11 1.5V3.5C11 3.89782 11.158 4.27936 11.4393 4.56066C11.7206 4.84196 12.1022 5 12.5 5H14.5L11 1.5Z" fill="#fff" stroke="#A1A1A1" stroke-linejoin="bevel"></path></svg><!----></button></div> <p>Теперь обработчик перед возвратом <i>None</i> печатает текст исключения, так что следующая рассинхронизация схемы будет видна в консоли сразу.</p> <p>Из этой истории можно извлечь два вывода. Во-первых, не стоит молча глотать широкие классы исключений. Во-вторых, пока в базе лежит только текущее состояние, миграцию вполне можно заменить удалением файла базы (предварительно остановив все терминалы, которые держат его открытым, иначе в Windows файл не удалится), и сервис пересоздаст таблицы. Когда появится история, этот путь будет закрыт, и придётся вести версию схемы, например через <i>PRAGMA user_version</i>, и выполнять <i>ALTER TABLE ... ADD COLUMN</i>.</p> <h4>Жёсткий перезапуск терминала</h4> <p>Оставался открытым вопрос, как поведёт себя сервис при перезапуске терминала менеджером. Функция <i>stop_mt5()</i> останавливает терминал через <i>psutil.Process.terminate()</i>, а в Windows это вызов <i>TerminateProcess()</i>, то есть жёсткое завершение процесса, а не обычное закрытие окна. В документации по автозагрузке сервисов не сказано, нужно ли для неё штатное закрытие, поэтому мы проверили это вручную.</p> <p>Мы завершили процесс терминала командой <i>Stop-Process -Force</i>, что по сути то же самое, что делает <i>terminate()</i>, и запустили <i>terminal64.exe /portable</i> заново, как это делают <i>stop_mt5()</i> и <i>start_mt5()</i> в связке. Сервис стартовал сам, примерно через две секунды после запуска терминала и без обращения к Навигатору, а значение <i>states.updated_at</i> снова начало расти. Таким образом, жёсткое завершение автозагрузке сервиса не мешает.</p> <h4>Автозапуск сервиса</h4> <p>Остаётся вопрос, как добавить сервис в новый терминал, не создавая его вручную через Навигатор. Мы надеялись сделать это через конфигурационный файл запуска, как это делается для советников. Однако, согласно <a href="https://www.metatrader5.com/ru/terminal/help/start_advanced/start" title="https://www.metatrader5.com/ru/terminal/help/start_advanced/start" target="_blank">справке по запуску платформы</a>, секция <i>[StartUp]</i> описывает только советники и скрипты, а про сервисы в ней ничего не сказано, и другого задокументированного способа нам найти не удалось.</p> <p>Можно было бы заменить сервис скриптом, запускаемым через <i>/config</i> и <i>[StartUp] Script=...</i>, но это вернуло бы тот
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">try</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;con = sqlite3.connect(self.database, timeout=<span class="number">3</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;con.row_factory = sqlite3.Row
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">try</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span> con.execute(self.STATE_QUERY, (folder,)).fetchone()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">finally</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;con.close()
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">except</span> sqlite3.OperationalError <span class="keyword">as</span> e:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment"># База ещё не создана, занята или не совпадает схема - идём дальше</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment"># без данных, но причину печатаем, чтобы она не оставалась скрытой</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="built_in">print</span>(<span class="string">f'read_state(<span class="subst">{folder!r}</span>): <span class="subst">{e}</span>'</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span> <span class="literal">None</span></pre><button type="button" class="copy-code-btn c-b8amxo" title="Скопировать в буфер обмена"><svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4.5H9
C9.39782 4.5 9.77936 4.65804 10.0607 4.93934
C10.342 5.22064 10.5 5.60218 10.5 6V14
C10.5 14.3978 10.342 14.7794 10.0607 15.0607
C9.77936 15.342 9.39782 15.5 9 15.5H4
C3.60218 15.5 3.22064 15.342 2.93934 15.0607
C2.65804 14.7794 2.5 14.3978 2.5 14V6
C2.5 5.60218 2.65804 5.22064 2.93934 4.93934
C3.22064 4.65804 3.60218 4.5 4 4.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.5H11L14.5 5V11
C14.5 11.3978 14.342 11.7794 14.0607 12.0607
C13.7794 12.342 13.3978 12.5 13 12.5H7
C6.60218 12.5 6.22064 12.342 5.93934 12.0607
C5.65804 11.7794 5.5 11.3978 5.5 11V3
C5.5 2.60218 5.65804 2.22064 5.93934 1.93934
C6.22064 1.65804 6.60218 1.5 7 1.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M11 1.5V3.5C11 3.89782 11.158 4.27936 11.4393 4.56066C11.7206 4.84196 12.1022 5 12.5 5H14.5L11 1.5Z" fill="#fff" stroke="#A1A1A1" stroke-linejoin="bevel"></path></svg><!----></button></div> <p>Информация о том, жив ли сам процесс терминала, по-прежнему приходит от <i>psutil</i>. Сочетание двух источников даёт три статуса вместо прежних двух:</p> <table class="standart" width="100%" cellspacing="0" cellpadding="3"> <thead> <tr> <th>Процесс (<i>psutil</i>)</th> <th>Свежесть <i>updated_at</i></th> <th>Статус</th> </tr> </thead> <tbody> <tr> <td data-label="Процесс (psutil)">жив</td> <td data-label="Свежесть updated_at">свежее порога</td> <td data-label="Статус"><i>running</i></td> </tr> <tr> <td data-label="Процесс (psutil)">жив</td> <td data-label="Свежесть updated_at">старше порога или записи ещё нет</td> <td data-label="Статус"><i>starting</i></td> </tr> <tr> <td data-label="Процесс (psutil)">процесса нет</td> <td data-label="Свежесть updated_at">—</td> <td data-label="Статус"><i>stopped</i></td> </tr> </tbody> </table> <p>Средний случай охватывает сразу несколько ситуаций: терминал ещё загружается, сервис в нём не запущен или завис. Порог свежести мы выбрали равным 30 секундам при интервале записи 2–3 секунды, чтобы оставался заметный запас на случай редких пропусков, когда база занята другим сервисом:</p> <div class="code-box"><pre class="code" data-enhanced="true">FRESHNESS_THRESHOLD_SEC = <span class="number">30</span></pre><button type="button" class="copy-code-btn c-b8amxo" title="Скопировать в буфер обмена"><svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4.5H9
C9.39782 4.5 9.77936 4.65804 10.0607 4.93934
C10.342 5.22064 10.5 5.60218 10.5 6V14
C10.5 14.3978 10.342 14.7794 10.0607 15.0607
C9.77936 15.342 9.39782 15.5 9 15.5H4
C3.60218 15.5 3.22064 15.342 2.93934 15.0607
C2.65804 14.7794 2.5 14.3978 2.5 14V6
C2.5 5.60218 2.65804 5.22064 2.93934 4.93934
C3.22064 4.65804 3.60218 4.5 4 4.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.5H11L14.5 5V11
C14.5 11.3978 14.342 11.7794 14.0607 12.0607
C13.7794 12.342 13.3978 12.5 13 12.5H7
C6.60218 12.5 6.22064 12.342 5.93934 12.0607
C5.65804 11.7794 5.5 11.3978 5.5 11V3
C5.5 2.60218 5.65804 2.22064 5.93934 1.93934
C6.22064 1.65804 6.60218 1.5 7 1.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M11 1.5V3.5C11 3.89782 11.158 4.27936 11.4393 4.56066C11.7206 4.84196 12.1022 5 12.5 5H14.5L11 1.5Z" fill="#fff" stroke="#A1A1A1" stroke-linejoin="bevel"></path></svg><!----></button></div> <p>Отдельно стоит учитывать ситуацию, когда терминал работает, а связи с сервером брокера нет: цифры счёта в базе в этом случае последние известные, а не актуальные. Для этого сервис выставляет код <i>-2</i> («Service: No connection to trade server»), и веб-интерфейс уже сейчас показывает его красным значком с восклицательным знаком.</p> <h3 id="compatibility">Совместимость с уже написанным интерфейсом</h3> <p>Структура ответа на запрос <i>POST /instances/{name}</i> (словарь с ключами <i>terminal</i>, <i>account</i>, <i>last_update</i>, <i>last_error</i> и <i>status</i>) осталась прежней, просто теперь она собирается из данных базы, а не из ответа модуля <i>MetaTrader5</i>. Поэтому <i>templates/index.html</i> и <i>static/script.js</i>, написанные ещё в третьей части, почти не потребовали изменений: механизм получения данных сменился полностью, а интерфейс этого даже не заметил.</p> <p>Единственное исключение — формат времени. Раньше в <i>last_update</i> попадала строка <i>str(datetime.now())</i>, а JavaScript разбирал её через <i>new Date()</i>. Формат вида <i>2026-07-25 12:34:56.789012</i>, без буквы <i>T</i>, браузер Chrome понимает, но спецификация этого не гарантирует. Теперь наружу отдаётся то же целое число секунд Unix, которое лежит в <i>updated_at</i>. Миллисекундная точность нам здесь не нужна, а лишнее преобразование только создавало бы повод для путаницы с единицами измерения в Python и JavaScript. На стороне клиента функция <i>formatDateTime()</i> просто умножает полученное значение на 1000, прежде чем передать его в <i>new Date()</i>.</p> <h3 id="deploy">Развёртывание через config/services.ini</h3> <p>Сервис компилируется и раскладывается в папку <i>MQL5/Services</i> каждого терминала, а дальше, согласно документации, экземпляр сервиса нужно создать через Навигатор — вручную, по одному разу в каждом терминале. Для четырёх терминалов из наших примеров это не проблема, но масштабировать такое развёртывание дальше не хочется.</p> <p>Во время экспериментов нашёлся способ обойтись без этого шага. Если положить в папку <i>config</i> терминала файл <i>services.ini</i>, а исполняемый файл сервиса к моменту запуска терминала уже будет лежать по пути, указанному в этом файле, то сервис запустится сам, без единого обращения к Навигатору:</p> <div class="code-box"><pre class="code" data-enhanced="true">&lt;service&gt;
name=ServiceStateWriter
path=Services\Shared Projects\mt5-manager\ServiceStateWriter.ex5
expertmode=0
enabled=1
&lt;inputs&gt;
InpDatabase=mt5-manager.sqlite
InpIntervalSec=2
InpJitterMs=1000
InpBusyTimeout=3000
&lt;/inputs&gt;
&lt;/service&gt;</pre><button type="button" class="copy-code-btn c-b8amxo" title="Скопировать в буфер обмена"><svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4.5H9
C9.39782 4.5 9.77936 4.65804 10.0607 4.93934
C10.342 5.22064 10.5 5.60218 10.5 6V14
C10.5 14.3978 10.342 14.7794 10.0607 15.0607
C9.77936 15.342 9.39782 15.5 9 15.5H4
C3.60218 15.5 3.22064 15.342 2.93934 15.0607
C2.65804 14.7794 2.5 14.3978 2.5 14V6
C2.5 5.60218 2.65804 5.22064 2.93934 4.93934
C3.22064 4.65804 3.60218 4.5 4 4.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.5H11L14.5 5V11
C14.5 11.3978 14.342 11.7794 14.0607 12.0607
C13.7794 12.342 13.3978 12.5 13 12.5H7
C6.60218 12.5 6.22064 12.342 5.93934 12.0607
C5.65804 11.7794 5.5 11.3978 5.5 11V3
C5.5 2.60218 5.65804 2.22064 5.93934 1.93934
C6.22064 1.65804 6.60218 1.5 7 1.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M11 1.5V3.5C11 3.89782 11.158 4.27936 11.4393 4.56066C11.7206 4.84196 12.1022 5 12.5 5H14.5L11 1.5Z" fill="#fff" stroke="#A1A1A1" stroke-linejoin="bevel"></path></svg><!----></button></div> <p>Стоит учитывать, что это не задокументированное поведение платформы. На странице <a href="https://www.metatrader5.com/en/terminal/help/start_advanced/structure" title="https://www.metatrader5.com/en/terminal/help/start_advanced/structure" target="_blank">«Файлы и папки»</a> официальной справки содержимое папки <i>config</i> перечислено явно: <i>accounts.dat</i>, <i>common.ini</i>, <i>metaeditor.ini</i>, <i>terminal.ini</i>, <i>servers.dat</i> и сертификаты, а файла <i>services.ini</i> среди них нет. Секция <i>[StartUp]</i>, о которой шла речь выше, о нём тоже ничего не говорит, и упоминаний нам не удалось найти ни в учебнике, ни в статьях о сервисах.</p> <p>О происхождении файла можно судить лишь косвенно. Он закодирован в UTF-16LE (с BOM <i>FF FE</i>), в отличие от обычного текстового <i>common.ini</i>, и такая кодировка типична для служебных файлов, которые терминал пишет и читает сам. Скорее всего, <i>services.ini</i> — это то, как терминал сохраняет сервисы, добавленные через Навигатор командой «Добавить сервис»: набор полей <i>name</i>, <i>path</i>, <i>expertmode</i>, <i>enabled</i> и блок <i>&lt;inputs&gt;</i> с входными параметрами соответствует тому, что вводится в диалоге добавления. Заготовив такой файл заранее, мы фактически сообщаем терминалу, что сервис уже был добавлен вручную, а раз при запуске терминал загружает сервисы, которые работали на момент его остановки, он запускает и этот.</p> <p>Практически это делает развёртывание нового терминала воспроизводимым: достаточно скопировать скомпилированный <i>ServiceStateWriter.ex5</i> по нужному пути и положить в <i>config</i> заранее подготовленный <i>services.ini</i>. Но поскольку MetaQuotes нигде не описывает этот формат, к находке стоит относиться как к рабочему, но неофициальному приёму: сегодня он работает, а гарантий, что он переживёт будущие сборки терминала, у нас нет.</p> <h4>Скрипт развёртывания</h4> <p>Развёртывание по ходу работы пришлось повторять не раз: поправили схему — нужно пересобрать сервис и разложить его по терминалам, сменили параметры — переложить <i>services.ini</i>. Вручную это быстро становится источником ошибок (именно забытый устаревший <i>.ex5</i> привёл к истории со схемой, о которой шла речь выше), поэтому мы собрали все шаги в скрипт <i>deploy_service.py</i>. Он запускается так же, как <i>run.py</i>, и берёт список терминалов и<>
C9.39782 4.5 9.77936 4.65804 10.0607 4.93934
C10.342 5.22064 10.5 5.60218 10.5 6V14
C10.5 14.3978 10.342 14.7794 10.0607 15.0607
C9.77936 15.342 9.39782 15.5 9 15.5H4
C3.60218 15.5 3.22064 15.342 2.93934 15.0607
C2.65804 14.7794 2.5 14.3978 2.5 14V6
C2.5 5.60218 2.65804 5.22064 2.93934 4.93934
C3.22064 4.65804 3.60218 4.5 4 4.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.5H11L14.5 5V11
C14.5 11.3978 14.342 11.7794 14.0607 12.0607
C13.7794 12.342 13.3978 12.5 13 12.5H7
C6.60218 12.5 6.22064 12.342 5.93934 12.0607
C5.65804 11.7794 5.5 11.3978 5.5 11V3
C5.5 2.60218 5.65804 2.22064 5.93934 1.93934
C6.22064 1.65804 6.60218 1.5 7 1.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M11 1.5V3.5C11 3.89782 11.158 4.27936 11.4393 4.56066C11.7206 4.84196 12.1022 5 12.5 5H14.5L11 1.5Z" fill="#fff" stroke="#A1A1A1" stroke-linejoin="bevel"></path></svg><!----></button></div> <p>Результат при этом попадает не рядом с исходником, а в зеркальную папку <i>MQL5\Services\Shared Projects\mt5-manager\ServiceStateWriter.ex5</i>, то есть туда, где его ищет Навигатор и откуда его будет брать <i>services.ini</i>. Однако на практике такой вызов оказался ненадёжным: после успешного первого раза те же команды стали завершаться с кодом 0, не создавая ни лога, ни нового файла. Судя по всему, запуску MetaEditor без участия пользователя не хватает интерактивной сессии рабочего стола, но это лишь предположение, так как документация эту ситуацию не описывает. Важнее причины вывод: код возврата MetaEditor не доказывает, что компиляция выполнена.</p> <p>Поэтому скрипт ведёт себя осторожно. Он компилирует сервис один раз, в терминале, где лежит рабочая копия исходника, и проверяет успех не по коду возврата, а по времени изменения <i>.ex5</i>. Если файл не обновился, скрипт сообщает об этом и просит открыть <i>ServiceStateWriter.mq5</i> в MetaEditor и нажать F7, а при повторном запуске видит, что <i>.ex5</i> уже новее исходника, пропускает компиляцию и продолжает с раскладки готового файла. Готовый <i>.ex5</i> копируется на остальные терминалы обычным копированием файла, и этот шаг ни разу нас не подвёл.</p> <p>Здесь есть допущение: один и тот же <i>.ex5</i> используется на всех терминалах, а сами терминалы бывают разных сборок (в нашем наборе это 5800, 6063 и 6292). Проблем с этим не возникло: сервис использует только обычные функции <i>AccountInfo*</i>, <i>TerminalInfo*</i> и <i>Database*</i>, и скомпилированный файл одинаково работает на всех трёх сборках.</p> <p>Наконец, канонический <i>services.ini</i> из репозитория копируется в папку <i>config</i> каждого терминала. Этот шаг не зависит от результата компиляции и выполняется всегда.</p> <p>После этого скрипт проверяет, какие терминалы сейчас запущены, и выводит их список с напоминанием о перезапуске: сервис, запущенный на момент замены файла, продолжает работать со старым кодом из памяти, поэтому новый <i>.ex5</i> вступит в силу только после перезапуска терминала или самого сервиса. Файлы самого менеджера скрипт не затрагивает, так что перезапускать <i>run.py</i> для обновления сервиса не нужно.</p> <h3 id="testing">Тестирование</h3> <p>Теперь проверим
FROM states s JOIN terminals t ON t.id = s.terminal_id;</pre><button type="button" class="copy-code-btn c-b8amxo" title="Скопировать в буфер обмена"><svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4.5H9
C9.39782 4.5 9.77936 4.65804 10.0607 4.93934
C10.342 5.22064 10.5 5.60218 10.5 6V14
C10.5 14.3978 10.342 14.7794 10.0607 15.0607
C9.77936 15.342 9.39782 15.5 9 15.5H4
C3.60218 15.5 3.22064 15.342 2.93934 15.0607
C2.65804 14.7794 2.5 14.3978 2.5 14V6
C2.5 5.60218 2.65804 5.22064 2.93934 4.93934
C3.22064 4.65804 3.60218 4.5 4 4.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.5H11L14.5 5V11
C14.5 11.3978 14.342 11.7794 14.0607 12.0607
C13.7794 12.342 13.3978 12.5 13 12.5H7
C6.60218 12.5 6.22064 12.342 5.93934 12.0607
C5.65804 11.7794 5.5 11.3978 5.5 11V3
C5.5 2.60218 5.65804 2.22064 5.93934 1.93934
C6.22064 1.65804 6.60218 1.5 7 1.5Z" fill="#fff" stroke="#A1A1A1"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M11 1.5V3.5C11 3.89782 11.158 4.27936 11.4393 4.56066C11.7206 4.84196 12.1022 5 12.5 5H14.5L11 1.5Z" fill="#fff" stroke="#A1A1A1" stroke-linejoin="bevel"></path></svg><!----></button></div> <p>Значение <i>updated_at</i> у работающих терминалов должно постоянно увеличиваться с интервалом в несколько секунд.</p> <h3 id="conclusion">Заключение</h3> <p>В этой части мы заменили опрос терминалов из Python на сервис, который работает внутри каждого терминала и сам записывает его состояние в общую базу SQLite. Веб-приложение теперь просто читает последние известные данные, а о том, жив ли процесс терминала, по-прежнему узнаёт через <i>psutil</i>. Благодаря этому из схемы ушли последовательные вызовы <i>mt5.initialize()</i> и <i>mt5.shutdown()</i> для каждого терминала, а интерфейс, написанный в предыдущей части, продолжил работать почти без изменений. Заодно мы познакомились с сервисом как с типом программ MQL5, нашли способ автоматически запускать его в новом терминале и написали скрипт для развёртывания.</p> <p>Для следующих частей у нас накопилось довольно много задач. Ближайшая из них — запись истории показателей в отдельную таблицу с более редким интервалом, вместе с журналом операций и построчным списком открытых позиций: сейчас в базе хранится только их количество. С появлением истории понадобится версионирование схемы базы, ведь удалить файл с накопленными данными уже будет нельзя. Кроме того, хотелось бы довести до конца автоматизацию развёртывания (установку нового терминала и компиляцию без участия MetaEditor), добавить редактирование конфигурации через браузер, авторизацию и HTTPS, а позже перейти к работе с несколькими серверами, как мы намечали в первой части, и к торговым операциям из браузера и управлению советниками.</p> <p>Спасибо за внимание и до новых встреч!</p> <div class="atten"> <p><b>Предупреждение</b>. Стоит подчеркнуть важность осторожности при использовании проекта. На данном этапе он находится в начальной стадии реализации, поэтому в нём никак не учитываются возможные угрозы утечки чувствительной информации при работе разрабатываемого веб-приложения в общедоступном режиме. В целях безопасности приложение должно запускаться только в защищённой или локальной среде. В текущем виде оно не предусматривает шифрования трафика, аутентификации пользователей или защиты от неса