English 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
preview
Разработка торгового советника с нуля (Часть 24): Обеспечиваем надежность системы (I)

Разработка торгового советника с нуля (Часть 24): Обеспечиваем надежность системы (I)

MetaTrader 5Трейдинг | 12 октября 2022, 14:21
1 204 0
Daniel Jose
Daniel Jose

1.0 - Введение

Вопреки мнению многих людей, некоторые моменты не такие простые. Система ордеров - один из таких моментов. Даже вы сами можете создать более скромную систему, которая будет отлично работать для ваших целей, мы тоже так сделали в статье "Разработка торгового советника с нуля", в которой была создана базовая система, которая может быть полезна для многих, но при этом недостаточна для других. Поэтому наступил момент, когда всё начало меняться, и родилась первая часть этой серии о новой системе ордеров. Это можно увидеть в статье "Разработка торгового советника с нуля (Часть 18)". Именно там мы начали разрабатывать систему, которая управлялась бы советником, но поддерживалась при этом MetaTrader 5. В той системе идея заключалась в том, чтобы не иметь ограничений по ордерам на графике. Поначалу система казалась довольно смелой, и я должен признать, что сам факт создания системы, в которой объекты будут обслуживаться не советником, а MetaTrader 5, казался мне довольно бессмысленным и неэффективным.

Однако, система находилась в разработке, и в статье "Разработка советника с нуля (Часть 23)" мы разработали призрачную систему для облегчения управления ордерами, позициями или лимитными точками (Take и Stop). Это было довольно интересно для разработки, но возникла одна проблема. Если мы посмотрим на количество используемых и видимых объектов по сравнению с количеством объектов, поддерживаемых MetaTrader 5, мы однозначно удивимся, потому что количество поддерживаемых объектов всегда будет выше.

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

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


2.0 - Планирование

Большая проблема здесь заключается в том, чтобы спланировать такую систему, которая проявляла бы два качества — скорость и надежность. В некоторых видах систем довольно сложно или даже невозможно добиться и того, и другого. Поэтому во многих случаях мы пытаемся сбалансировать ситуацию, но поскольку речь идет о деньгах, НАШИХ деньгах, то мы не хотим рисковать ими, приобретая систему, у которой не будет этих качеств. Нужно помнить, что мы имеем дело с системой, которая работает в РЕАЛЬНОЕ ВРЕМЯ, а это самый сложный сценарий, в который может попасть разработчик, поскольку мы всегда должны стараться иметь систему, которая крайне быстра: она мгновенно реагирует на события, в то время как проявляет достаточную надежность, чтобы не рухнуть при попытке улучшить ее. Таким образом, мы сами можем убедиться в том, что задача довольно сложная.

Скорость может быть достигнута за счет того, что функции вызываются и выполняются наиболее подходящим образом, избегая ненужных вызовов в еще более ненужное время. Благодаря этому мы получим систему настолько быструю, насколько это возможно в рамках языка. Однако, если мы хотим что-то еще быстрее, то нам придется спуститься на уровень машинного языка, и в этом случае, мы имеем в виду ASSEMBLY, но это часто не нужно, мы можем использовать язык C и получить не менее хорошие результаты.

Один из способов достижения нужной надежности — постараться как можно больше повторно использовать код (reuse), чтобы он постоянно тестировался в разных случаях. Но это только один путь. Другой путь — использование ООП (объектно-ориентированного программирования). Если это сделать правильно и должным образом с каждым объектным классом, не манипулируя данными классов объектов напрямую, за исключением случая наследования, то этого будет достаточно, чтобы иметь очень надежную систему. В некоторых моментах всё это снижает скорость выполнения, но данное снижение настолько мало, что его практически можно игнорировать в силу экспоненциального роста, который порождает инкапсуляцию, обеспечиваемую классом. Эта инкапсуляция дает нам необходимую надежность.

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

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

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

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

После этих наблюдений, можно перейти к делу.


3.0 - Реализация

3.0.1 - Новое моделирование показателей положения

Первое, что можно заметить в новом формате кода - это изменение функции, которая теперь стала макросом.

inline string MountName(ulong ticket, eIndicatorTrade it, eEventType ev, bool isGhost = false)
{
        return StringFormat("%s%c%c%c%llu%c%c%c%s", def_NameObjectsTrade, def_SeparatorInfo, (char)it, def_SeparatorInfo, ticket, def_SeparatorInfo, (char)(isGhost ? ev + 32 : ev), def_SeparatorInfo, (isGhost ? def_IndicatorGhost : def_IndicatorReal));
}

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

#define macroMountName(ticket, it, ev, Ghost) 								 \
		StringFormat("%s%c%llu%c%c%c%c%c%c%c", def_NameObjectsTrade, def_SeparatorInfo,          \                                                                                                                                                                                                                                                                                                
                                                       ticket, def_SeparatorInfo,                        \                                                                                                                                                                                                                                        
                                                       (char)it, def_SeparatorInfo,                      \ 
                                                       (char)(Ghost ? ev + 32 : ev), def_SeparatorInfo,  \ 
                                                       (Ghost ? def_IndicatorGhost : def_IndicatorReal))

Обратите внимание, что данные в старой версии макроса и данные в этой версии были изменены. Этому изменению есть причина, о которой мы ещё поговорим в данной статье.

Но из-за этой модификации мы также должны внести небольшое изменение в код другой функции.

inline bool GetIndicatorInfos(const string sparam, ulong &ticket, eIndicatorTrade &it, eEventType &ev)
                        {
                                string szRet[];
                                char szInfo[];
                                
                                if (StringSplit(sparam, def_SeparatorInfo, szRet) < 2) return false;
                                if (szRet[0] != def_NameObjectsTrade) return false;
                                ticket = (ulong) StringToInteger(szRet[1]);
                                StringToCharArray(szRet[2], szInfo);
                                it = (eIndicatorTrade)szInfo[0];
                                StringToCharArray(szRet[3], szInfo);
                                ev = (eEventType)szInfo[0];

                                return true;
                        }

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

