TradeObjects: Автоматизация торговли на основе графических объектов в MetaTrader
Геометрические построения на графиках котировок — одни из самых популярных инструментов трейдера уже на протяжении десятилетий. С развитием технологий становится все легче наносить линии поддержки или сопротивления, исторические уровни цен и целые фигуры — например, каналы и сетку Фибоначчи. ПО для алгоритмического трейдинга позволяет не только анализировать классические фигуры, но и торговать на их основе. Для MetaTrader тоже разработаны программы, автоматизирующие процесс в той или иной степени: достаточно добавить объект на график с запущенным экспертом или скриптом, и далее программа сама в нужный момент откроет позицию, будет её сопровождать и закроет в соответствии с настройками. С помощью такого ПО можно не только торговать онлайн, но и тренировать свои навыки в тестере в режиме визуализации. Подобные программы представлены и в Базе исходных кодов, и в Маркете сообщества трейдеров.
Но не все так гладко. Как правило, программы в Базе исходных кодов имеют упрощенный функционал, редко обновляются и потому устаревают (вплоть до потери совместимости с последними версиями языка MQL и терминала), а коммерческие продукты не всем по карману.
В этой статье мы попробуем разработать новый инструмент, представляющий собой золотую середину. Он будет максимально простым и при этом предоставит достаточно широкие возможности. Он будет совместим как с MetaTrader 4, так и с MetaTrader 5. А благодаря открытому исходному коду, его при необходимости легко можно расширить и модифицировать под свои нужды.
Поддержка MetaTrader 4 важна не только с точки зрения охвата большой части аудитории, которая по-прежнему пользуется этой версией, но и из-за некоторых ограничений тестера MetaTrader 5. В частности, визуальный тестер MetaTrader 5 не позволяет на текущий момент интерактивно работать с объектами (добавлять, удалять, редактировать свойства), что необходимо для данного инструмента. Поэтому оттачивать навыки по управлению объектами на истории можно только в тестере MetaTrader 4.
Выработка требований
Главное идеологическое требование для нашей системы автоматической торговли по графическим объектам — простота. Мы будем использовать стандартный интерфейс пользователя MetaTrader и свойства графических объектов, без специальных панелей и сложной логики настройки. Как показывает практика, все плюсы мощных и богатых на всевозможные опции систем часто нивелируются трудностями освоения и малой востребованностью каждого конкретного режима их эксплуатации. Мы будем стремиться использовать только общеизвестные приемы работы с объектами и интуитивно понятную интерпретацию их свойств.
Среди множества типов графических объектов для принятия торговых решений чаще всего используются:
- трендовая линия;
- горизонтальная линия;
- вертикальная линия;
- равноудаленный канал;
- линии Фибоначчи.
Именно для них мы и обеспечим поддержку в первую очередь. Этот список можно было бы расширить новыми типами, доступными в MetaTrader 5, но это нарушит совместимость с MetaTrader 4. Вместе с тем, на основе базовых принципов обработки объектов, которые будут реализованы в данном проекте, пользователи легко могут добавить другие объекты, в соответствии со своими предпочтениями.
Каждый из вышеперечисленных объектов формирует в двумерном пространстве чарта некую логическую границу, пересечение или отскок от которой дает сигнал. Его интерпретация обычно соответствует одной из следующих ситуаций:
- пробой или отскок от линии поддержки/сопротивления;
- пробой или отскок от исторического ценового уровня;
- достижение заданного уровня стоп-лосса или тейк-профита;
- наступление заданного момента времени.
В зависимости от выбранной стратегии, трейдер может в результате события произвести покупку или продажу, установить отложенный ордер или закрыть имеющуюся позицию. Всё это должна уметь и наша система. Таким образом, перечень основных функций будет выглядеть так:
- отсылка уведомлений различным способами (алерт, push-нотификация, e-mail);
- открытие рыночных ордеров;
- установка отложенных ордеров (buy stop, sell stop, buy limit, sell limit);
- полное или частичное закрытие открытой позиции, в том числе в качестве стоп-лосса и тейк-профита.
Разработка пользовательского интерфейса
Во многих аналогичных продуктах пользовательскому интерфейсу уделяется очень много внимания. Панели, диалоги, кнопки, повсеместный drag'n'drop... Всего этого в нашем проекте не будет. Вместо специального графического интерфейса воспользуемся тем, что по умолчанию дает MetaTrader.
У каждого объекта есть стандартный набор свойств, который можно приспособить под текущую задачу.
Во-первых, нужно будет отличать объекты, предназначенные для автоторговли, от прочих объектов, которые пользователь может нанести на график. Для этого дадим нашим объектам названия с предопределенным префиксом. Если префикс не задавать, эксперт воспримет все объекты с подходящими свойствами как активные. Но такой режим не рекомендуется, так как терминал может создавать свои объекты (например, закрытые ордера), а они могут оказать побочные эффекты.
Во-вторых, для выполнения различных функций (список которых приведен выше) зарезервируем разные стили оформления. Например, в настройках эксперта зададим стиль STYLE_DASH для установки отложенных ордеров, а STYLE_SOLID — для немедленного входа в рынок при пересечении ценой соответствующей линии. Стили разных операций должны отличаться.
В-третьих, надо указать направление торговли. Например, для покупок можно использовать синий цвет, а для продаж — красный. Действия, не связанные со входом в рынок и выходом из него, будем помечать третьим цветом — например, серым. Это, например, уведомления или установка отложенных ордеров. Последний случай отнесен к "нейтральной" категории, потому что часто устанавливается сразу несколько — обычно пара — разнонаправленных отложенных ордеров.
В-четвертых, необходимо определение типа отложенных ордеров. Это можно однозначно сделать по взаимному расположению линии ордера относительно текущей рыночной цены и цвета линии. Например, синяя линия над ценой подразумевает buy stop, но синяя под ценой — buy limit.
Наконец, в-пятых, для большинства операций необходима некая дополнительная информация, атрибуты. В частности, если сработал алерт (которых может быть несколько), то пользователь, скорее всего, захочет получить осмысленное сообщение. Для этой цели хорошо подходит пока еще не задействованное поле Описание. Для ордеров в этом поле будем указывать размер лота и время истечения. Всё это опционально, поскольку для удобства и минимизации необходимых настроек объектов в эксперте будут предусмотрены входные параметры со значениями по умолчанию. Помимо настроек объектов, эти умолчания будут содержать и размеры стоп-лосса и тейк-профита.
Для случаев, когда по каждой линии устанавливаются специфические размеры стоп-лосса или тейк-профита, воспользуемся объектами, в которых объединено несколько линий. К примеру, равноудаленный канал имеет две линии. Первая из них, лежащая на двух точках, будет отвечать за формирование торгового сигнала, а параллельная — с третьей точкой — задаст расстояние до стоп-лосса или тейк-профита. С каким именно уровнем мы имеем дело, легко определить по взаимному расположению и цвету линий. Например, для красного канала дополнительная линия, расположенная выше основной, сформирует стоп-лосс. Если бы она была ниже, то трактовалась бы как тейк-профит.
Если хочется задать и стоп-лосс, и тейк-профит, объект должен состоять как минимум из трех линий. Для этого подходит, например, сетка уровней Фибоначчи. В проекте по умолчанию используются стандартные уровни 38.2% (стоп-лосс), 61.8% (точка входа в рынок) и 161.8% (тейк-профит). Более гибкую настройку этого и более сложных типов объектов, в данной статье мы рассматривать не будем.
При активации того или иного объекта в результате пересечения ценой необходимо пометить объект как отработавший. Это можно сделать, например, установив ему атрибут "фона" OBJPROP_BACK. Для визуальной обратной связи с пользователем мы приглушим исходный цвет таких объектов. Например, синяя линия станет темно-синей после её обработки.
Однако среди трейдерской разметки зачастую встречаются настолько "сильные" линии или уровни, что связанное с ними событие — например, отбой от линии поддержки на коррекциях восходящего тренда — может происходить много раз.
Предусмотрим такую ситуацию с помощью толщины линии. Как известно, стили MetaTrader позволяют задать толщину от 1 до 5. При активации линии будем смотреть на её толщину, и если она больше 1, то вместо выключения из дальнейшей работы уменьшим толщину на 1. Так мы можем обозначить на графике ожидаемые множественные события с числом повторений до 5.
У этой возможности есть нюанс: цены, как правило, флуктуируют вокруг некоторого значения, и любая линия может быть пересечена много раз в течение небольшого периода времени. Трейдер, торгующий вручную, анализирует динамику отклонений цены на глаз и фактически "фильтрует" весь ценовой шум. В эксперте необходимо реализовать механизм, делающий то же самое автоматически.
Для этого цели введем входные параметры, определяющие размер "горячей области" пересечения, т.е. минимальный диапазон движения цены и его длительность, в пределах которых сигналы не будут формироваться. Иными словами, событие "пересечение линии" состоится не сразу же, как цена пересечет её, а только когда она отступит на заданное расстояние в пунктах.
Аналогичным образом введем параметр, задающий минимальный интервал между двумя последовательными событиями с одной и той же линией (только для линий толщиной больше 1). Здесь возникает новая задача: надо где-то хранить время предыдущего события с линией. Используем для этого свойство объекта OBJPROP_ZORDER. Это число типа long, и значение datetime там прекрасно помещается. Изменение порядка отображения линий практически не сказывается на внешнем представлении графика.
В интерфейсе пользователя для настройки линии, предназначенной для работы с системой, достаточно:
- открыть диалог свойств объекта;
- добавить выбранный префикс к имени;
- опционально указать параметры в описании:
- лот для линий рыночных и отложенных ордеров, частичного закрытия позиции,
- имена линий отложенных ордеров для линии активации отложенных ордеров,
- срок истечения для линии отложенного ордера;
- цвет как индикатор направления (по умолчанию, синий — покупка, красный — продажа, серый — нейтральный);
- стиль как селектор операции (алерт, вход в рынок, установка отложенного ордера, закрытие позиции);
- ширину как индикатор повторения события.
Настройка свойств горизонтальной линии для buy limit ордера (синяя штриховая) лотом 0.02 и сроком истечения 24 бара (часа)
Перечень контролируемых системой ордеров (отвечающих описанным требованиям) будет выводиться в комментарии на чарте, вместе с детализацией — тип объекта, описание, статус.
Разработка механизма исполнения
В соответствии с требованиями и общими соображениями, изложенными выше, начнем реализацию эксперта с входных параметров, в которых передаются префикс имен объектов, обрабатываемые цвета и стили, значения по умолчанию, а также размеры областей, генерирующих события на графике.
input int Magic = 0; input double Lot = 0.01 /*default lot*/; input int Deviation = 10 /*tolerance to price changes during order execution*/; input int DefaultTakeProfit = 0 /*points*/; input int DefaultStopLoss = 0 /*points*/; input int DefaultExpiration = 0 /*bars*/; input string CommonPrefit = "exp" /*empty to handle all compatible objects*/; input color BuyColor = clrBlue /*market and pending buy orders - open, close, sl, tp*/; input color SellColor = clrRed /*market and pending sell orders - open, close, sl, tp*/; input color ActivationColor = clrGray /*activation of pending orders placement, alert*/; input ENUM_LINE_STYLE InstantType = STYLE_SOLID /*opens market trades*/; input ENUM_LINE_STYLE PendingType = STYLE_DASH /*defines probable pending orders (requires activation)*/; input ENUM_LINE_STYLE CloseStopLossTakeProfitType = STYLE_DOT /*applied to open positions*/; input int EventHotSpot = 10 /*points*/; input int EventTimeSpan = 10 /*seconds*/; input int EventInterval = 10 /*bars*/;
Сам эксперт оформим в виде класса TradeObjects (файл TradeObjects.mq4, он же .mq5). Публичными в нем будут только конструктор, деструктор и методы обработки стандартных событий.
class TradeObjects { private: Expert *e; public: void handleInit() { detectLines(); } void handleTick() { #ifdef __MQL4__ if(MQLInfoInteger(MQL_TESTER)) { static datetime lastTick = 0; if(TimeCurrent() != lastTick) { handleTimer(); lastTick = TimeCurrent(); } } #endif e.trailStops(); } void handleTimer() { static int counter = 0; detectLines(); counter++; if(counter == EventTimeSpan) // wait until we have history record of bid for EventTimeSpan { counter = 0; if(PreviousBid > 0) processLines(); if(PreviousBid != Bid) PreviousBid = Bid; } } void handleChart(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id == CHARTEVENT_OBJECT_CREATE || id == CHARTEVENT_OBJECT_CHANGE) { if(checkObjectCompliance(sparam)) { if(attachObject(sparam)) { display(); describe(sparam); } } else { detectLines(); } } else if(id == CHARTEVENT_OBJECT_DELETE) { if(removeObject(sparam)) { display(); Print("Line deleted: ", sparam); } } } TradeObjects() { e = new Expert(Magic, Lot, Deviation); } ~TradeObjects() { delete e; } };
Экземпляр данного класса создадим статически, а затем привяжем его обработчики событий к соответствующим глобальным функциям.
TradeObjects to; void OnInit() { ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true); EventSetTimer(1); to.handleInit(); } void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { to.handleChart(id, lparam, dparam, sparam); } void OnTimer() { to.handleTimer(); } void OnTick() { to.handleTick(); }
Все торговые операции поручим отдельному движку, который скрыт во внешнем классе Expert (файл Expert01.mqh). Мы создаем его экземпляр (e) в конструкторе и удаляем в деструкторе класса TradeObjects. Более подробно рассмотрим движок позднее, а пока отметим, что TradeObjects будет делегировать ему многие операции.
Все обработчики событий handleInit, handleTick, handleTimer, handleChart вызывают метод detectLines, который нам предстоит написать. В нем будут анализироваться имеющиеся объекты и отбираться только те, что удовлетворяют нашим требованиям, причем пользователь может создавать, удалять, редактировать объекты в процессе выполнения эксперта. Найденные объекты сохраняются во внутренний массив. Его наличие позволяет отслеживать изменение состояния графика и сообщать пользователю об обнаружении новых или удалении старых объектов.
Периодически по таймеру вызывается другой метод processLines, который должен в цикле по массиву проверять наступление событий и выполнять соответствующие действия.
Как можно заметить, у объектов проверяется префикс, а также их стили, цвета и статус с помощью метода checkObjectCompliance (см. далее). Подходящие объекты добавляются во внутренний массив функцией attachObject, удаленные с графика — удаляются из массива функцией removeObject. Список объектов отображается в виде комментария на чарте с помощью метода display.
Массив состоит из простых структур, содержащих название объекта и его состояние:
private: struct LineObject { string name; int status; void operator=(const LineObject &o) { name = o.name; status = o.status; } }; LineObject objects[];
Статус прежде всего используется для пометки существующих объектов — например, это делается сразу после добавления нового объекта в массив функцией attachObject:
protected: bool attachObject(const string name) { bool found = false; int n = ArraySize(objects); for(int i = 0; i < n; i++) { if(objects[i].name == name) { objects[i].status = 1; found = true; break; } } if(!found) { ArrayResize(objects, n + 1); objects[n].name = name; objects[n].status = 1; return true; } return false; }
Проверка существования каждого объекта в последующие моменты времени происходит в методе detectLines:
bool detectLines() { startRefresh(); int n = ObjectsTotal(ChartID(), 0); int count = 0; for(int i = 0; i < n; i++) { string obj = ObjectName(ChartID(), i, 0); if(checkObjectCompliance(obj)) { if(attachObject(obj)) { describe(obj); count++; } } } if(count > 0) Print("New lines: ", count); bool changes = stopRefresh() || (count > 0); if(changes) { display(); } return changes; }
Здесь в начале вызывается вспомогательная функция startRefresh, которая сбрасывает флаги статуса в 0 у всех объектов массива, затем внутри цикла с помощью attachObject рабочие объекты вновь получают статус 1, а в конце происходит вызов stopRefresh, которая находит невостребованные объекты во внутреннем массиве по нулевому статусу, о чем сигнализируется пользователю.
Проверка каждого объекта на соответствие требованиям выполняется в методе checkObjectCompliance:
bool checkObjectCompliance(const string obj) { if(CommonPrefit == "" || StringFind(obj, CommonPrefit) == 0) { if(_ln[ObjectGetInteger(0, obj, OBJPROP_TYPE)] && _st[ObjectGetInteger(0, obj, OBJPROP_STYLE)] && _cc[(color)ObjectGetInteger(0, obj, OBJPROP_COLOR)]) { return true; } } return false; }
В нем, помимо префикса имени, проверяются наборы флагов с типами, стилями и цветами объектов. Для этого используется вспомогательный класс Set:
#include <Set.mqh> Set<ENUM_OBJECT> _ln(OBJ_HLINE, OBJ_VLINE, OBJ_TREND, OBJ_CHANNEL, OBJ_FIBO); Set<ENUM_LINE_STYLE> _st(InstantType, PendingType, CloseStopLossTakeProfitType); Set<color> _cc(BuyColor, SellColor, ActivationColor);
Теперь поговорим о главном методе — processLines. Поскольку он является центральным, это сказывается на его размере. Целиком код можно найти в приложении, а здесь в качестве иллюстрации приведем наиболее показательные фрагменты.
void processLines() { int n = ArraySize(objects); for(int i = 0; i < n; i++) { string name = objects[i].name; if(ObjectGetInteger(ChartID(), name, OBJPROP_BACK)) continue; int style = (int)ObjectGetInteger(0, name, OBJPROP_STYLE); color clr = (color)ObjectGetInteger(0, name, OBJPROP_COLOR); string text = ObjectGetString(0, name, OBJPROP_TEXT); datetime last = (datetime)ObjectGetInteger(0, name, OBJPROP_ZORDER); double aux = 0, auxf = 0; double price = getCurrentPrice(name, aux, auxf); ...
В цикле проходим по всем объектам, исключая уже сработавшие (у них, как мы договорились, будет установлен флаг OBJPROP_BACK). С помощью функции getCurrentPrice, которая будет показана ниже, узнаем значения цены текущего объекта. Поскольку некоторые типы объектов состоят из нескольких линий, передаем дополнительные значения цен через 2 параметра.
if(clr == ActivationColor) { if(style == InstantType) { if(checkActivation(price)) { disableLine(i); if(StringFind(text, "Alert:") == 0) Alert(StringSubstr(text, 6)); else if(StringFind(text, "Push:") == 0) SendNotification(StringSubstr(text, 5)); else if(StringFind(text, "Mail:") == 0) SendMail("TradeObjects", StringSubstr(text, 5)); else Print(text); } }
Далее проверяем стиль объекта для выяснения типа события и как его цена на 0-м баре соотносится с ценой Bid — в случае алерта и установки отложенных ордеров это делает функция checkActivation. Если активация произошла, выполняем соответствующее действие (в случае алерта выводим сообщение или отправляем адресату) и помечаем объект как выключенный с помощью disableLine.
Для торговых операций код активации, конечно, усложнится. Вот, например, упрощенный вариант для покупки по рынку и закрытию открытых коротких позиций:
else if(clr == BuyColor) { if(style == InstantType) { int dir = checkMarket(price, last); if((dir == 0) && checkTime(name)) { if(clr == BuyColor) dir = +1; else if(clr == SellColor) dir = -1; } if(dir > 0) { double lot = StringToDouble(ObjectGetString(0, name, OBJPROP_TEXT)); // lot[%] if(lot == 0) lot = Lot; double sl = 0.0, tp = 0.0; if(aux != 0) { if(aux > Ask) { tp = aux; if(DefaultStopLoss != 0) sl = Bid - e.getPointsForLotAndRisk(DefaultStopLoss, lot) * _Point; } else { sl = aux; if(DefaultTakeProfit != 0) tp = Bid + e.getPointsForLotAndRisk(DefaultTakeProfit, lot) * _Point; } } else { if(DefaultStopLoss != 0) sl = Bid - e.getPointsForLotAndRisk(DefaultStopLoss, lot) * _Point; if(DefaultTakeProfit != 0) tp = Bid + e.getPointsForLotAndRisk(DefaultTakeProfit, lot) * _Point; } sl = NormalizeDouble(sl, _Digits); tp = NormalizeDouble(tp, _Digits); int ticket = e.placeMarketOrder(OP_BUY, lot, sl, tp); if(ticket != -1) // success { disableLine(i); } else { showMessage("Market buy failed with '" + name + "'"); } } } else if(style == CloseStopLossTakeProfitType) // close sell position, stoploss for sell, takeprofit for sell { int dir = checkMarket(price) || checkTime(name); if(dir != 0) { double lot = StringToDouble(ObjectGetString(0, name, OBJPROP_TEXT)); // lot if(lot > 0) { if(e.placeMarketOrder(OP_BUY, lot) != -1) // will trigger OrderCloseBy(); { disableLine(i); } else { showMessage("Partial sell close failed with '" + name + "'"); } } else { if(e.closeMarketOrders(e.mask(OP_SELL)) > 0) { disableLine(i); } else { showMessage("Complete sell close failed with '" + name + "'"); } } } }
Здесь используется функция checkMarket (более усложненный вариант checkActivation, обе приведены далее), которая выполняет проверку наступления события. При срабатывании условия получаем из свойств объекта уровни стоп-лосса или тейк-профита, лота, а затем открываем ордер.
Размер лота задается в описании объекта в контрактах или как процент от свободной маржи — в последнем случае величина записывается как отрицательная. Смысл такой нотации легко запомнить, если представить, что Вы фактически указываете, какую часть средств "откусить" под обеспечение нового ордера.
Функции checkActivation и checkMarket похожи, они используют входные настройки эксперта, определяющие размер области срабатывания событий:
bool checkActivation(const double price) { if(Bid >= price - EventHotSpot * _Point && Bid <= price + EventHotSpot * _Point) { return true; } if((PreviousBid < price && Bid >= price) || (PreviousBid > price && Bid <= price)) { return true; } return false; } int checkMarket(const double price, const datetime last = 0) // returns direction of price movement { if(last != 0 && (TimeCurrent() - last) / PeriodSeconds() < EventInterval) { return 0; } if(PreviousBid >= price - EventHotSpot * _Point && PreviousBid <= price + EventHotSpot * _Point) { if(Bid > price + EventHotSpot * _Point) { return +1; // up } else if(Bid < price - EventHotSpot * _Point) { return -1; // down } } if(PreviousBid < price && Bid >= price && MathAbs(Bid - PreviousBid) >= EventHotSpot * _Point) { return +1; } else if(PreviousBid > price && Bid <= price && MathAbs(Bid - PreviousBid) >= EventHotSpot * _Point) { return -1; } return 0; }
Напомним, что цена PreviousBid сохраняется экспертом в обработчике handleTimer с периодичностью EventTimeSpan секунд. Результат работы функций — признак пересечения ценой Bid цены объекта на 0-м баре, причем checkActivation возвращает простой логический флаг, а checkMarket — направление движения цены: +1 — вверх, -1 — вниз.
Особо отметим, что контроль пересечения котировками объектов выполняется по цене bid. Это сделано потому, что весь график построен по цене bid, включая и нанесенные линии. Даже если трейдер сформировал разметку для ордеров покупки, они сработают по правильным сигналам: график ask неявно проходит над графиком bid на величину спреда, и потенциальные линии, которые могли бы быть построены по графику ask, пересекали бы цену синхронно с текущей разметкой по bid.
Для линий стиля PendingType и нейтрального ActivationColor поведение особенное: в момент их пересечения ценой выставляются отложенные ордера. Размещение самих ордеров задается другими линиями, имена которых перечислены через слэш ('/') в описании линии активации. Если описание линии активации пустое, система найдет все линии отложенных ордеров по стилю и установит их. Направление отложенных ордеров, как и для рыночных ордеров, соответствует их цвету — BuyColor или SellColor для покупки или продажи, а в описании можно указать лот и срок истечения (в барах).
Способы сочетания стилей и цветов объектов и их соответствующее значение приведены в таблице.
Цвет и Стиль | BuyColor | SellColor | ActivationColor |
InstantType | покупка по рынку | продажа по рынку | алерт |
PendingType | потенциальный отложенный ордер на покупку | потенциальный отложенный ордер на продажу | инициирование выставления отложенных ордеров |
CloseStopLossTakeProfitType | закрытие, стоп-лосс, тейк-профит для короткой позиции |
закрытие, стоп-лосс, тейк-профит для длинной позиции |
закрыть всё |
Вернемся к методу getCurrentPrice — пожалуй, самому важному после processLines.
double getCurrentPrice(const string name, double &auxiliary, double &auxiliaryFibo) { int type = (int)ObjectGetInteger(0, name, OBJPROP_TYPE); if(type == OBJ_TREND) { datetime dt1 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 0); datetime dt2 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 1); int i1 = iBarShift(NULL, 0, dt1, true); int i2 = iBarShift(NULL, 0, dt2, true); if(i1 <= i2 || i1 == -1 || i2 == -1) { Print("Incorrect line: ", name); return 0; } double p1 = ObjectGetDouble(0, name, OBJPROP_PRICE, 0); double p2 = ObjectGetDouble(0, name, OBJPROP_PRICE, 1); double k = -(p1 - p2)/(i2 - i1); double b = -(i1 * p2 - i2 * p1)/(i2 - i1); return b; } else if(type == OBJ_HLINE) { return ObjectGetDouble(0, name, OBJPROP_PRICE, 0); } else if(type == OBJ_VLINE) { return EMPTY_VALUE; // should not be a null, not used otherwise } else if(type == OBJ_CHANNEL) { datetime dt1 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 0); datetime dt2 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 1); datetime dt3 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 2); int i1 = iBarShift(NULL, 0, dt1, true); int i2 = iBarShift(NULL, 0, dt2, true); int i3 = iBarShift(NULL, 0, dt3, true); if(i1 <= i2 || i1 == -1 || i2 == -1 || i3 == -1) { Print("Incorrect channel: ", name); return 0; } double p1 = ObjectGetDouble(0, name, OBJPROP_PRICE, 0); double p2 = ObjectGetDouble(0, name, OBJPROP_PRICE, 1); double p3 = ObjectGetDouble(0, name, OBJPROP_PRICE, 2); double k = -(p1 - p2)/(i2 - i1); double b = -(i1 * p2 - i2 * p1)/(i2 - i1); double dy = i3 * k + b - p3; auxiliary = p3 - i3 * k; return b; } else if(type == OBJ_FIBO) { // level 61.8 is enter point at retracement (buy/sell limit), // 38.2 and 161.8 as stoploss/takeprofit double p1 = ObjectGetDouble(0, name, OBJPROP_PRICE, 0); double p2 = ObjectGetDouble(0, name, OBJPROP_PRICE, 1); datetime dt1 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 0); datetime dt2 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 1); if(dt2 < dt1) { swap(p1, p2); } double price = (p2 - p1) * ObjectGetDouble(0, name, OBJPROP_LEVELVALUE, 4) + p1; auxiliary = (p2 - p1) * ObjectGetDouble(0, name, OBJPROP_LEVELVALUE, 2) + p1; auxiliaryFibo = (p2 - p1) * ObjectGetDouble(0, name, OBJPROP_LEVELVALUE, 6) + p1; return price; } return 0; }
Суть проста — в зависимости от типа объекта рассчитать для него цену на 0-м баре (для основной и дополнительных линий). При размещении объектов на графике важно обращать внимание, чтобы все точки объекта находились в прошлом — там, где имеется валидный номер бара. В противном случае объект будет считаться недействительным, поскольку для него невозможно однозначно рассчитать цену.
В случае вертикальной линии мы возвращаем EMPTY_VALUE — то есть, это и не ноль, но и не конкретная цена (потому что такая линия удовлетворяет любой цене). Поэтому для вертикальных линий следует использовать дополнительную проверку на совпадение с текущим временем. Это делает функция checkTime, вызов который внимательные читатели уже могли заметить во фрагменте processLines.
bool checkTime(const string name) { return (ObjectGetInteger(0, name, OBJPROP_TYPE) == OBJ_VLINE && (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 0) == Time[0]); }
Наконец, рассмотрим реализацию функции disableLine, которая уже много раз встречалась в коде.
void disableLine(const string name) { int width = (int)ObjectGetInteger(0, name, OBJPROP_WIDTH); if(width > 1) { ObjectSetInteger(0, name, OBJPROP_WIDTH, width - 1); ObjectSetInteger(0, name, OBJPROP_ZORDER, TimeCurrent()); } else { ObjectSetInteger(0, name, OBJPROP_BACK, true); ObjectSetInteger(0, name, OBJPROP_COLOR, darken((color)ObjectGetInteger(0, name, OBJPROP_COLOR))); } display(); }
Если толщина линии больше 1, мы её уменьшаем на 1 и сохраняем текущее время события в свойстве OBJPROP_ZORDER. В случае обычных линий — перемещаем их на задний фон и приглушаем цвет. Объекты в фоне считаются отключенными.
Что касается свойства OBJPROP_ZORDER, то оно считывается, как было показано выше, в методе processLines в переменную datetime last, которая затем передается в качестве аргумента в метод checkMarket(price, last). Внутри мы отслеживаем, чтобы время с момента предыдущей активации превышало заданный во входной переменной интервал (в барах):
if(last != 0 && (TimeCurrent() - last) / PeriodSeconds() < EventInterval) { return 0; }
TradeObjects позволяет выполнить частичное закрытие, если в описании объекта типа CloseStopLossTakeProfitType указан лот. Система открывает встречный ордер заданного объема, а потом вызывает OrderCloseBy. Для включения режима предусмотрен специальный флаг AllowOrderCloseBy во входных переменных. Если он включен, встречные позиции всегда будут "схлопываться" в одну. Напомню, что эта функция разрешена не на всех счетах (эксперт проверяет эту настройку и выводит в лог соответствующее сообщение, если данная возможность заблокирована). В случае MetaTrader 5 счет должен быть с хеджированием. Желающие могут усовершенствовать систему в плане альтернативной реализации частичного закрытия — без использования OrderCloseBy, с просмотром списка позиций и выбора конкретной, уменьшаемой с помощью каких-либо атрибутов.
Вернемся к классу Expert, выполняющему для TradeObjects все торговые операции. Он представляет собой простейший набор методов по открытию и закрытию ордеров, сопровождению стоп-лоссов, расчету лотов, исходя из заданного риска. В нем используется метафора ордеров MetaTrader 4, которая адаптируется для MetaTrader 5 с помощью библиотеки MT4Orders.
Класс не предоставляет функционала по изменению установленных отложенных ордеров. Перемещение их цены, уровней стоп-лосса и тейк-профита оставлено в ведении самого терминала: если включена опция "Показывать торговые уровни", он позволяет это делать с помощью Drag'n'Drop.
Класс Expert можно заменить на любой другой, которым вы пользуетесь.
Приложенный исходный код компилируется как в MetaTrader 4, так и в MetaTrader 5 (с дополнительными заголовочными файлами).
Следует отметить, что в текущей реализации TradeObjects есть отход от строгих практик ООП в угоду простоте. Например, нужно было бы иметь абстрактный торговый интерфейс, реализовать класс эксперта Expert как наследник этого интерфейса и затем передавать его в класс TradeObjects (например, через параметр конструктора). Это известный шаблон ООП внедрения зависимости (dependency injection). В данном проекте торговый движок жестким образом "зашит в код": создается и удаляется внутри объекта TradeObjects.
Кроме того, в нарушение принципов ООП, мы используем глобальные входные переменные непосредственно в коде класса TradeObjects. Лучший стиль программирования потребовал бы передавать их в класс как параметры конструктора или специальных методов. Это позволило бы, например, использовать TradeObjects как библиотеку внутри другого эксперта, дополняя его функциями ручной торговли по разметке.
В принципе, следовать базовым принципам ООП становится тем важнее, чем крупнее разрабатываемый проект. Поскольку здесь мы рассматриваем довольно простой и обособленный движок автоторговли по объектам, то его совершенствование (которое, как известно, не знает границ) оставлено для факультативного изучения.
Программа в действии
Ниже показано, как система выглядит в работе, при настройках стилей и цветов по умолчанию.
Допустим, мы увидели на графике формирование фигуры "голова-плечи" и размещаем горизонтальную красную линию для сделки продажи. Система выводит перечень обнаруженных и контролируемых ею объектов в комментарии.
Стоп-лосс будет установлен в соответствии с параметром эксперта DefaultStopLoss, а вместо тейк-профита ограничим время нахождения в рынке с помощью вертикальной синей пунктирной линии.
По достижении этой линии позиция закрывается (вне зависимости от прибыльности). Сработавшие линии помечаются как неактивные (их цвет приглушается, и сами они отправляются на задний план).
Через некоторое время котировки, похоже, собираются еще раз двинуться вниз, и мы ставим уровни Фибоначчи в надежде на отбой от уровня 61.8 (это всё, что умеет Фибоначчи в данном проекте по умолчанию, но вы можете реализовать и другие типы поведения). Обратите внимание, что цвет объекта Фибоначчи — это цвет диагональной линии, а не уровней: цвет уровней задается отдельной настройкой.
Когда цена доходит до уровня, открывается сделка, причем с заданными ценами стоп-лосс (38.2) и тейк-профит (161.8, не виден на скриншоте).
Какое-то время спустя мы видим формирование линии сопротивления сверху и размещаем синий канал в предположении, что цена все же пойдет вверх.
Отметим, что все линии до сих пор не содержали описания, и потому ордера открывались с лотом из параметра Lot (0.01, по умолчанию). В данном же случае стоит описание '-1', т.е. размер лота будет вычислен, как требующий 1% свободной маржи. Поскольку вспомогательная линия расположена ниже основной, канал задает расстояние до стоп-лосса (отличного от значения по умолчанию).
Канал действительно пробивается, и новая длинная позиция открывается. Как мы видим, объем был расчитан как 0.04 (при депозите 1000$). Синий сегмент на скриншоте — это сработавший канал, перемещенный в фон (так MetaTrader 4 изображает каналы на заднем плане).
Чтобы закрыть обе открытие позиции на покупку, разместим явную линию тейк-профита — красную пунктирную.
Цена доходит до данного уровня, и оба ордера закрываются.
Предположим, что после такого движения цена будет ходить в "коридоре". Чтобы поймать эту волатильность, установим две горизонтальные штриховые линии для лимитных ордеров сверху и снизу от цены, а также серую штриховую вертикальную линию для их активации. В принципе, они не обязаны быть горизонтальными или вертикальными.
Обратите внимание, что, например, для нижнего отложенного ордера в описании задан кастомизированный лот 0.02 и срок истечения через 24 бара (часа). По достижении линии активации устанавливаются отложенные ордера.
Через некоторое время срабатывает sell limit.
Buy limit истекает в понедельник.
Мы ставим вертикальную серую пунктирную линию, означающую закрытие всех позиций.
По её достижению закрывается короткая позиция, но если бы была открыта и длинная, то она была бы закрыта тоже.
В процессе работы эксперт выводит основные события в лог.
Отработавшие объекты, разумеется, можно удалять, чтобы очистить график. В примере они оставлены в качестве протокола выполненных действий.
К статье приложены шаблоны для MetaTrader 4 и MetaTrader 5 с линиями для демонстрационной торговли в тестере на графике EURUSD H1, начиная с 1 июля 2017 (вышеописанный период). Используются стандартные настройки эксперта за исключением параметра DefaultStopLoss, установленного в -1 (что соответствует потере 1% свободной маржи). Для наглядной иллюстрации расчета и сопровождения стоп-лосса предлагается начальный депозит 1000$, плечо 1:500. В случае MetaTrader 5 шаблон предварительно необходимо переименовать в tester.tpl (загрузка и редактирование шаблонов непосредственно в тестере пока не поддерживается платформой).
Заключение
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
в последних версиях мт5 не работает
Попробуйте вот эту модификацию. Пожалуйста, проверьте, нормально ли работает и отпишитесь (если будут проблемы - сообщите подробности).
Попробуйте вот эту модификацию. Пожалуйста, проверьте, нормально ли работает и отпишитесь (если будут проблемы - сообщите подробности).
привет
при компиляции выдал ошибку в строчке другого файла я заменил на 1 и компеляция прошла. Ответ по тесту чуть позже
Expert01.mqh имеет 231 ошибку
В целом не работает
привет
при компиляции выдал ошибку в строчке другого файла я заменил на 1 и компеляция прошла. Ответ по тесту чуть позже
Expert01.mqh имеет 231 ошибку
В целом не работает
Приложенный выше вариант моих исходников компилируется без проблем. Но вам нужно убедиться, что вы взяли свежую версию MT4Orders.mqh - это не моя библиотека, её поддерживает fxsaber.
Привет, библиотеку обновил, советник перекомпилил, без ошибок. Но он не работает как надо, крайне редко замечает линии и то после того как в них что-нибудь изменить, только горизонтальные, но чаще вообще не определяет и просто спит. Где искать проблему ? Спасибо
У меня работает. Нужны подробности что и как делаете, настройки.