Язык MQL4 для "чайников". Пользовательские индикаторы (часть 2)

Antoniuk Oleg | 26 декабря, 2007


Введение

Это пятая статья из цикла "Язык MQL4 для 'чайников'". Сегодня мы научимся использовать графические объекты - очень мощное средство разработки, которое позволяет существенно расширить возможности индикаторов. Кроме того, вы можете использовать их также в скриптах и советниках. Мы узнаем как создавать объекты, изменять их параметры, проверять ошибки. Конечно, мне не удастся описать полностью все объекты, их слишком много. Но вы получите все необходимые знания, чтобы разобраться в этом самостоятельно. Также в этой статье содержится пошаговое руководство-пример по созданию сложного сигнального индикатора. На основе последнего, вы сможете самостоятельно создавать любые сигнальные индикаторы, которые показывают торговые сигналы на всех периодах по нескольким индикаторам. При этом, многие параметры будут доступны пользователю для настройки, что позволит гибко изменять внешний вид.


Что такое графические объекты?

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

На этот график было добавлено 4 графических объекта:

Сегодня мы научимся добавлять такие объекты с помощью MQL4. Представьте себе, сколько рутинных действий вы можете автоматизировать, используя объекты! Например, вам когда-нибудь приходилось рассчитывать точку разворота, уровни поддержки и сопротивления, а потом вручную их рисовать? Да, работы там немного, но автоматизировав этот процесс на MQL4, терминал сам все рассчитает и нарисует соответствующие уровни. Все что вам нужно будет сделать – это дважды кликнуть по названию скрипта, чтобы все сделали за вас. Кроме того, используя графические объекты, можно писать очень полезные сигнальные индикаторы.

Концепция работы с объектами

В MQL4 с любыми графическими объектами работают примерно в такой последовательности:

Получается вот такой «жизненный цикл». Сейчас мы рассмотрим каждую из стадий более детально.

Создание графического объекта

Чтобы нарисовать любой графический объект используют одну универсальную функцию – ObjectCreate(). Вот ее прототип:

bool ObjectCreate(string name, int type, int window, datetime time1, 
                  double price1, datetime time2=0,double price2=0, 
                  datetime time3=0, double price3=0)

Функция возвращает true, если все нормально, и false, если объект не может быть создан и возникла ошибка. Чтобы узнать код ошибки, нужно использовать функцию GetLastError():

if(ObjectCreate(/* аргументы */)==false)
{
   // возникла ошибка, выведем код ошибки в журнал
   Print("Ошибка при вызове ObjectCreate():",GetLastError());
}

Зачем нужен код ошибки? По нему вы сможете найти описание ошибки и устранить ее при возможности. Все описания кодов находятся: Справочник MQL4 -> Стандартные константы -> Коды ошибок.

Рассмотрим все аргументы функции ObjectCreate():


Пример создания объектов. Рисуем линии

Теперь, чтобы хорошенько разобраться, нарисуем парочку линий. Отметим максимальную и минимальную цену за последний день. Для этого создадим новый скрипт и изменим функцию start():

int  start()
{
   double price=iHigh(Symbol(),PERIOD_D1,0);
   // эта полезная функция возвращает максимальную цену на:
   // * указанном финансовом инструменте, у нас - это Symbol() - 
   //   активный финансовый инструмент
   // * указанном периоде, у нас - это PERIOD_D1 (дневной)
   // * указанном баре, у нас - это 0, последний бар
 
   ObjectCreate("highLine",OBJ_HLINE,0,0,price);
   // рассмотрим все параметры: 
   // "highLine" - уникальное название объекта
   // OBJ_HLINE - тип объекта, который соответствует горизонтальной линии
   // 0 - рисуем объект в главном окне (график цен)
   // 0 - координата по оси Х (время), так как мы выводим горизонтальную
   //     линию, то указывать эту координату не нужно
   // price - координата по оси Y (цена). У нас это максимальная цена
   
   price=iLow(Symbol(),PERIOD_D1,0);
   // функция по аргументам полностью совпадает с iHigh, но возвращает
   // минимальную цену
   
   ObjectCreate("lowLine",OBJ_HLINE,0,0,price);
 
   return(0);
}

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

Линии нарисованы, но есть нечто, что мне совершенно не нравится! Этот насыщенный красный, посмотрите, просто ужасный цвет, всегда используйте только оттенки. Шутка. Конечно, вам еще много чего может не понравиться. Например, толщина линий, их стиль.

Изменение свойств объекта. Настраиваем внешний вид линий

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

bool ObjectSet( string name, int index, double value);

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