Но, может быть, вы потом спросите: "Зачем эти изменения? Разве система не работала уже идеально?". Да, это так, но есть вещи, которые мы не можем контролировать, например, простое улучшение разработчиками платформы MetaTrader 5 некоторых функций, которые не используются в советнике и которые не принесут пользы нашему советнику. Правило заключается в том, чтобы не пытаться изобрести колесо, а просто постараться использовать имеющиеся ресурсы. Поэтому мы всегда должны попытаться использовать функции, предоставляемые языком, которым в данном случае является MQL5, и избегать создания собственных функций. Может это кажется абсурдным, но на самом деле, если немного задуматься, то мы увидим, что время от времени в платформе происходят улучшения некоторых функций, и если мы будем использовать эти же функции, то получим лучшую производительность и большую безопасность в наших программах, и это без необходимости прилагать дополнительные усилия.

Таким образом, цель оправдывает средства, однако, помогут ли сделанные выше изменения советнику извлечь пользу из любых улучшений в библиотеке MQL5? Ответ на этот вопрос - НЕТ. Приведенные выше изменения необходимы для того, чтобы моделирование имен объектов получилось правильным, чтобы мы могли эффективно использовать возможные будущие улучшения, исходящие от разработчиков MQL5 и платформы MetaTrader 5. Ниже показываем один из пунктов, который может быть полезен:

inline void RemoveIndicator(ulong ticket, eIndicatorTrade it = IT_NULL)
{
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
        if ((it == IT_NULL) || (it == IT_PENDING) || (it == IT_RESULT))
                ObjectsDeleteAll(Terminal.Get_ID(), StringFormat("%s%c%llu%c", def_NameObjectsTrade, def_SeparatorInfo, ticket, (ticket > 1 ? '*' : def_SeparatorInfo)));
        else ObjectsDeleteAll(Terminal.Get_ID(), StringFormat("%s%c%llu%c%c", def_NameObjectsTrade, def_SeparatorInfo, ticket, def_SeparatorInfo, (char)it));
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
        m_InfoSelection.bIsMovingSelect = false;
        ChartRedraw();
}

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

inline void RemoveIndicator(ulong ticket, eIndicatorTrade it = IT_NULL)
{
#define macroDestroy(A, B)      {                                                                               \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_GROUND, B));                            \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_LINE, B));                              \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_CLOSE, B));                             \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_EDIT, B));                              \
                if (A != IT_RESULT)     ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_MOVE, B));      \
                else ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_PROFIT, B));                       \
                                }

        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
        if ((it == IT_NULL) || (it == IT_PENDING) || (it == IT_RESULT))
        {
                macroDestroy(IT_RESULT, true);
                macroDestroy(IT_RESULT, false);
                macroDestroy(IT_PENDING, true);
                macroDestroy(IT_PENDING, false);
                macroDestroy(IT_TAKE, true);
                macroDestroy(IT_TAKE, false);
                macroDestroy(IT_STOP, true);
                macroDestroy(IT_STOP, false);
        } else
        {
                macroDestroy(it, true);
                macroDestroy(it, false);
        }
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
#undef macroDestroy
}

Может вам покажется, что код сделали более компактным, но нет. То, что произошло сокращение кода - это очевидное изменение, которое заметит менее опытный человек, а правда гораздо глубже. На самом деле, старый код был заменен новым, который лучше использует ресурсы платформы, однако, так как моделирование имен объектов, которое было ранее, не позволяло это улучшить. Мы переделали моделирование, и таким образом мы теперь можем рассчитывать на возможность использования функции самого MQL5. Таким образом, если эта функция по какой-либо причине будет улучшена, советник получит выгоду от этой модификации без необходимости внесения с нашей стороны каких-либо изменения в структуру советника. Речь идет о функции ObjectsDeleteAll. Если мы используем ее правильно, MetaTrader 5 сама выполнит за нас очистку, причем нам не нужно указывать слишком много деталей, мы просто укажем имя объекта или объектов и доверим MetaTrader 5 всё остальное. Точки, где используется эта функция, выделены в новом коде. Обратите внимание на то, как мы сделали моделирование, чтобы сообщить о префиксе, который будет использован. Это было невозможно до модификации моделирования имен объектов.

Я бы хотел обратить ваше внимание на одну деталь во фрагменте нового кода, которая выделена ниже.

if ((it == IT_NULL) || (it == IT_PENDING) || (it == IT_RESULT))
        ObjectsDeleteAll(Terminal.Get_ID(), StringFormat("%s%c%llu%c", def_NameObjectsTrade, def_SeparatorInfo, ticket, (ticket > 1 ? '*' : def_SeparatorInfo)));

Как вы думаете, почему я добавил выделенную часть?

Суть в том, что если система создает тикет, начинающийся со значения, равного 1, то, как только отложенный ордер будет размещен, все объекты будут удалены с экрана. Неужели вы не поняли?! Вход, используемый для размещения отложенного ордера, имеет значение 1, т.е. индикатор 0 на самом деле имеет значение 1, а не 0, поскольку 0 используется для выполнения других тестов в советнике. Поэтому начальное значение будет равно 1. Теперь у нас есть проблема: давайте предположим, что торговая система создает тикет 1221766803, поэтому объект, который будет представлять этот тикет, будет иметь в качестве префикса следующее значение: SMD_OT#1221766803, поэтому, когда советник выполнит функцию ObjectsDeleteAll для удаления индикатора 0, имя объекта будет иметь вид SMD_OT#1и это приведет к удалению всех объектов, запущенных с этой информацией, включая вновь созданную систему. Чтобы решить эту проблему, мы внесем небольшую корректировку в имя, чтобы проинформировать функции ObjectsDeleteAll, добавляя дополнительный символ в конце имени, чтобы функция знала, удаляем ли мы индикатор 0 или другой индикатор.

Таким образом, если индикатор 0 является удаляемым, то функция получит следующее значение SMD_OT#1#. Это позволит избежать проблему. В то же время, в случае приведенного выше примера, функция получит имя SMD_OT#1221766803*. Вроде бы простая вещь, но из-за неё можно долго пытаться понять, почему советник удаляет индикатор из только что размещенного ордера.

