English
preview
Создание типобезопасной шины событий в MQL5: Снижение связности компонентов советника без глобальных переменных

Создание типобезопасной шины событий в MQL5: Снижение связности компонентов советника без глобальных переменных

MetaTrader 5 — Эксперты |
17 1
Ushana Kevin Iorkumbul
Ushana Kevin Iorkumbul

Введение

Когда советник выходит за рамки одного блока торговой логики, разработчики часто прибегают к самому доступному механизму координации: глобальным переменным. Менеджеру рисков необходимо знать текущую просадку, поэтому на уровне файла появляется переменная типа double с именем g_current_drawdown. Генератор сигналов выдает сигнал, и менеджер ордеров получает уведомление через флаг g_signal_active типа bool. За несколько недель в советнике средней сложности накапливаются десятки таких "мостов" для передачи состояния, каждый из которых представляет собой прямую, невидимую зависимость между компонентами, изначально задуманными как независимые.

Последствия такого подхода носят не только структурный характер. Он приводит к взаимозависимости компонентов на уровне поведения. Когда менеджер рисков считывает значение g_current_drawdown, он исходит из того, что нужный модуль записал его в нужное время в рамках нужного цикла обработки тика. Если модуль исполнения будет переработан так, чтобы обновлять это значение асинхронно, или если другой компонент также начнет записывать в эту переменную, поведение менеджера рисков незаметно изменится. На уровне языка соблюдение контракта не обеспечивается. Глобальная переменная — это канал доступа к общему изменяемому состоянию без контроля доступа, без гарантий типизации сверх тех, что дает примитивный тип, и без сведений о том, кто и когда сформировал значение.

Рассмотрим реалистичную архитектуру, состоящую из трёх компонентов: генератора сигналов, менеджера ордеров и монитора просадки. В упрощённой архитектуре на основе глобальных переменных связи между ними выглядят так:

Рисунок 1: Взаимозависимость компонентов в архитектуре трёхкомпонентного советника

Рисунок 1: Взаимозависимость компонентов в архитектуре трёхкомпонентного советника, в котором независимые модули непреднамеренно зависят друг от друга из-за неконтролируемых операций чтения и записи в централизованном пуле глобальных переменных.

Каждый компонент читает данные из общего пула и записывает их в него. Добавление четвёртого компонента требует проверки всех существующих глобальных переменных, чтобы понять, какие из них он должен учитывать, а значения каких может нарушить. Перед удалением компонента необходимо вручную отследить все ссылки на него. Модульное тестирование отдельного компонента в изоляции практически невозможно, поскольку его поведение зависит от правильной предварительной настройки глобального состояния.

Шаблон публикации и подписки, реализованный здесь в виде типобезопасной шины событий, полностью устраняет этот канал взаимодействия через общее состояние. Компоненты не читают переменные друг друга. Они публикуют события, описывающие произошедшее, и подписываются на те события, на которые им необходимо реагировать. Шина выступает посредником при любом взаимодействии. Ни один компонент не содержит указателя на какой-либо другой компонент. Граф зависимостей преобразуется из сети связей "N к N" в топологию "звезда" с шиной в центре.

Рисунок 2: Архитектура с низкой связностью и топологией 'звезда', в которой компоненты советника взаимодействуют исключительно через каналы публикации и подписки при посредничестве центральной шины событий.

Рисунок 2: Архитектура с низкой связностью и топологией 'звезда', в которой компоненты советника взаимодействуют исключительно через каналы публикации и подписки при посредничестве центральной шины событий.


Обзор архитектуры

Реализация состоит из четырёх основных элементов: абстрактного интерфейса слушателя (IEventListener), иерархии типизированных структур событий, самой шины (CEventBus) и структуры-обертки слота подписки (SListenerSlot). Эта структура-обертка позволяет обойти ограничение компилятора MQL5, связанное с многомерными массивами указателей. Пятый элемент — перечисление типов событий — задаёт классификацию, проверяемую на этапе компиляции, и тем самым обеспечивает типобезопасную маршрутизацию.

Архитектура намеренно не предусматривает зависимостей от тяжеловесных фреймворков. MQL5 предоставляет шаблоны, интерфейсы (через абстрактные классы), динамические массивы и средства управления указателями. Этих базовых элементов достаточно для создания корректной и эффективной шины без использования внешних библиотек.