Теперь давайте изменим наши линии, а именно, цвет, толщину и стиль. Измените функцию start() того же скрипта:

int  start()
{
   double price=iHigh(Symbol(),PERIOD_D1,0);
 
   ObjectCreate("highLine",OBJ_HLINE,0,0,price);
   price=iLow(Symbol(),PERIOD_D1,0);
   ObjectCreate("lowLine",OBJ_HLINE,0,0,price);
   
   ObjectSet("highLine",OBJPROP_COLOR,LimeGreen);
   // изменяем цвет верхней линии
   ObjectSet("highLine",OBJPROP_WIDTH,3);
   // теперь линия будет толщиной в 3 пикселя
   
   ObjectSet("lowLine",OBJPROP_COLOR,Crimson);
   // изменяем цвет нижней линии
   ObjectSet("lowLine",OBJPROP_STYLE,STYLE_DOT);
   // теперь нижняя линия будет пунктирной   
 
   return(0);
}

На графике вы должны увидеть что-то похожее на это:


Удаление объектов

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

bool ObjectDelete(string name);

Эта функция удаляет объект с указанным названием. Если вы укажите название объекта, который существует лишь в вашем воображении, то вам будет возвращено false.

int ObjectsDeleteAll(int window=EMPTY,int type=EMPTY);

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

ObjectDeleteAll();
// удаляем все подряд

Если вы создали объекты в подокне (например, в окне какого-то индикатора), то указав в первом аргументе номер подокна, можно полностью избавится от объектов в нем. Сейчас указывайте в первом аргументе всегда 0, так как с подокнами мы разберемся позже.

Если вам нужно удалить все объекты определенного типа, то укажите во втором аргументе тип объектов-смертников:

ObjectsDeleteAll(0, OBJ_ARROW);
// удаляем все стрелочки

Как этим всем правильно пользоваться?

Вы можете подумать, что нужно много знать, чтобы разбираться в чем-то подобном. Например, что нужно выучить все эти свойства и типы объектов, и держать их в голове. Чушь! Сейчас я объясню вам, как вы должны писать подобный код, при этом ничего не зная, ведь все есть в справке!

Смотрите. Для начала откройте Инструментарий (CTRL+T). Там внизу есть много закладок, выберите Справка. Теперь представим, что вы хотите нарисовать какой-то новый графический объект, но пока не умеете этого делать. Для этого вы используете функцию ObjectCreate(), напишите ее, аргументы пока пропустите. Теперь курсор подведите внутрь названия функции и нажмите F1. Вуаля! Посмотрите, в справке отобразилась информация об этой функции. То есть даже ничего искать не нужно. Теперь посмотрите на описание функции. После него следует описание всех аргументов, как в этом уроке. Обратите внимание на описание аргумента type:

Да, это обычная ссылка. Просто кликаем по ней и смотрим, какие вообще существуют графические объекты. Допустим, вам понравился эллипс:

Внимательно читаем описание: нам пишут, что нужно 2 координаты. Приступаем:

int  start()
{
   ObjectCreate("ellipse",OBJ_ELLIPSE,0,Time[100],Low[100],Time[0],High[0]);
   // указываем 2 точки для создания эллипса:
   // * певрая - левая нижняя точка
   // * вторая - правая верхняя 
 
   return(0);
}
Также сказано, что свойство OBJPROP_SCALE устанавливает соотношение сторон, поэтому если выставить его в единицу, то получится обычный круг:

int  start()
{
   ObjectsDeleteAll();
   // очистим график перед рисованием
   
   ObjectCreate("ellipse",OBJ_ELLIPSE,0,Time[100],Low[100],Time[0],High[0]);
   
   ObjectSet("ellipse",OBJPROP_SCALE,1.0);
   // изменяем соотношение сторон
   ObjectSet("ellipse",OBJPROP_COLOR,Gold);
   // заодно и цвет поменяем
 
   return(0);
}

Уверен, что такого круга у вас не получилось, так как сначала нужно выставить единичный масштаб в свойствах графика (Нажимаем правую кнопку мыши на любом пустом месте графика и выбираем Свойства):



Как видите все достаточно просто. Важно отметить, что вы можете подвести курсор на любое ключевое слово в коде и нажать F1, после чего вы будете автоматически перемещены на соответствующую статью в справке. Таким образом, вы можете ничего не зная о названиях констант типов и свойств, все равно, быстро и эффективно писать код, используя встроенную справку. В MetaEditor-е есть еще одно важное свойство, которое упрощает написание кода: когда прописываете аргументы в какой-то встроенной функции, нажмите комбинацию CTRL + SHIFT + пробел. После этого появится подсказка с прототипом функции, очень удобно и запоминать ничего не нужно:

Создание графических объектов в подокнах

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

//+------------------------------------------------------------------+
//|                                   creatingObjectsInSubWindow.mq4 |
//|                                                     Antonuk Oleg |
//|                                            antonukoleg@gmail.com |
//+------------------------------------------------------------------+
#property copyright "Antonuk Oleg"
#property link      "antonukoleg@gmail.com"
 
#property indicator_separate_window
// индикатор будет рисоваться в отдельном подокне
#property indicator_minimum 1
// минимальное значение индикатора равно 1
#property indicator_maximum 10
// максимальное - 10
 
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int init()
{
   IndicatorShortName("NiceLine");
   // эта простая функция устанавливает короткое название индиктора,
   // то, которое вы видите в левом верхнем углу любого индикатора.
   // Зачем это нужно? Дело в том, что функция WindowFind ищет подокно
   // с указанным ей коротким названием и возвращает его номер.
 
   int windowIndex=WindowFind("NiceLine");
   // находим номер подокна нашего индикатора
   
   if(windowIndex<0)
   {
      // если номер подокна равен -1, то возникла ошибка
      Print("Can\'t find window");
      return(0);
   }  
 
   ObjectCreate("line",OBJ_HLINE,windowIndex,0,5.0);
   // рисуем линию в подокне нашего индикатора
               
   ObjectSet("line",OBJPROP_COLOR,GreenYellow);
   ObjectSet("line",OBJPROP_WIDTH,3);
 
   WindowRedraw();      
   // перерисовываем окно, чтобы увидить линиию
 
   return(0);
}
//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
int deinit()
{
   ObjectsDeleteAll();
   // удаляем все объекты
   
   return(0);
}
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int start()
{
   return(0);
}

Запустите индикатор. И о ужас, никакой линии нет!



Спокойно, поменяйте период графика.



Появилась… Что происходит? Дело в том, что узнать номер подокна в функции init(), когда она запускается первый раз индикатором, нельзя. Возможно, это связано с тем, что подокно еще не создано терминалом во время инициализации. Звучит умно, но как это исправить? Нужно просто все делать в фукции start(), когда окно уже создано, например так:

//+------------------------------------------------------------------+
//|                                   creatingObjectsInSubWindow.mq4 |
//|                                                     Antonuk Oleg |
//|                                            antonukoleg@gmail.com |
//+------------------------------------------------------------------+
#property copyright "Antonuk Oleg"
#property link      "antonukoleg@gmail.com"
 
#property indicator_separate_window
#property indicator_minimum 1
#property indicator_maximum 10
 
bool initFinished=false;
// добавляем переменную, которая будет запоминать состояние инициализации.
// false - инициализации еще не было
// true - была
 
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int init()
{
   return(0);
}
//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
int deinit()
{
   ObjectsDeleteAll();
   // удаляем все объекты
   
   return(0);
}
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int start()
{
   if(initFinished==false)
   {
      IndicatorShortName("NiceLine");
 
      int windowIndex=WindowFind("NiceLine");
   
      if(windowIndex<0)
      {
         // если номер подокна равен -1, то возникла ошибка
         Print("Can\'t find window");
         return(0);
      }  
 
      ObjectCreate("line",OBJ_HLINE,windowIndex,0,5.0);
      // рисуем линию в подокне нашего индикатора
               
      ObjectSet("line",OBJPROP_COLOR,GreenYellow);
      ObjectSet("line",OBJPROP_WIDTH,3);
 
      WindowRedraw();      
      // перерисовываем окно, чтобы увидить линиию   
      
      initFinished=true;
      // рисование закончено
   }
   
   return(0);
}

Теперь все будет рисоваться с первого раза. Из этого всего вы должны запомнить, что номер подокна нужно узнавать в функции start(), а не init().

Потренируйтесь

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


Пишем сигнальный индикатор. А что это такое?

Представьте себе ситуацию. Трейдер использует несколько индикаторов для принятия решений о входе на рынок: Moving Average, Parabolic SAR и Williams’ Percent Range. Это все встроенные в терминал индикаторы, которые вместе выглядят примерно так:



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

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

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


Основа

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