Теперь поговорим об одной любопытной детали. Обратите внимание, что в конце новой функции, есть вызов ChartRedraw, но зачем он там нужен? Разве MetaTrader 5 не делает это обновление за нас?! Да, это так, но мы никогда не знаем точно, когда это произойдет, и у нас есть еще одна проблема: все вызовы для размещения или удаления объектов на графике являются синхронными, т.е. они выполняются в определенное время, но не обязательно в то время, которое мы себе представляем. Однако, наша система ордеров будет использовать объекты либо для отображения, либо для управления ордерами, и мы должны быть уверены в том, что объект находится на графике или его там нет, когда мы собираемся анализировать вещи. Мы не можем позволить себе думать, что MetaTrader 5 уже поместил или удалил объекты с графика, потому что мы должны быть уверены в этом,вот почему мы заставляем платформу сделать это обновление.

Таким образом, когда мы вызываем ChartRedraw, мы заставляем платформу обновить список объектов на графике, поэтому мы можем быть уверены, что определенный объект присутствует или отсутствует на графике. Если мы все ещё не понимаем причину этого, давайте рассмотрим следующую тему.


3.0.2 - Меньше объектов - больше скорость

Функция инициализации индикаторов в предыдущей версии была громоздкой. Было много повторяющихся проверок, и некоторые вещи были продублированы. В дополнение к некоторым незначительным проблемам, система повторно использовала очень маленькое количество из уже внедренных мощностей. Поэтому, чтобы воспользоваться преимуществами нового моделирования, я решил уменьшить количество объектов, создаваемых при инициализации. Таким образом, функция избавилась от избыточного, и осталась такой, как показано ниже:

void Initilize(void)
{
        ChartSetInteger(Terminal.Get_ID(), CHART_SHOW_OBJECT_DESCR, false);
        ChartSetInteger(Terminal.Get_ID(), CHART_SHOW_TRADE_LEVELS, false);
        ChartSetInteger(Terminal.Get_ID(), CHART_DRAG_TRADE_LEVELS, false);
        for (int c0 = OrdersTotal(); c0 >= 0; c0--) IndicatorInfosAdd(OrderGetTicket(c0));
        for (int c0 = PositionsTotal(); c0 >= 0; c0--) IndicatorInfosAdd(PositionGetTicket(c0));
}

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

inline void IndicatorAdd(ulong ticket)
{
        char ret;
                                
        if (ticket == def_IndicatorTicket0) ret = -1; else
        {
                if (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_PENDING, EV_LINE, false), OBJPROP_PRICE) != 0) return;
                if (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_RESULT, EV_LINE, false), OBJPROP_PRICE) != 0) return;
                if ((ret = GetInfosTradeServer(ticket)) == 0) return;
        }
        switch (ret)
        {
                case  1:
                        CreateIndicatorTrade(ticket, IT_RESULT);
                        PositionAxlePrice(ticket, IT_RESULT, m_InfoSelection.pr);
                        break;
                case -1:
                        CreateIndicatorTrade(ticket, IT_PENDING);
                        PositionAxlePrice(ticket, IT_PENDING, m_InfoSelection.pr);
                        break;
        }
        ChartRedraw();
        UpdateIndicators(ticket, m_InfoSelection.tp, m_InfoSelection.sl, m_InfoSelection.vol, m_InfoSelection.bIsBuy);
}

Посмотрите внимательно на приведенный выше код. На первый взгляд код содержит проверки, которые не имеют особого смысла, но они существуют по очень простой причине. Эта функция является единственным способом реального создания индикатора отложенного ордера или позиции. Таким образом, две выделенные линии будут проверять, существует ли индикатор. Для этого проверяется, хранится ли какое-нибудь значение в объекте, который используется в качестве линии. Здесь это значение будет значением цены, в которой находится объект. Это значение должно быть не нулевым если индикатор находится на графике. Во всех остальных случаях он будет равен нулю, либо потому что индикатор не существует, либо по любой другой причине - это не важно. Теперь вы поняли, почему мы должны принудительно обновить график? Если бы это не было сделано, советник добавлял бы объекты без необходимости, поэтому мы не можем ждать, пока платформа совершит это действие в какое-то неизвестное время, мы должны быть уверены, что график был обновлен. В противном случае, когда эти проверки будут выполнены, они будут сообщать вещи, не соответствующие текущему состоянию индикаторов, что сделает систему менее надежной.

Хотя кажется, что эти проверки замедляют скорость работы самого эксперта, но это концептуальная ошибка, потому что когда мы делаем такие проверки и не пытаемся заставить платформу создать объект, который, возможно уже находится в очереди на создание, мы сообщаем платформе "UPDATE NOW". Затем, когда нам это нужно, мы делаем проверку, чтобы убедиться в том, был ли объект уже создан, и в случае, если он уже создан, мы используем его нужным образом. Это называется «программировать в правильном направлении». Поскольку так мы заставляем платформу работать меньше и без необходимости тестировать созданные или не созданные объекты, то советник становится более надежным, потому что мы знаем, что у нас есть данные, с которыми мы хотим работать.

Таким образом, поскольку тесты покажут, что на графике нет индикатора, соответствующего заявленному входу, то будет создан индикатор. Но стоит обратить внимание, что в самом начале мы делаем еще один тест и проверяем, что мы создаем индикатор 0 или любой другой индикатор. Это гарантирует, что у нас нет ненужных объектов, поддерживаемых MetaTrader 5; у нас есть только те объекты, которые реально используем на графике. Поэтому, если мы создаем индикатор 0, то дальнейшее тестирование не требуется, так как мы его создадим в совершенно особых и специфических условиях. Этот индикатор 0 используется для позиционирования команд с помощью команд SHIFT или CTRL плюс работа с мышью. Но не волнуйтесь, скоро мы посмотрим, как он работает и обслуживается.