Классификация типов событий

Все события в системе идентифицируются значением из единого перечисления. Это перечисление служит ключом маршрутизации, по которому шина сопоставляет публикуемые события с зарегистрированными подписчиками.

enum ENUM_EA_EVENT
{
   EA_EVENT_SIGNAL_LONG    = 0,  // Signal: Market Long Condition Detected
   EA_EVENT_SIGNAL_SHORT   = 1,  // Signal: Market Short Condition Detected
   EA_EVENT_SIGNAL_FLAT    = 2,  // Signal: No Active Directional Bias
   EA_EVENT_ORDER_FILL     = 3,  // Execution: Order Fill Confirmed
   EA_EVENT_ORDER_REJECT   = 4,  // Execution: Order Rejected by Broker
   EA_EVENT_DRAWDOWN_WARN  = 5,  // Risk: Drawdown Warning Threshold Crossed
   EA_EVENT_DRAWDOWN_HALT  = 6,  // Risk: Hard Drawdown Limit Reached — Trading Halted
   EA_EVENT_SESSION_OPEN   = 7,  // Session: Market Session Begin
   EA_EVENT_SESSION_CLOSE  = 8   // Session: Market Session End
};

Использование перечисления вместо строковых идентификаторов дает два конкретных преимущества. Во-первых, компилятор проверяет допустимость типов событий в месте вызова, исключая публикацию событий или подписку на события с ошибкой в названии или с неопределённым типом. Во-вторых, целочисленные значения перечислений позволяют напрямую обращаться к элементам массива по индексу, заменяя цикл сравнения строк при поиске подписчиков доступом к массиву за O(1). Это даёт существенную разницу, когда шина обрабатывает сотни событий в секунду в периоды высокой волатильности.

Структура полезной нагрузки события

Каждое событие содержит полезную нагрузку в виде стандартизированной структуры. Цель проектирования в данном случае — создать запись фиксированного размера, выделяемую в стеке, которую можно передавать по константной ссылке по всей цепочке диспетчеризации без выделения памяти в куче.

struct SEventPayload
{
   ENUM_EA_EVENT  event_type;     // Routing key identifying the event class
   datetime       timestamp;      // Server time at point of publication
   long           order_ticket;   // Ticket number for fill/reject events (0 if unused)
   double         value_primary;  // Primary numeric payload (price, drawdown %, PnL)
   double         value_secondary;// Secondary numeric payload (SL level, TP level, lot size)
   string         message;        // Human-readable diagnostic string for logging
};

Поле "message" является единственным элементом структуры, выделенным в куче, поскольку строки MQL5 представляют собой объекты кучи с подсчётом ссылок. При высокочастотной диспетчеризации, когда выделение памяти для поля message создаёт заметные накладные расходы, это поле можно оставить равным пустому строковому литералу, не влияя на маршрутизацию. Числовые поля охватывают большинство вариантов полезной нагрузки финансовых событий: в событии исполнения поле order_ticket используется для идентификатора позиции, value_primary — для цены исполнения, а value_secondary — для исполненного объёма. В событии просадки поле value_primary содержит текущую просадку в процентах, а поле тикета остаётся равным нулю.

В таблице ниже для каждого типа события указано, как предполагается использовать поля полезной нагрузки:

Тип событияorder_ticketvalue_primaryvalue_secondarymessage
EA_EVENT_SIGNAL_LONG0Ориентировочная цена входаСмещение SL, полученное на основе ATRИдентификатор источника сигнала
EA_EVENT_SIGNAL_SHORT0Ориентировочная цена входаСмещение SL, полученное на основе ATRИдентификатор источника сигнала
EA_EVENT_SIGNAL_FLAT00,00,0Причина отсутствия выраженной направленности
EA_EVENT_ORDER_FILLТикет позицииЦена исполненияИсполненный объем (лоты)Название символа
EA_EVENT_ORDER_REJECTТикет заявки, которую пытались исполнитьЗапрашиваемая ценаЗапрашиваемый объемОписание ошибки брокера
EA_EVENT_DRAWDOWN_WARN0Текущая просадка, %Пороговое значение предупреждения, %Средства на счете во время сессии
EA_EVENT_DRAWDOWN_HALT0Текущая просадка, %Жесткий предел, %Причина приостановки торговли
EA_EVENT_SESSION_OPEN00,00,0Идентификатор названия сессии
EA_EVENT_SESSION_CLOSE00,00,0Идентификатор названия сессии