Мы видим обычную текстовую метку «Х». Если зайти в свойства этого объекта, то вы увидите, что координаты задаются в пикселях. Пиксель – это мельчайшая точка на экране. Обратите внимание, что левый верхний угол окна имеет координаты x=0 и y=0 (0,0). Если увеличивать x, то объект будет двигаться вправо, если уменьшать – влево. Аналогично с координатой y. Если ее увеличивать, то объект будет двигаться вниз, а если уменьшать – вверх. Важно понять и запомнить этот принцип. Чтобы быстрее разобраться, создайте одну метку, передвигайте ее и смотрите, как изменяются координаты в свойствах. Также посмотрите старые котировки, пролистав график. Обратите внимание, что при этом метка неподвижна. Используя такие метки как основу, мы можем создать сигнальный индикатор, который будет избавлен от всех недостатков, о которых писалось выше.


Возможности текстовой метки

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

int init()
{
   // сейчас мы создадим текстовую метку.
   // для этого, как всегда, используем функцию ObjectCreate.
   // координаты указывать не нужно
   ObjectCreate("signal",OBJ_LABEL,0,0,0,0,0);
 
   // изменяем координату х
   ObjectSet("signal",OBJPROP_XDISTANCE,50);
 
   // изменяем координату у
   ObjectSet("signal",OBJPROP_YDISTANCE,50);
 
   // чтобы указать текст метки, используем эту функцию
   ObjectSetText("signal","lambada",14,"Tahoma",Gold);
   // "signal" - название объекта
   // "lambada" - текст метки
   // 14 - размер шрифта
   // Gold - цвет
 
   return(0);
}

Как видите, все довольно просто. Функцию ObjectCreate() мы будем использовать только при инициализации для создания всех необходимых объектов. А с помощью ObjectSetText() будем изменять внешний вид объектов при каждом изменении цены в функции start(). Также сейчас нужно изменить функцию deinit():

int deinit()
{
   // при удалении нашего индикатора, удаляем все объекты
   ObjectsDeleteAll();
 
   return(0);
}

Теперь запустите индикатор и посмотрите на результат:

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



Используем шрифт Wingdings

Сейчас давайте создадим метки с использованием шрифта Wingdings. Изменим функцию start():

int init()
{
 
   ObjectCreate("signal",OBJ_LABEL,0,0,0,0,0);
   ObjectSet("signal",OBJPROP_XDISTANCE,50);
   ObjectSet("signal",OBJPROP_YDISTANCE,50);
 
   // используем значки из шрифта Wingdings
   ObjectSetText("signal","lambada",14,"Tahoma",Gold);
   // CharToString() - эта функция возвращает строку с единственным
   // символом, код которого вы указываете в единственном аргументе.
   // Просто выберите понравившийся вам значок из таблички выше и
   // впишите его номер в этой функции
   // 60 - используем крупный шрифт
   // "Wingdings" - используем шрифт Wingdings
 
   return(0);
}

Смотрим на результат:


Рисуем макет таблицы сигналов

Сейчас давайте нарисуем макет таблицы сигналов. Это будет просто куча квадратиков:

int init()
{
   // используем 2 цикла. Первый цикл, со счетчиком "x" рисует поочередно
   // каждый столбец слева направо. Второй цикл рисует значки каждого
   // столбца сверху вниз. На каждой итерации цикла будет создавать по метке.
   // Эти 2 цикла создают 9 столбцов (9 периодов) по 3 метки (3 типа сигналов)
   // в каждом.
   for(int x=0;x<9;x++)
      for(int y=0;y<3;y++)
      {
         ObjectCreate("signal"+x+y,OBJ_LABEL,0,0,0,0,0);
         // создаем очередную метку. Обратите внимание, что название метки
         // создается "на лету" и зависит от счетчиков "x" и "y"
 
         ObjectSet("signal"+x+y,OBJPROP_XDISTANCE,x*20);
         // изменяем координату Х.
         // x*20 - каждая метка создается с интервалом в 20 пикселей по
         // горизонтали и напрямую зависит от счетчика "x"
 
         ObjectSet("signal"+x+y,OBJPROP_YDISTANCE,y*20);
         // изменяем координату Y.
         // y*20 - каждая метка создается с интервалом в 20 пикселей по
         // вертикали и напрямую зависит от счетчика "y"
 
         ObjectSetText("signal"+x+y,CharToStr(110),20,"Wingdings",Gold);
         // используем 110-й код символа, который соответствует квадрату
      }
   
   return(0);
}



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