В приведенном выше коде есть одна важная деталь: почему мы обновляем график до вызова функции Update?! Это бессмысленно. Чтобы понять это, давайте рассмотрим функцию UpdateIndicators, расположенную чуть ниже.

void UpdateIndicators(ulong ticket, double tp, double sl, double vol, bool isBuy)
{
        double pr;
        bool b0 = false;
                                
        pr = macroGetLinePrice(ticket, IT_RESULT);
        pr = (pr > 0 ? pr : macroGetLinePrice(ticket, IT_PENDING));
        SetTextValue(ticket, IT_PENDING, vol);
        if (tp > 0)
        {
                if (b0 = (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_TAKE, EV_LINE, false), OBJPROP_PRICE) == 0 ? true : b0))
                        CreateIndicatorTrade(ticket, IT_TAKE);
                PositionAxlePrice(ticket, IT_TAKE, tp);
                SetTextValue(ticket, IT_TAKE, vol, (isBuy ? tp - pr : pr - tp));
        }
        if (sl > 0)
        {
                if (b0 = (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_STOP, EV_LINE, false), OBJPROP_PRICE) == 0 ? true : b0))
                        CreateIndicatorTrade(ticket, IT_STOP);
                PositionAxlePrice(ticket, IT_STOP, sl);
                SetTextValue(ticket, IT_STOP, vol, (isBuy ? sl - pr : pr - sl));
        }
        if (b0) ChartRedraw();
}

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

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

Но может быть вы потом зададитесь вопросом: «Зачем столько принудительных обновлений, неужели это действительно необходимо?!» И ответ этому - БОЛЬШОЕ и УВЕРЕННОЕ ДА... а причина этого — функция, приведенная ниже:

inline double SecureChannelPosition(void)
{
        double Res = 0, sl, profit, bid, ask;
        ulong ticket;
                                
        bid = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_BID);
        ask = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_ASK);
        for (int i0 = PositionsTotal() - 1; i0 >= 0; i0--) if (PositionGetSymbol(i0) == Terminal.GetSymbol())
        {
                IndicatorAdd(ticket = PositionGetInteger(POSITION_TICKET));
                SetTextValue(ticket, IT_RESULT, PositionGetDouble(POSITION_VOLUME), profit = PositionGetDouble(POSITION_PROFIT), PositionGetDouble(POSITION_PRICE_OPEN));
                sl = PositionGetDouble(POSITION_SL);
                if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
                {
                        if (ask < sl) ClosePosition(ticket);
                }else
                {
                        if ((bid > sl) && (sl > 0)) ClosePosition(ticket);
                }
                Res += profit;
        }
        return Res;
};

Вы могли бы подумать, что в этой функции нет ничего особенного. Вы уверены?! ОШИБКА!... эта функция содержит ключевой момент: мы должны убедиться в том, что индикатор находится на графике, иначе весь код, позволяющий его создать, будет вызываться несколько раз, создавая большую очередь для управления MetaTrader 5, и некоторые данные могут быть потеряны или даже устареют. Всё это сделает систему нестабильной, менее безопасной и, следовательно, ненадежной. Обратите внимание: мы выделили вызов функции, которая создает индикатор. Если бы мы не заставляли MetaTrader 5 обновлять график вместо нас в стратегические моменты, то у нас могли бы возникнуть проблемы, так как вышеупомянутая функция вызывается именно событием OnTick, а в периоды высокой волатильности количество вызовов, поступающих от OnTick, довольно большое, что может породить избыток объектов в очереди, а это совсем не хорошо. Таким образом, данные принудительно обновляются через вызов ChartRedraw и проверяются через ObjectGetDouble, тем самым уменьшая вероятность того, что в очереди будет слишком много объектов.

И прежде всего, даже не глядя на то, как работает система, можно подумать: «Как хорошо, что теперь, в случае случайного удаления объекта TradeLine, советник заметит это, и при безуспешной проверке через ObjectGetDouble и неудаче, индикатор создастся заново». В этом и заключается идея. Но оператору или пользователю не рекомендуется удалять объекты, присутствующие в окне списка объектов, не зная, какой именно объект удален, так как при удалении любого объекта (кроме TradeLine), советник может не заметить индикатор, оставшись без средств доступа к нему, поскольку он просто не имеет другого способа доступа, кроме как с помощью кнопок, присутствующих на нем.

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