Интерфейс слушателя

MQL5 не поддерживает типы интерфейсов в том смысле, в каком они понимаются в Java или C#, однако абстрактные классы с чисто виртуальными методами выполняют ту же структурную роль. Любой компонент, которому требуется получать события от шины, должен наследоваться от IEventListener и реализовывать его единственный метод диспетчеризации.

//--- IEventListener
class IEventListener
{
public:
   virtual void OnEvent(const SEventPayload &payload) = 0;
   virtual     ~IEventListener() {}
};

Метод OnEvent принимает полезную нагрузку события по константной ссылке. Это не стилистический выбор; это условие, обеспечивающее производительность и безопасность. Передача структуры SEventPayload размером 80 байт по значению привела бы к ее копированию в кадр стека каждого слушателя при каждом вызове диспетчеризации. При десяти зарегистрированных слушателях и ста событиях в секунду это составляет тысячу ненужных копирований структуры в секунду. Квалификатор const гарантирует, что ни одна реализация обработчика не сможет изменить общие данные события. Это предотвращает ситуацию, когда один обработчик искажает данные, которые получат последующие обработчики в рамках того же цикла рассылки события.

Чисто виртуальный деструктор гарантирует, что шина может безопасно удалять объекты слушателей через указатели на базовый класс при очистке, не вызывая неопределенного поведения, связанного с использованием невиртуального деструктора в полиморфной иерархии.

Класс CEventBus

Шина является единственным посредником между издателями и подписчиками. Ее внутренняя модель данных представляет собой массив фиксированной длины из структур-оберток SListenerSlot, индексируемый непосредственно значением перечисления типов событий. MQL5 не поддерживает двумерные динамические массивы указателей на объекты, поэтому требуется промежуточная структура. Каждый SListenerSlot хранит динамический массив IEventListener* и счетчик активных подписчиков. В шине объявлен SListenerSlot m_slots[EA_EVENT_COUNT = 9] — массив фиксированной длины из обычных структур. Такое объявление полностью допустимо в MQL5. Такая структура обеспечивает поиск подписчиков по типу события за время O(1). Для каждого слота подписки требуется выделить небольшой, заранее известный объем памяти.

Рисунок 3: Схема размещения CEventBus в памяти с массивом m_slots фиксированной длины из 9 элементов

Рисунок 3: Схема размещения CEventBus в памяти с массивом m_slots фиксированной длины из 9 элементов; показаны границы структур-оберток SListenerSlot, отделяющие внутренние счетчики от динамических массивов указателей, с индексацией по значению перечисления за время O(1).



Структура-обертка слота подписки

MQL5 не допускает использования зубчатых или двумерных динамических массивов указателей на объекты. Объявление вида IEventListener *m_subscribers[EA_EVENT_COUNT][] имеет недопустимый для компилятора MQL5 синтаксис и приводит к ошибкам доступа к массиву во всех местах, где предпринимается попытка изменить размер внутреннего измерения или обратиться к его элементам по индексу. Структура SListenerSlot решает эту проблему, оборачивая динамический массив для каждого события в именованный агрегат, с которым компилятор может правильно работать:

struct SListenerSlot
{
   IEventListener *listeners[];  // Dynamic array of subscriber pointers for one event type
   int             count;        // Active subscriber count in this slot

   SListenerSlot() : count(0) { ArrayResize(listeners, 0); }
};

Затем шина объявляет SListenerSlot m_slots[EA_EVENT_COUNT] — массив фиксированной длины, состоящий из простых структур. Все вызовы ArrayResize и операции с индексами обращаются к m_slots[slot].listeners и m_slots[slot].count, а не к двумерному массиву. Конструктор по умолчанию структуры SListenerSlot инициализирует счетчик нулем и задает нулевую емкость внутреннего массива, поэтому каждый слот находится в корректном пустом состоянии уже при создании шины, без явного цикла инициализации по всему диапазону перечисления.

Управление подписками