int init()
{
   for(int x=0;x<9;x++)
      for(int y=0;y<3;y++)
      {
         ObjectCreate("signal"+x+y,OBJ_LABEL,0,0,0,0,0);
         ObjectSet("signal"+x+y,OBJPROP_XDISTANCE,x*20+12);
         // добавим горизонтальный отступ в 12 пикселей
         ObjectSet("signal"+x+y,OBJPROP_YDISTANCE,y*20+20);
         // добавим вертикальный отступ в 20 пикселей
         ObjectSetText("signal"+x+y,CharToStr(110),20,"Wingdings",Gold);
      }
 
   return(0);
}



Оживляем макет

Теперь давайте заставим работать хотя бы один из квадратиков. Допустим, верхний левый будет показывать сигнал скользящих средних на минутном таймфрейме (M1). Если сигнал на покупку, то квадратик будем закрашивать в зеленый цвет, если на продажу, то в красный. Для этого нужно изменить функцию start(), поехали:

int start()
{
   // если быстрая скользящая средняя (период - 13) больше чем медленная,
   // то это сигнал на покупку. Проверяем последний бар
   if(iMA(Symbol(),1,13,0,0,0,0)>iMA(Symbol(),1,24,0,0,0,0))
      ObjectSetText("signal00",CharToStr(110),20,"Wingdings",YellowGreen);
   // изменяем цвет метки с названием "signal00" (самая левая верхняя)
   // на зеленый
 
   else
   // иначе, если быстрая средняя меньше чем медленная, то это сигнал на продажу
      ObjectSetText("signal00",CharToStr(110),20,"Wingdings",Tomato); 
      // изменяем цвет метки на красный
 
   return(0);
}



Оживляем верхний ряд

Продолжим оживление. Самому левому квадратику соответствует самый маленький таймфрейм – M1. Теперь давайте сделаем так, чтобы каждый следующий квадратик (по горизонтали) отвечал за более крупный таймфрейм. Так, например, второй квадрат будет показывать сигналы на M5, третий – на M15 и так далее до MN1. Конечно, все это мы будем делать в цикле. Подумайте сами. Ведь все что изменяется, это только название метки, к которой мы обращаемся и период. Квадратиков всего 9, поэтому используем один счетчик. Но тут возникает проблема с периодами, так как он изменяется без закономерности. Смотрите сами:


Первая мысль: цикл тут не прокатит. А вот и нет! Все что нужно сделать – это объявить специальный массив в самом начале кода индикатора, смотрите:

//////////////////////////////////////////////////////////////////////
//
//                                                  signalTable.mq4 
//                                                     Antonuk Oleg 
//                                            antonukoleg@gmail.com 
//
//////////////////////////////////////////////////////////////////////
#property copyright "Antonuk Oleg"
#property link      "antonukoleg@gmail.com"
 
#property indicator_chart_window
 
int period[]={1,5,15,30,60,240,1440,10080,43200};

Все периоды записаны в массиве, теперь их можно без проблем использовать в цикле:

int start()
{
   // используем цикл, чтобы оживить все квадратики первой линии
   for(int x=0;x<9;x++)
   {
      if(iMA(Symbol(),period[x],13,0,0,0,0)>iMA(Symbol(),period[x],24,0,0,0,0))
         ObjectSetText("signal"+x+"0",CharToStr(110),20,"Wingdings",YellowGreen);
         // "signal"+x+"0" - создаем название метки динамически в зависимости от
         // счетчика "х"
      else
         ObjectSetText("signal"+x+"0",CharToStr(110),20,"Wingdings",Tomato); 
   }
 
   return(0);
}

Мы используем массив period[] как таблицу соответствия значения счетчика «х» и периода. Представьте себе, сколько кода пришлось бы написать, если бы не этот маленький массивчик! Смотрим на результат, первый ряд сигнальных квадратиков готов:





Добавляем надписи

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

#property indicator_chart_window
 
int period[]={1,5,15,30,60,240,1440,10080,43200};  
 
string periodString[]={"M1","M5","M15","M30","H1","H4","D1","W1","MN1"};

Надписи будем создавать в init() используя цикл:

int init()
{
   for(int x=0;x<9;x++)
      for(int y=0;y<3;y++)
      {
         ObjectCreate("signal"+x+y,OBJ_LABEL,0,0,0,0,0);
         ObjectSet("signal"+x+y,OBJPROP_XDISTANCE,x*20+12);
         ObjectSet("signal"+x+y,OBJPROP_YDISTANCE,y*20+20);
         ObjectSetText("signal"+x+y,CharToStr(110),20,"Wingdings",Gold);
      }
 
   // создаем надписи периодов слева направо
   for(x=0;x<9;x++)
   {
      // все как обычно
      ObjectCreate("textPeriod"+x,OBJ_LABEL,0,0,0,0,0);
      ObjectSet("textPeriod"+x,OBJPROP_XDISTANCE,x*20+12);
      ObjectSet("textPeriod"+x,OBJPROP_YDISTANCE,10);
      ObjectSetText("textPeriod"+x,periodString[x],8,"Tahoma",Gold);
      // используем массив periodString[], чтобы указать надписи
   }
   
   return(0);
}