void DispatchMessage(int id, long lparam, double dparam, string sparam)
{
        ulong   ticket;
        double  price;
        bool    bKeyBuy,
                bKeySell,
                bEClick;
        datetime        dt;
        uint            mKeys;
        char            cRet;
        eIndicatorTrade it;
        eEventType      ev;
                                
        static bool bMounting = false, bIsDT = false;
        static double valueTp = 0, valueSl = 0, memLocal = 0;
                                
        switch (id)
        {
                case CHARTEVENT_MOUSE_MOVE:
                        Mouse.GetPositionDP(dt, price);
                        mKeys   = Mouse.GetButtonStatus();
                        bEClick  = (mKeys & 0x01) == 0x01;    //Клик левой кнопкой
                        bKeyBuy  = (mKeys & 0x04) == 0x04;    //Нажатый SHIFT
                        bKeySell = (mKeys & 0x08) == 0x08;    //Нажатый CTRL
                        if (bKeyBuy != bKeySell)
                        {
                                if (!bMounting)
                                {
                                        Mouse.Hide();
                                        bIsDT = Chart.GetBaseFinance(m_InfoSelection.vol, valueTp, valueSl);
                                        valueTp = Terminal.AdjustPrice(valueTp * Terminal.GetAdjustToTrade() / m_InfoSelection.vol);
                                        valueSl = Terminal.AdjustPrice(valueSl * Terminal.GetAdjustToTrade() / m_InfoSelection.vol);
                                        m_InfoSelection.it = IT_PENDING;
                                        m_InfoSelection.pr = price;
                                }
                                m_InfoSelection.tp = m_InfoSelection.pr + (bKeyBuy ? valueTp : (-valueTp));
                                m_InfoSelection.sl = m_InfoSelection.pr + (bKeyBuy ? (-valueSl) : valueSl);
                                m_InfoSelection.bIsBuy = bKeyBuy;
                                if (!bMounting)
                                {
                                        IndicatorAdd(m_InfoSelection.ticket = def_IndicatorTicket0);
                                        m_TradeLine.SpotLight(macroMountName(def_IndicatorTicket0, IT_PENDING, EV_LINE, false));
                                        m_InfoSelection.bIsMovingSelect = bMounting = true;
                                }
                                MoveSelection(price);
                                if ((bEClick) && (memLocal == 0))
                                {
                                        RemoveIndicator(def_IndicatorTicket0);
                                        CreateOrderPendent(m_InfoSelection.vol, bKeyBuy, memLocal = price,  price + m_InfoSelection.tp - m_InfoSelection.pr, price + m_InfoSelection.sl - m_InfoSelection.pr, bIsDT);
                                }
                        }else if (bMounting)
                        {
                                RemoveIndicator(def_IndicatorTicket0);
                                Mouse.Show();
                                memLocal = 0;
                                bMounting = false;
                        }else if ((!bMounting) && (bKeyBuy == bKeySell))
                        {
                                if (bEClick) SetPriceSelection(price); else MoveSelection(price);
                        }
                        break;
                case CHARTEVENT_OBJECT_DELETE:
                        if (GetIndicatorInfos(sparam, ticket, it, ev))
                        {
                                if (GetInfosTradeServer(ticket) == 0) break;
                                CreateIndicatorTrade(ticket, it);
                                if ((it == IT_PENDING) || (it == IT_RESULT))
                                        PositionAxlePrice(ticket, it, m_InfoSelection.pr);
                                ChartRedraw();
				m_TradeLine.SpotLight();
                                m_InfoSelection.bIsMovingSelect = false;
                                UpdateIndicators(ticket, m_InfoSelection.tp, m_InfoSelection.sl, m_InfoSelection.vol, m_InfoSelection.bIsBuy);
                        }
                        break;
                case CHARTEVENT_CHART_CHANGE:
                        ReDrawAllsIndicator();
                        break;
                case CHARTEVENT_OBJECT_CLICK:
                        if (GetIndicatorInfos(sparam, ticket, it, ev)) switch (ev)
                        {
                                case EV_CLOSE:
                                        if ((cRet = GetInfosTradeServer(ticket)) != 0) switch (it)
                                        {
                                                case IT_PENDING:
                                                case IT_RESULT:
                                                        if (cRet < 0) RemoveOrderPendent(ticket); else ClosePosition(ticket);
                                                        break;
                                                case IT_TAKE:
                                                case IT_STOP:
							m_InfoSelection.ticket = ticket;
							m_InfoSelection.it = it;
                                                        m_InfoSelection.bIsMovingSelect = true;
                                                        SetPriceSelection(0);
                                                        break;
                                        }
                                        break;
                                case EV_MOVE:
                                        if (m_InfoSelection.bIsMovingSelect)
                                        {
                                                m_TradeLine.SpotLight();
                                                m_InfoSelection.bIsMovingSelect = false;
                                        }else
                                        {
                                                m_InfoSelection.ticket = ticket;
                                                m_InfoSelection.it = it;
                                                if (m_InfoSelection.bIsMovingSelect = (GetInfosTradeServer(ticket) != 0))
                                                m_TradeLine.SpotLight(macroMountName(ticket, it, EV_LINE, false));
                                        }
                                        break;
                        }
                        break;
        }
}

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


3.0.3 - Давайте разберем функцию DispatchMessage

Эта тема является объяснением того, что происходит в функции DispatchMessage. Если вы, просто посмотрев код, уже получили представление о том, как это работает, то эта тема ничего вам не даст.

Первое, что мы имеем после локальных переменных - это статические переменные.

static bool bMounting = false, bIsDT = false;
static double valueTp = 0, valueSl = 0, memLocal = 0;

Их можно просто объявить как приватные переменные в классе, но поскольку они будут использоваться только в этом месте кода, то нет смысла, чтобы другие функции в классе видели эти переменные. Однако из-за этого мы должны объявить их как статические, потому что они должны помнить свои значения при повторном вызове функци. Если мы не добавим ключевое слово static в объявленные, то они потеряют свои значения, как только функция завершится. Как только это будет сделано, мы начнем обрабатывать события, которые MetaTrader 5 указывает советнику.

Как только это будет сделано, мы начнем обрабатывать события, которые MetaTrader 5 указывает советнику. Первое событие, которое мы рассмотрим, можно увидеть ниже:

case CHARTEVENT_MOUSE_MOVE:
        Mouse.GetPositionDP(dt, price);
        mKeys   = Mouse.GetButtonStatus();
        bEClick  = (mKeys & 0x01) == 0x01;    //Клик левой кнопкой
        bKeyBuy  = (mKeys & 0x04) == 0x04;    //Нажатый SHIFT
        bKeySell = (mKeys & 0x08) == 0x08;    //Нажатый CTRL

Здесь мы собираем и изолируем данные от мыши и некоторых клавиш (от клавиатуры), связанных с мышью. Как только мы это сделали, мы внесем длинный набор кода, который начинается с теста.

if (bKeyBuy != bKeySell)

Если вы нажмете клавишу SHIFT или CTRL, но не обе одновременно, этот тест заставит советника представить, что вы хотите разместить ордер по определенной цене. Если это так, проведем повторное тестирование.

if (!bMounting)
{
        Mouse.Hide();
        bIsDT = Chart.GetBaseFinance(m_InfoSelection.vol, valueTp, valueSl);
        valueTp = Terminal.AdjustPrice(valueTp * Terminal.GetAdjustToTrade() / m_InfoSelection.vol);
        valueSl = Terminal.AdjustPrice(valueSl * Terminal.GetAdjustToTrade() / m_InfoSelection.vol);
        m_InfoSelection.it = IT_PENDING;
        m_InfoSelection.pr = price;
}

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