При регистрации подписчика сохраняется сырой указатель на объект слушателя. Шина не владеет этими объектами и не управляет их жизненным циклом. Компонент, создающий слушатель, должен гарантировать, что объект слушателя остается действительным в течение всего периода регистрации. Это осознанное проектное решение: инверсия владения (при которой шина владеет всеми слушателями) потребовала бы от шины управления временем жизни разнородных объектов, что противоречит типичной структуре советника, где компоненты размещаются в стеке как члены класса советника верхнего уровня.

Перед диспетчеризацией шина должна проверять корректность зарегистрированных указателей. Функция CheckPointer() в MQL5 возвращает POINTER_INVALID для недействительных указателей на объекты в куче, в том числе на удаленные, и POINTER_DYNAMIC для корректных указателей на объекты, выделенные в куче. Эта проверка выполняется при каждом цикле диспетчеризации, а не только при регистрации, поскольку слушатель может быть удален между регистрацией и следующим вызовом публикации.

Механика публикации

При вызове Publish() шина извлекает массив подписчиков для данного типа события, проходит по нему, проверяет каждый указатель и синхронно вызывает OnEvent(). По умолчанию диспетчеризация выполняется синхронно, то есть поток выполнения издателя блокируется до тех пор, пока все слушатели не завершат выполнение своих реализаций метода OnEvent(). Эта модель подходит для однопоточных советников MQL5, в которых все операции выполняются в потоке советника в рамках обработчика тика. Если слушатель выполняет ресурсоемкие операции (запись в базу данных, HTTP-запросы через WebRequest), затраты времени на них возникают синхронно при обработке тика, что необходимо учитывать при реализации слушателей.

Интерфейс класса намеренно сделан компактным:

МетодСигнатураДоступАрхитектурное назначение
Subscribebool Subscribe(ENUM_EA_EVENT type, IEventListener *listener)publicРегистрирует слушателя для определённого типа события
Unsubscribebool Unsubscribe(ENUM_EA_EVENT type, IEventListener *listener)publicУдаляет регистрацию конкретного слушателя по совпадению указателей
Publishvoid Publish(const SEventPayload &payload)publicРассылает событие всем подписчикам, зарегистрированным для его типа
SubscriberCountint SubscriberCount(ENUM_EA_EVENT type)publicВозвращает количество активных подписчиков для заданного типа события (для диагностики)
Clearvoid Clear()publicУдаляет все подписки, не удаляя объекты-слушатели
m_slotsSListenerSlot m_slots[EA_EVENT_COUNT]privateТаблица подписок, индексированная по типу события



Анализ накладных расходов и практические ограничения

Шина событий вносит измеримые накладные расходы по сравнению с прямыми вызовами функций. Понимание этих накладных расходов на архитектурном уровне необходимо, чтобы определить, когда этот шаблон уместен, а когда — нет.

Задержка диспетчеризации: каждый вызов Publish() выполняет одно обращение к элементу массива по индексу, проверку границ, цикл по подписчикам, один вызов CheckPointer() и один вызов виртуальной функции для каждого подписчика. На современном оборудовании сама виртуальная диспетчеризация занимает примерно от 2 до 5 наносекунд на каждый вызов из-за косвенного обращения к таблице виртуальных функций (vtable). Для советника с пятью подписчиками на каждый тип события и ста публикуемыми событиями за тик суммарные накладные расходы составляют менее одной микросекунды, что пренебрежимо мало по сравнению с сетевой задержкой при передаче ордеров. Для скальпинговых стратегий со сверхнизкой задержкой, в которых само время выполнения обработчика тика является показателем производительности, эти накладные расходы следует измерять отдельно с помощью тестов производительности.

Объём занимаемой памяти: таблица подписок выделяет массив указателей IEventListener* для каждого типа события. При наличии девяти типов событий и не более десяти подписчиков на каждое из них максимальный размер таблицы составляет 90 ячеек указателей, что занимает 720 байт на 64-битной платформе. Структура SEventPayload занимает примерно 80 байт в стеке во время диспетчеризации. Оба объёма памяти незначительны для длительно работающего советника.