Добавим немного параметров

Пора сделать индикатор более гибким, добавить парочку параметров, чтобы пользователь мог сам настраивать внешний вид индикатора:

#property copyright "Antonuk Oleg"
#property link      "antonukoleg@gmail.com"
 
#property indicator_chart_window
 
extern int scaleX=20, // горизонтальный интервал, с которым создаются квадратики
           scaleY=20, // вертикальный интервал
           offsetX=35, // горизонтальный отступ всех квадратиков
           offsetY=20, // вертикальный отступ
           fontSize=20; // размер шрифта
           
int period[]={1,5,15,30,60,240,1440,10080,43200};
string periodString[]={"M1","M5","M15","M30","H1","H4","D1","W1","MN1"};

Также нужно изменить код функций init() и start(), чтобы все работало:

int init()
{
   for(int x=0;x<9;x++)
      for(int y=0;y<3;y++)
      {
         ObjectCreate("signal"+x+y,OBJ_LABEL,0,0,0,0,0);
         ObjectSet("signal"+x+y,OBJPROP_XDISTANCE,x*scaleX+offsetX);
         ObjectSet("signal"+x+y,OBJPROP_YDISTANCE,y*scaleY+offsetY);
         ObjectSetText("signal"+x+y,CharToStr(110),fontSize,"Wingdings",Gold);
      }
 
   for(x=0;x<9;x++)
   {
      ObjectCreate("textPeriod"+x,OBJ_LABEL,0,0,0,0,0);
      ObjectSet("textPeriod"+x,OBJPROP_XDISTANCE,x*scaleX+offsetX);
      ObjectSet("textPeriod"+x,OBJPROP_YDISTANCE,offsetY-10);
      ObjectSetText("textPeriod"+x,periodString[x],8,"Tahoma",Gold);
   }
   
   return(0);
}
 
int start()
{
   for(int x=0;x<9;x++)
   {
      if(iMA(Symbol(),period[x],13,0,0,0,0)>iMA(Symbol(),period[x],24,0,0,0,0))
         ObjectSetText("signal"+x+"0",CharToStr(110),fontSize,"Wingdings",YellowGreen);
      else
         ObjectSetText("signal"+x+"0",CharToStr(110),fontSize,"Wingdings",Tomato); 
   }
 
   return(0);
}

Оживляем остальные ряды

Второй ряд у нас будет отвечать за сигналы от Williams’ Percent Range, а третий – от Parabolic SAR. Изменяем функцию start():

int start()
{
   for(int x=0;x<9;x++)
   {
      if(iMA(Symbol(),period[x],13,0,0,0,0)>iMA(Symbol(),period[x],24,0,0,0,0))
         ObjectSetText("signal"+x+"0",CharToStr(110),fontSize,"Wingdings",YellowGreen);
      else
         ObjectSetText("signal"+x+"0",CharToStr(110),fontSize,"Wingdings",Tomato); 
   }
 
   // оживляем второй ряд
   for(x=0;x<9;x++)
   {
      // если абсолютное значение WPR меньше 20, то это сигнал на покупку
      if(MathAbs(iWPR(Symbol(),period[x],13,0))<20.0)
         ObjectSetText("signal"+x+"1",CharToStr(110),fontSize,"Wingdings",YellowGreen);   
      // если абсолютное значение WPR больше 80, то это сигнал на продажу
      else if(MathAbs(iWPR(Symbol(),period[x],13,0))>80.0)
         ObjectSetText("signal"+x+"1",CharToStr(110),fontSize,"Wingdings",Tomato);   
      // иначе, если нет сигналов, то закрашиваем квадратик в серый цвет
      else
         ObjectSetText("signal"+x+"1",CharToStr(110),fontSize,"Wingdings",DarkGray);      
   }
 
   // оживляем третий ряд
   for(x=0;x<9;x++)
   {
      // если текущая цена больше значения SAR, то это сигнал на покупку
      if(iSAR(Symbol(),period[x],0.02,0.2,0)<Close[0])
         ObjectSetText("signal"+x+"2",CharToStr(110),fontSize,"Wingdings",YellowGreen);
      // иначе, это сигнал на продажу
      else
         ObjectSetText("signal"+x+"2",CharToStr(110),fontSize,"Wingdings",Tomato);
   }
 
   return(0);
}