Следующий шаг - создание уровней тейка и стопа, а также указание того, будем ли мы покупать или продавать.

m_InfoSelection.tp = m_InfoSelection.pr + (bKeyBuy ? valueTp : (-valueTp));
m_InfoSelection.sl = m_InfoSelection.pr + (bKeyBuy ? (-valueSl) : valueSl);
m_InfoSelection.bIsBuy = bKeyBuy;

Они создаются за пределами круга, потому что при перемещении мыши в другой ценовой диапазон мы также должны будем переместить тейк и стоп. Но почему этого кода нет в тесте сборки? Причина в том, что если изменить, отпустить клавишу SHIFT и нажать клавишу CTRL, или наоборот, не перемещая мышь, при этом с индикаторами на экране, значения индикаторов тейка и стопа будут меняться местами, чтобы избежать этого, данный фрагмент нужно оставить за пределами теста. Но это вынуждает нас провести новый тест сборки, который можно увидеть ниже:

if (!bMounting)
{
        IndicatorAdd(m_InfoSelection.ticket = def_IndicatorTicket0);
        m_TradeLine.SpotLight(macroMountName(def_IndicatorTicket0, IT_PENDING, EV_LINE, false));
        m_InfoSelection.bIsMovingSelect = bMounting = true;
}

Но почему два теста? Разве мы не можем сделать это с помощью одного? Это было бы идеально, но функция, выделенная в приведенном выше коде, не позволяет нам этого сделать, нужно посмотреть на IndicatorAdd, чтобы понять этот факт. После создания индикатора 0, мы устанавливаем его как выбранный и показываем, что он уже запущен и собран. Поэтому можно переместить его с помощью следующей строки.

MoveSelection(price);

Однако, даже в рамках этого же критерия нажатия SHIFT или CTRL для размещения отложенного ордера, у нас есть заключительный этап.

if ((bEClick) && (memLocal == 0))
{
        RemoveIndicator(def_IndicatorTicket0);
        CreateOrderPendent(m_InfoSelection.vol, bKeyBuy, memLocal = price,  price + m_InfoSelection.tp - m_InfoSelection.pr, price + m_InfoSelection.sl - m_InfoSelection.pr, bIsDT);
}

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

Одновременно с удалением индикатора 0 с графика, отправляется ордер на торговый сервер, все параметры которого заполнены должным образом.

Теперь давайте перейдем к следующему шагу...

if (bKeyBuy != bKeySell)
{

// ... описанный код до настоящего момента ....

}else if (bMounting)
{
        RemoveIndicator(def_IndicatorTicket0);
        Mouse.Show();
        memLocal = 0;
        bMounting = false;
}

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

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

if (bKeyBuy != bKeySell)
{

// ... уже описанный код ....

}else if (bMounting)
{

// ... уже описанный код ...

}else if ((!bMounting) && (bKeyBuy == bKeySell))
{
        if (bEClick) SetPriceSelection(price); else MoveSelection(price);
}

Выделенный код - это последний шаг работы мыши в рамках обработки сообщения. В случае, если у нас нет ни собранного индикатора 0, ни клавиш SHIFT или CTRL в разном состоянии, означающее, что они могут быть нажаты или отпущены одновременно, мы имеем следующее поведение: если мы щелкаем левой кнопкой мыши, то цена будет отправлена в индикатор, а если мы только перемещаем мышь, цена будет использоваться для перемещения индикатора. Но тогда у нас возникает вопрос: какой индикатор? Не волнуйтесь, ведь скоро мы увидим, какой именно индикатор, но если вам интересно, то индикатор 0 не использует этот выделенный фрагмент. Если вы не понимаете, вернитесь к началу этой темы и прочитайте, как работает данная обработка сообщений.

Ниже показываем следующее сообщение, которое мы рассмотрим:

case CHARTEVENT_OBJECT_DELETE:
        if (GetIndicatorInfos(sparam, ticket, it, ev))
        {
                if (GetInfosTradeServer(ticket) == 0) break;
                CreateIndicatorTrade(ticket, it);
                if ((it == IT_PENDING) || (it == IT_RESULT))
                        PositionAxlePrice(ticket, it, m_InfoSelection.pr);
                ChartRedraw();
		m_TradeLine.SpotLight();
                m_InfoSelection.bIsMovingSelect = false;
                UpdateIndicators(ticket, m_InfoSelection.tp, m_InfoSelection.sl, m_InfoSelection.vol, m_InfoSelection.bIsBuy);
        }
        break;

Помните, выше я сказал, что в советнике есть небольшая система безопасности для предотвращения неправильного удаления индикаторов? Так вот, эта система содержится в коде обработки сообщений о событиях, отправляемых MetaTrader 5 при удалении объекта.

Когда это происходит, MetaTrader 5 сообщает с помощью параметра sparam имя удаленного объекта, по которому проверяется, был ли это индикатор, и если да, то какой. Обратите внимание, что не имеет значения, какой объект был затронут. Мы хотим знать, какой индикатор был затронут, после этого мы проверим, есть ли какой-либо ордер или позиция, связанная с индикатором, и если да, то мы снова создадим весь индикатор. В крайнем случае, если затронутый индикатор был базовым, то мы немедленно позиционируем его снова и заставим MetaTrader 5 незамедлительно поместить индикатор на график, независимо от того, какой это был индикатор. Мы удалим указание выбора любого выбранного индикатора и сделаем запрос на обновление в предельных данных индикатора. 

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

case CHARTEVENT_CHART_CHANGE:
        ReDrawAllsIndicator();
        break;

За ним идет событие, о котором пойдет речь - это событие щелчка по объекту.

case CHARTEVENT_OBJECT_CLICK:
        if (GetIndicatorInfos(sparam, ticket, it, ev)) switch (ev)
        {
//....
        }
        break;

Это начинается так, как показано выше, где MetaTrader 5 сообщает нам, на каком объекте кликнули, чтобы советник мог протестировать и проверить, какой тип события обрабатывать. Пока у нас есть 2 события CLOSE и MOVE. Давайте сначала рассмотрим событие CLOSE, которое закроет и определит конец индикатора на экране.