Ограничения проверки указателей: проверка CheckPointer() предотвращает диспетчеризацию событий удалённым объектам, но не защищает от обращения к объектам, уничтоженным без освобождения памяти (то есть к локальным объектам, область видимости которых уже завершилась). Если слушатель — локальная переменная, размещённая в стеке функции, которая уже завершила работу, CheckPointer() не обнаружит, что указатель на него стал недействительным. Время жизни всех слушателей, зарегистрированных в шине, должно охватывать всё время работы шины. Более безопасный подход — создавать слушателей в куче и хранить их в главном классе советника. Удалите их в функции OnDeinit() после очистки шины.

Без очередей и потокобезопасности: в модели выполнения MQL5 каждый советник выполняется в одном потоке. Приведенная ниже реализация шины не включает в себя очередь сообщений, отложенную диспетчеризацию или какие-либо примитивы параллелизма. Если в будущей версии среды выполнения MQL5 будут реализованы многопоточные контексты выполнения, то шине потребуется мьютекс для доступа к m_subscribers и кольцевой буфер без блокировок для очереди публикации. Здесь такие меры не предусмотрены, поскольку текущая спецификация среды выполнения MQL5 их не требует.

Риск рекурсивной публикации: если реализация метода OnEvent() слушателя вызывает метод Publish() на той же шине, начинается цикл рекурсивной диспетчеризации. Шина не обнаруживает и не предотвращает это. Публикация события EA_EVENT_DRAWDOWN_WARN слушателем EA_EVENT_ORDER_FILL безопасна только в том случае, если ни один слушатель EA_EVENT_DRAWDOWN_WARN не публикует событие исполнения ордера. Зацикливание публикаций может привести к переполнению стека вызовов MQL5 и завершению работы советника. Снижение этого риска обеспечивается архитектурой, а не проверками во время выполнения: обработчики должны публиковать события только при изменениях внешнего состояния, а не в ответ на другие события шины.

Интеграция шины: советник из трех компонентов

В следующем примере архитектуры три независимых компонента советника взаимодействуют через шину без каких-либо прямых взаимных ссылок между ними:

  • CSignalEngine отслеживает динамику цен и публикует события EA_EVENT_SIGNAL_LONG, EA_EVENT_SIGNAL_SHORT и EA_EVENT_SIGNAL_FLAT. Он не хранит указателей на менеджер ордеров или монитор рисков.
  • COrderManager подписывается на события сигналов и публикует события EA_EVENT_ORDER_FILL и EA_EVENT_ORDER_REJECT после обмена данными с брокером. Он не хранит указателей на генератор сигналов или монитор рисков.
  • CDrawdownMonitor подписывается на события исполнения для отслеживания экспозиции по позициям и публикует события EA_EVENT_DRAWDOWN_WARN и EA_EVENT_DRAWDOWN_HALT при превышении пороговых значений риска. Он не хранит указателей ни на генератор сигналов, ни на менеджер ордеров.

Функция OnInit() верхнего уровня советника создает все три компонента, создает шину и настраивает подписки. Функция OnTick() запускает генератор сигналов, который внутри себя вызывает метод Publish(), используя хранящуюся у него ссылку на шину. Вся цепочка от тика до исполнения проходит через шину без прямой связности между модулями.

Рисунок 4: Последовательность обработки одного рыночного тика в событийно-ориентированной архитектуре, показывающая, как сигналы и события исполнения маршрутизируются через центральную шину без прямой связности между компонентами.

Рисунок 4: Последовательность обработки одного рыночного тика в событийно-ориентированной архитектуре, показывающая, как сигналы и события исполнения маршрутизируются через центральную шину без прямой связности между компонентами.



Заключение

Архитектура CEventBus заменяет неявную, хрупкую сеть глобальных переменных явным контрактом взаимодействия, соблюдение которого контролируется компилятором. При регистрации компоненты объявляют, какие события они публикуют и какие получают, благодаря чему структура зависимостей сложного советника становится наглядной, доступной для проверки и рефакторинга без межмодульных побочных эффектов.

Основные компромиссы, связанные с этим подходом, четко определены. Синхронная диспетчеризация обеспечивает детерминированный порядок выполнения в пределах одного тика, однако это означает, что производительность слушателя напрямую влияет на задержку обработчика тика. Управление подписками на основе указателей требует от вызывающей стороны строгого контроля времени жизни объектов; шина намеренно не владеет своими подписчиками. Поиск в таблице подписок имеет сложность O(1) и хорошо масштабируется при увеличении числа подписчиков на каждый тип события, однако фиксированная таблица, индексируемая значениями перечисления, ограничивает набор типов событий перечислением, заданным на этапе компиляции, поэтому регистрация типов событий во время выполнения невозможна без изменения архитектуры.