Добавляем названия сигналов

Пришло время подписать каждый ряд. Создадим 3 надписи слева, используя массив, как и раньше:

int period[]={1,5,15,30,60,240,1440,10080,43200};
string periodString[]={"M1","M5","M15","M30","H1","H4","D1","W1","MN1"},
       // создаем еще один массив с названиями индикаторов
       signalNameString[]={"MA","WPR","SAR"};

Изменяем функцию init():

int init()
{
   for(int x=0;x<9;x++)
      for(int y=0;y<3;y++)
      {
         ObjectCreate("signal"+x+y,OBJ_LABEL,0,0,0,0,0);
         ObjectSet("signal"+x+y,OBJPROP_XDISTANCE,x*scaleX+offsetX);
         ObjectSet("signal"+x+y,OBJPROP_YDISTANCE,y*scaleY+offsetY);
         ObjectSetText("signal"+x+y,CharToStr(110),fontSize,"Wingdings",Gold);
      }
 
   for(x=0;x<9;x++)
   {
      ObjectCreate("textPeriod"+x,OBJ_LABEL,0,0,0,0,0);
      ObjectSet("textPeriod"+x,OBJPROP_XDISTANCE,x*scaleX+offsetX);
      ObjectSet("textPeriod"+x,OBJPROP_YDISTANCE,offsetY-10);
      ObjectSetText("textPeriod"+x,periodString[x],8,"Tahoma",Gold);
   }
   
   // рисуем названия сигналов сверху вниз
   for(y=0;x<3;y++)
   {
      ObjectCreate("textSignal"+y,OBJ_LABEL,0,0,0,0,0);
      ObjectSet("textSignal"+y,OBJPROP_XDISTANCE,offsetX-25);
      ObjectSet("textSignal"+y,OBJPROP_YDISTANCE,y*scaleY+offsetY+8);
      ObjectSetText("textSignal"+y,signalNameString[y],8,"Tahoma",Gold);
   }
   
   return(0);
}




Добавляем возможность изменять угол привязки

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

Поэтому мы добавляем новый параметр – corner:

#property indicator_chart_window
 
extern int scaleX=20,
           scaleY=20, 
           offsetX=35, 
           offsetY=20, 
           fontSize=20,
           corner=0; // добавляем параметр для выбора угла привязки

Осталось изменить функцию init():

int init()
{
   // таблица сигналов
   for(int x=0;x<9;x++)
      for(int y=0;y<3;y++)
      {
         ObjectCreate("signal"+x+y,OBJ_LABEL,0,0,0,0,0);
         ObjectSet("signal"+x+y,OBJPROP_CORNER,corner);
         // изменяем угол привязки
         ObjectSet("signal"+x+y,OBJPROP_XDISTANCE,x*scaleX+offsetX);
         ObjectSet("signal"+x+y,OBJPROP_YDISTANCE,y*scaleY+offsetY);
         ObjectSetText("signal"+x+y,CharToStr(110),fontSize,"Wingdings",Gold);
      }
 
   // названия таймфреймов
   for(x=0;x<9;x++)
   {
      ObjectCreate("textPeriod"+x,OBJ_LABEL,0,0,0,0,0);
      ObjectSet("textPeriod"+x,OBJPROP_CORNER,corner);
      // изменяем угол привязки
      ObjectSet("textPeriod"+x,OBJPROP_XDISTANCE,x*scaleX+offsetX);
      ObjectSet("textPeriod"+x,OBJPROP_YDISTANCE,offsetY-10);
      ObjectSetText("textPeriod"+x,periodString[x],8,"Tahoma",Gold);
   }
 
   // названия индикаторов
   for(y=0;x<3;y++)
   {
      ObjectCreate("textSignal"+y,OBJ_LABEL,0,0,0,0,0);
          ObjectSet("textSignal"+y,OBJPROP_CORNER,corner);
      // изменяем угол привязки
      ObjectSet("textSignal"+y,OBJPROP_XDISTANCE,offsetX-25);
      ObjectSet("textSignal"+y,OBJPROP_YDISTANCE,y*scaleY+offsetY+8);
      ObjectSetText("textSignal"+y,signalNameString[y],8,"Tahoma",Gold);
   }
   
   return(0);
}

Добавляем новые параметры

Осталось добавить еще несколько параметров для гибкой настройки внешнего вида нашего индикатора. Вынесем в параметры:

Сначала объявим все эти параметры в начале кода:

extern int scaleX=20,
           scaleY=20,
           offsetX=35,
           offsetY=20,
           fontSize=20,
           corner=0,
           symbolCodeBuy=110, // код символа для сигнала на покупку
           symbolCodeSell=110, // на продажу
           symbolCodeNoSignal=110; // если сигнала нет
           
extern color signalBuyColor=YellowGreen, // цвет смвола для сигнала на покупку
             signalSellColor=Tomato, // на продажу
             noSignalColor=DarkGray, // нет сигнала
             textColor=Gold; // цвет всех надписей

Изменяем функцию init():

int init()
{
   // таблица сигналов
   for(int x=0;x<9;x++)
      for(int y=0;y<3;y++)
      {
         ObjectCreate("signal"+x+y,OBJ_LABEL,0,0,0,0,0);
         ObjectSet("signal"+x+y,OBJPROP_CORNER,corner);
         ObjectSet("signal"+x+y,OBJPROP_XDISTANCE,x*scaleX+offsetX);
         ObjectSet("signal"+x+y,OBJPROP_YDISTANCE,y*scaleY+offsetY);
         ObjectSetText("signal"+x+y,CharToStr(symbolCodeNoSignal),
                       fontSize,"Wingdings",noSignalColor);
      }
 
   // названия таймфреймов
   for(x=0;x<9;x++)
   {
      ObjectCreate("textPeriod"+x,OBJ_LABEL,0,0,0,0,0);
      ObjectSet("textPeriod"+x,OBJPROP_CORNER,corner);
      ObjectSet("textPeriod"+x,OBJPROP_XDISTANCE,x*scaleX+offsetX);
      ObjectSet("textPeriod"+x,OBJPROP_YDISTANCE,offsetY-10);
      ObjectSetText("textPeriod"+x,periodString[x],8,"Tahoma",textColor);
   }
 
   // названия индикаторов
   for(y=0;x<3;y++)
   {
      ObjectCreate("textSignal"+y,OBJ_LABEL,0,0,0,0,0);
      ObjectSet("textSignal"+y,OBJPROP_CORNER,corner);
      ObjectSet("textSignal"+y,OBJPROP_XDISTANCE,offsetX-25);
      ObjectSet("textSignal"+y,OBJPROP_YDISTANCE,y*scaleY+offsetY+8);
      ObjectSetText("textSignal"+y,signalNameString[y],8,"Tahoma",textColor);
   }
   
   return(0);
}

Изменяем функцию start():

int start()
{
   for(int x=0;x<9;x++)
   {
      if(iMA(Symbol(),period[x],13,0,0,0,0)>iMA(Symbol(),period[x],24,0,0,0,0))
         ObjectSetText("signal"+x+"0",CharToStr(symbolCodeBuy),fontSize,
         "Wingdings",signalBuyColor);
      else
         ObjectSetText("signal"+x+"0",CharToStr(SymbolCodeSell),fontSize,
         "Wingdings",signalSellColor); 
   }
 
   for(x=0;x<9;x++)
   {
      if(MathAbs(iWPR(Symbol(),period[x],13,0))<20.0)
         ObjectSetText("signal"+x+"1",CharToStr(symbolCodeBuy),fontSize,
         "Wingdings",signalBuyColor);   
      else if(MathAbs(iWPR(Symbol(),period[x],13,0))>80.0)
         ObjectSetText("signal"+x+"1",CharToStr(symbolCodeSell),fontSize,
         "Wingdings",signalSellColor);   
      else
         ObjectSetText("signal"+x+"1",CharToStr(symbolCodeNoSignal),fontSize,
         "Wingdings",noSignalColor);      
   }
 
   for(x=0;x<9;x++)
   {
      if(iSAR(Symbol(),period[x],0.02,0.2,0)<Close[0])
         ObjectSetText("signal"+x+"2",CharToStr(symbolCodeBuy),fontSize,
         "Wingdings",signalBuyColor);
      else
         ObjectSetText("signal"+x+"2",CharToStr(symbolCodeSell),fontSize,
         "Wingdings",signalSellColor);
   }
 
   return(0);
}

Изменяем внешний вид

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

extern int scaleX=20,
           scaleY=20,
           offsetX=35,
           offsetY=20,
           fontSize=20,
           corner=2,
           symbolCodeBuy=67, 
           symbolCodeSell=68, 
           symbolCodeNoSignal=73; 
           
extern color signalBuyColor=Gold,
             signalSellColor=MediumPurple,
             noSignalColor=WhiteSmoke,
             textColor=Gold;



Домашнее задание

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


Заключение

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