case EV_CLOSE:
        if ((cRet = GetInfosTradeServer(ticket)) != 0) switch (it)
        {
                case IT_PENDING:
                case IT_RESULT:
                        if (cRet < 0) RemoveOrderPendent(ticket); else ClosePosition(ticket);
                        break;
                case IT_TAKE:
                case IT_STOP:
			m_InfoSelection.ticket = ticket;
			m_InfoSelection.it = it;
                        m_InfoSelection.bIsMovingSelect = true;
                        SetPriceSelection(0);
                        break;
        }
        break;

На заключительном событии будет сделано следующее: Он вместе с сервером будет использовать тикет для поиска того, что нужно закрыть, и есть ли у нас на самом деле то, что нужно закрыть, потому что может случиться так, что к тому времени, когда что-то надо будет закрыть, сервер уже закрыл это, а советник еще не был проинформирован об этом. Поскольку у нас есть то, что нужно закрыть, давайте это реализуем правильным способом, так у нас будут тесты и правильный способ сообщить классу для того, чтобы закрыть или удалить индикатор с графика.

Так, мы приступим к последнему шагу в данной теме, его описание можно увидеть ниже.

case EV_MOVE:
        if (m_InfoSelection.bIsMovingSelect)
        {
                m_TradeLine.SpotLight();
                m_InfoSelection.bIsMovingSelect = false;
        }else
        {
                m_InfoSelection.ticket = ticket;
                m_InfoSelection.it = it;
                if (m_InfoSelection.bIsMovingSelect = (GetInfosTradeServer(ticket) != 0))
                m_TradeLine.SpotLight(macroMountName(ticket, it, EV_LINE, false));
        }
        break;

Событие MOVE - это событие, которое заставляет выделение индикатора перемещаться в соответствии с движениями мыши. Индикация осуществляется здесь, но перемещение выполняется во время события движения мыши. Помните, в начале темы я говорил, что есть условие, при котором мы не имеем дело с индикатором 0 и даже так нечто всё равно будет двигаться. Так вот, это нечто указывается в данной точке, в событии move. Здесь мы будем проверять, выбрано ли что-то для перемещения: если да, то индикатор, который был выбран, больше не будет выбран и не будет получать события движения мыши, а новый индикатор будет выбран. В этом случае данные нового индикатора, который получит данные мыши, будут сохранены в структуре, и этот индикатор получит изменение, указывающее на то, что он будет выбран. Это изменение видно по толщине линии, что придает ему больше наглядности.


3.0.4 - Новый класс объекта Mouse

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

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

Давайте начнем с создания некоторых определений, это заменяет систему создания постоянных имен переменных.

#define def_MousePrefixName "MOUSE "
#define def_NameObjectLineH def_MousePrefixName + "H"
#define def_NameObjectLineV def_MousePrefixName + "TMPV"
#define def_NameObjectLineT def_MousePrefixName + "TMPT"
#define def_NameObjectBitMp def_MousePrefixName + "TMPB"
#define def_NameObjectText  def_MousePrefixName + "TMPI"

После этого новая функция инициализации выглядит следующим образом:

void Init(color c1, color c2, color c3)
{
        m_Infos.cor01 = c1;
        m_Infos.cor02 = c2;
        m_Infos.cor03 = c3;
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_MOUSE_MOVE, true);
        ChartSetInteger(Terminal.Get_ID(), CHART_CROSSHAIR_TOOL, false);
        Show();
}

Обратите внимание, что она намного проще предыдущей версии, хотя в ней нет ничего лишнего. На этом этапе у нас есть вызов, который будет показывать систему мыши. Вызов осуществляется в выделенной точке в предыдущем коде, он вызовет код, который фактически создаст систему индикации на оси цен.

inline void Show(void)
{
        if (ObjectGetDouble(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_PRICE) == 0)
        {
                ObjectCreate(Terminal.Get_ID(), def_NameObjectLineH, OBJ_HLINE, 0, 0, 0);
                ObjectSetString(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_TOOLTIP, "\n");
                ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_BACK, false);
        }
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_COLOR, m_Infos.cor01);
}

Этот код очень интересен, потому что в нем мы проверяем, существует ли объект указателя мыши в цене или нет. Если проверка прошла успешно, то это означает, что на графике существует линия или что-либо связанное с мышью, поэтому всё, что мы делаем - это настройка цвета горизонтальной линии. Но зачем мы проводим эту проверку? Чтобы понять это, нам нужно посмотреть на функцию, отвечающую за скрытие, а точнее за удаление объектов, прикрепленных к мыши. Давайте рассмотрим приведенную ниже функцию:

inline void Hide(void)
{
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
        ObjectsDeleteAll(Terminal.Get_ID(), def_MousePrefixName + "T");
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_COLOR, clrNONE);
}

Здесь мы имеем дело с очень любопытным стилем работы. Все объекты, связанные с мышью и имеющие заданное имя, будут удалены с графика MetaTrader 5, поэтому список объектов всегда будет небольшим. Однако, горизонтальная линия не будет удалена, изменится только ее цвет, поэтому функция, показывающая мышь, проводит проверку перед созданием объекта, потому что на самом деле он не исключается из списка объектов, он только скрывается, но все остальные объекты удаляются из списка объектов. Но тогда как мы будем использовать эти другие объекты, которые используются в исследованиях? Поскольку исследования - это кратковременные моменты, то когда мы просто хотим узнать некоторую информацию, не будет имеет смысла держать объекты в списке только для того, чтобы использовать их 1-2 раза. Лучше создать их, провести исследование и затем удалить их из списка, таким образом мы получим более надежную систему.

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

Таким образом, наше внимание приковано к функции DispatchMessage, которая запускается так, как показано ниже:

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        int     w = 0;
        uint    key;
        static int b1 = 0;
        static double memPrice = 0;

Сразу после этого у нас есть код, который начнет обрабатывать первое событие.