Для большинства архитектур советников институционального уровня эти условия задают инженерные рамки, а не ограничивают возможности. Этот шаблон обеспечивает снижение связности, необходимое для независимой разработки, тестирования и развития больших наборов компонентов, а вносимые им накладные расходы значительно ниже уровня шума при любом измерении задержки, включающем время сетевого обмена с брокером в обоих направлениях.


Программы, использованные в статье

#НазваниеТипОписание
1EventBusSystem.mqhПодключаемый файлЕдиный подключаемый файл, объединяющий все шесть исходных модулей в порядке зависимостей: классификацию событий ENUM_EA_EVENT, структуру SEventPayload, абстрактный базовый класс IEventListener, механизм маршрутизации CEventBus, CSignalEngine, COrderManager и CDrawdownMonitor
2EventBusDemo.mq5Демонстрационный советникДемонстрационный советник, подключающий файл "EventBusSystem.mqh". Связывает все три компонента через CEventBus и обрабатывает события OnInit, OnDeinit, OnTick и OnTradeTransaction.

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22930

Прикрепленные файлы |
EventBusSystem.mqh (25.46 KB)
EventBusDemo.mq5 (7.77 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Stanislav Korotky
Stanislav Korotky | 15 июн. 2026 в 11:18
Мне нравятся эти схемы, но зачем тут упоминать о глобальных переменных? Глобальные переменные обычно используются для сохранения состояния или взаимодействия между множеством различных программ на MQL5 — а не внутри одного автотрейдера, одного индикатора или какого-либо другого отдельного объекта. Если речь идет об одном из последних случаев, то вы используете переменные/объекты MQL5 с встроенной типовой безопасностью, что очевидно и продемонстрировано в статье. Действительно, сложная торговая система может состоять из множества программ, что обеспечивает многопоточное выполнение и требует какого-либо механизма синхронизации — и глобальные переменные позволяют это реализовать.
Создание фреймворка для модульного тестирования на этапе компиляции в MQL5 с использованием утверждений препроцессора Создание фреймворка для модульного тестирования на этапе компиляции в MQL5 с использованием утверждений препроцессора
В MQL5 отсутствуют встроенные средства модульного тестирования, поэтому ошибки в служебных функциях расчёта размера лота, стоимости пункта и нормализации часто попадают в рабочий код. В этой статье представлен фреймворк без внешних зависимостей, построенный на макросах утверждений препроцессора, наборах тестов на основе интерфейсов и центральном средстве запуска тестов с модулем форматирования результатов. Он работает как скрипт в обработчике OnStart, выполняет детерминированные тесты и выводит сводку об успешных и неуспешных тестах на вкладку "Эксперты", позволяя выявить дефекты округления, обработки граничных значений и ошибок до развёртывания.
Нейросети в трейдинге: AlphaGen — поиск торговых факторов Нейросети в трейдинге: AlphaGen — поиск торговых факторов
Начинаем изучение AlphaGen — фреймворка автоматического поиска формульных факторов для прогнозирования рыночной доходности и формирования торговых сигналов. Разбираем, как генератор создаёт выражения из цен и объёмов, а совместная оценка факторов помогает отбирать полезные признаки для торговой модели. Рассматриваем адаптацию алгоритма к финансовым временным рядам. В практической части реализуем OpenCL-примитивы типизированного стека и маскирования токенов, обеспечивающие корректное построение формул с учётом ограничений доступной истории.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Учет тональности новостей для защиты торговой стратегии на MQL5 Учет тональности новостей для защиты торговой стратегии на MQL5
Представляем динамический фильтр тональности новостей для советников MQL5. Он использует WebRequest для отправки запросов к REST API, извлекает из JSON тональность и оценку уверенности и на их основе разрешает или блокирует входы в позицию и выходы из позиции с учетом сигналов скользящей средней и индекса относительной силы (RSI). Вы получите пример советника, инструкции по интеграции, обновления по таймеру, а также способы обхода ограничений тестера стратегий. В результате сокращается число сделок против преобладающего направления новостного потока, а при смене тональности риск снижается раньше.