switch (id)
{
        case CHARTEVENT_MOUSE_MOVE:
                Position.X = (int)lparam;
                Position.Y = (int)dparam;
                ChartXYToTimePrice(Terminal.Get_ID(), Position.X, Position.Y, w, Position.dt, Position.price);
                ObjectMove(Terminal.Get_ID(), def_NameObjectLineH, 0, 0, Position.price = Terminal.AdjustPrice(Position.price));
                if (b1 > 0) ObjectMove(Terminal.Get_ID(), def_NameObjectLineV, 0, Position.dt, 0);
                key = (uint) sparam;
                if ((key & 0x10) == 0x10)    //Средняя кнопка....
                {
                        CreateObjectsIntern();
                        b1 = 1;
                }

Когда мы нажимаем среднюю кнопку мыши, мы так генерируем вызов. Но сейчас это не тот случай, потом мы увидим, что эта функция делает. Обратите внимание, что мы пытаемся переместить объект, которого не существует, так как он отсутствует в списке объектов, который поддерживает MetaTrader 5. Этот вызов будет происходить только при нажатии средней кнопки мыши, обратите внимание на переменную b1, она контролирует, в какой момент трейдер находится внутри множества, участвующего в генерации исследования.

Как только трейдер нажмет левую кнопку мыши, и первый шаг будет завершен, у нас будет запущен следующий фрагмент:

if (((key & 0x01) == 0x01) && (b1 == 1))
{
        ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, false);
        ObjectMove(Terminal.Get_ID(), def_NameObjectLineT, 0, Position.dt, memPrice = Position.price);
        b1 = 2;
}

Он будет позиционировать линию тренда и вызывать следующий шаг с изменением значения переменной b1. На этом этапе можно перейти к следующему фрагменту.

if (((key & 0x01) == 0x01) && (b1 == 2))
{
        ObjectMove(Terminal.Get_ID(), def_NameObjectLineT, 1, Position.dt, Position.price);
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineT, OBJPROP_COLOR, (memPrice > Position.price ? m_Infos.cor03 : m_Infos.cor02));
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectText, OBJPROP_COLOR, (memPrice > Position.price ? m_Infos.cor03 : m_Infos.cor02));
        ObjectMove(Terminal.Get_ID(), def_NameObjectBitMp, 0, Position.dt, Position.price);
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectBitMp, OBJPROP_ANCHOR, (memPrice > Position.price ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER));
        ObjectSetString(Terminal.Get_ID(), def_NameObjectText, OBJPROP_TEXT, StringFormat("%.2f ", Position.price - memPrice));
        ObjectMove(Terminal.Get_ID(), def_NameObjectText, 0, Position.dt, Position.price);
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectText, OBJPROP_ANCHOR, (memPrice > Position.price ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER));
}

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

Но обратите внимание, что исследование будет проводиться, пока мышь удерживается левой кнопкой, как только мы ее отпустим, то выполнится следующий фрагмент.

if (((key & 0x01) != 0x01) && (b1 == 2))
{
        b1 = 0;
        ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, true);
        Hide();
        Show();
}
Position.ButtonsStatus = (b1 == 0 ? key : 0);

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

Код, создающий объекты для запуска исследований, отсутствовал, но поскольку это довольно простая функция, я не буду акцентировать на ней внимание в статье.


4.0 - Заключение

Хотя может показаться ерундой, но все изменения, сделанные здесь, на самом деле имеют большое значение для самой системы. Есть одна вещь, которую следует помнить: Наша система команд основана на графических объектах на экране, поэтому чем больше объектов обрабатывает советник, тем ниже будет его производительность при запросе конкретного объекта. И чтобы еще больше усложнить ситуацию, мы имеем дело с системой, которая работает в режиме реального времени, т.е. чем быстрее система нашего советника, тем лучше будет его производительность. Поэтому чем меньше вещей должен делать советник, тем лучше. В идеале он должен уметь работать только с системой ордеров, а все остальное мы должны выносить на другой уровень, а MetaTrader 5 должен позаботиться о нем. Это мы сделаем, конечно, постепенно, поскольку нам придется внести много небольших изменений, но ничего слишком сложного. Это будет сделано в течение нескольких следующих статей, скорее всего даже не в одной, посвященной исключительно повышению надежности советника.

Одно могу сказать точно: в будущем советник будет отвечать только за систему ордеров. В следующей статье мы предадим советнику очень интересный окончательный вид: мы еще больше уменьшим количество объектов, присутствующих в списке во время работы советника, поскольку большим генератором объектов является система ордеров, и покажем, как изменить эту систему таким образом, чтобы максимально снизить нагрузку, которую она создает на MetaTrader 5.

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


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

Простое создание сложных индикаторов с помощью объектов Простое создание сложных индикаторов с помощью объектов
В статье представлен метод создания сложных индикаторов, позволяющий избежать проблем при работе с несколькими графиками и буферами, а также при объединении данных из нескольких источников.
DoEasy. Элементы управления (Часть 21): Элемент управления SplitContainer. Разделитель панелей DoEasy. Элементы управления (Часть 21): Элемент управления SplitContainer. Разделитель панелей
В статье создадим класс вспомогательного объекта-разделителя панелей для элемента управления SplitContainer.
Популяционные алгоритмы оптимизации: Рой частиц (PSO) Популяционные алгоритмы оптимизации: Рой частиц (PSO)
В данной статье рассмотрим популярный алгоритм "Рой Частиц" (PSO — particle swarm optimisation). Ранее мы обсудили такие важные характеристики алгоритмов оптимизации как сходимость, скорость сходимости, устойчивость, масштабируемость, разработали стенд для тестирования, рассмотрели простейший алгоритм на ГСЧ.
DoEasy. Элементы управления (Часть 20): WinForms-объект SplitContainer DoEasy. Элементы управления (Часть 20): WinForms-объект SplitContainer
Сегодня начнём разрабатывать элемент управления SplitContainer из набора элементов MS Visual Studio. Этот элемент состоит из двух панелей, разделённых вертикальным или горизонтальным перемещаемым разделителем.