Tag Archives: обучение

Нужны ли трейдеру индикаторы?

Вы уверены, что понимаете, чем являются индикаторы на самом деле? Даже некоторые «опытные» форекс-трейдеры не совсем это понимают. В этом легко убедиться, увидев их истеричные комментарии к индикаторным стратегиям. Да и тот факт, что индикаторы до сих пор продают (и покупают), тоже говорит о тотальном невежестве…

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

Индикаторы для Forex не нужны ? - Технический анализ для чайников 📈

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

Что такое индикаторы?

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

Полученная кривая или гистограмма значительно упрощает «хаос» движений котировок.

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

Цель торговых индикаторов – это обобщение исторических данных цены на графике, чтобы трейдеру было легче интерпретировать поведение рынков.

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

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

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

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

Мы имеем дело с ограниченным числом переменных: цена, время, иногда объемы.

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

Так каким же образом индикатор вам покажет что-то, чего нет на графике?

Типы индикаторов

На рынке принято делить индикаторы на три условные группы:

  • Трендовые – рисующие на графике кривые и ломаные линии, описывающие ценовые колебания, подсказывающие тренд;
  • Информационные – отображающие данные из внешних источников или вспомогательную статистику;
  • Осцилляторы –  определяют текущую позицию относительно какого-то диапазона.

Самый распространенный трендовый индикатор – скользящая средняя (Moving Average), которая преобразует колебания котировок в среднее значение цен закрытия свечей на заданном временном интервале.

Трейдер самостоятельно задает эти настройки при добавлении инструмента на график. Это можно сделать в торговом терминале Metatrader через меню «Вставка», выбрав раздел «Индикаторы», где содержатся более 50 инструментов. Moving Average находится в разделе Трендовые индикаторы.

Окно настроек открывается автоматически после выбора индикатора и содержит оптимальные параметры, уже установленные по умолчанию.

Равный 14 период означает, что каждая точка кривой на графике – это среднее значений 14 цен закрытия (Close) предшествующих свечей. Точка автоматически пересчитывается всякий раз при появлении новой котировки. Трейдер будет наблюдать постоянно «плавающий» конец кривой до завершения формирования свечи.

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

Объемы отображаются на графике Metatrader в виде гистограммы через меню правой кнопки мыши. В выпавшем списке выберите «Свойства», в открывшемся окне перейдите на вкладку «Общие» и поставьте галочку в пункте «Показывать объемы».  

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

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

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

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

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

Осцилляторы позволяют определить текущее положение цены относительно какого-либо диапазона, как правило, минимума и максимума периода индикатора. Кривая инструмента располагается в подвале графика, обычно не выходя за пределы значений 0 и 1 или 0 и 100.

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

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

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

Как лучше торговать – с индикаторами или на «чистом» графике?

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

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

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

Созданная на основе индикаторов система может быть запрограммирована в виде торгового робота, который выведет трейдинг на совершенно другой уровень. Сигналы пересечения Moving Average или зон осцилляторов может понять даже новичок. Они помогут ему найти однозначные точки входа/выхода и не дадут пропустить сделку профессионалу.

Некоторые сигналы, например, дивергенцию, невозможно увидеть невооруженным взглядом. Нельзя отказаться и от наблюдения за долгосрочным индикатором – скользящей с периодом 200 дней, ставшей «шаблоном» для институциональных инвесторов.

Как выбрать индикатор?

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

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

Пакет индикаторов в Metatrader представляет собой оптимальный вариант использования 4 видов ценовых данных свечи. Большинство других индикаторов – это производные классических формул. Трейдер сможет оценить удобства их вариаций после того, как наберется опыта работы с инструментами теханализа в Metatrader.

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

Каждый индикатор на графике должен иметь цель. Одна цель – один индикатор.

Пример классической трендовой торговой системы:

  • Скользящая средняя (MA) для определения направления торговли;
  • Осциллятор Stochastic для поиска точек выхода;
  • Точки Parabolic SAR для установки стоп-лосса, ограничивающего убытки.

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

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

Заключение

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

Некоторые индикаторы, такие как MACD, RSI или Moving Average больших периодов, широко распространены в торговых терминалах крупных игроков и аналитиков. Наблюдение за ними может дать трейдеру понимание шаблонов действий институциональных инвесторов.

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

P.S. Также не забывайте, что у нас на форуме в разделе «Классические индикаторы» представлено несколько тысяч модификаций всех самых известных и популярных индикаторов/осцилляторов и большинство из них с открытым кодом.


С уважением Павел Власов
Tlap.com

В помощь Трейдеру, Индикаторы Форекс, Новичкам , , , , ,

Форекс в Новогодние праздники

Близится Новый Год, а вместе с ним и долгожданные долгие выходные. Мысли о застолье, гуляниях и т. п. уже сейчас не дают офисным служащим спокойно спать на работе. А как же обстоят дела на форексе? Когда отключать советники и прекращать ручную торговлю?

Читать дальше

В помощь Трейдеру, Новичкам , ,

Лучший способ расчета стоп-лосса

О стоп-лоссах уже было сказано немало, но наши читатели продолжают присылать вопросы вроде «А стоп 30 пунктов для EURUSD на M15 – это нормально?».

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

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

Читать далее

В помощь Трейдеру, Новичкам , , , , ,

MQL4: Использование структур для повышения эффективности разработки программ

Здравствуйте, коллеги форекс-программисты!

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

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

MQL4 программирование - Структуры, улучшаем оптимизацию кода

Что такое структуры?

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

Синтаксис

Структурный тип данных в общем виде определяется так:

struct <имя_структуры>
  { 
   <тип> <элемент_1>;
   <тип> <элемент_2>;
   <тип> <элемент_3>;
    …
   <тип> <элемент_n>;
  };

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

  • цена открытия;
  • цена закрытия;
  • максимум;
  • минимум;
  • объём;
  • время открытия.

Другие характеристики, такие как тип свечи («медвежья» или «бычья»), высота тела свечи, размер верхней тени, размер нижней тени и прочие предполагается вычислять в реальном времени. Это не всегда удобно, особенно если надо часто обрабатывать такие параметры для целого массива свечей. Можно все расчёты делать в момент формирования новой свечи, а результаты хранить в переменных. Тогда для хранения указанных характеристик свечи нам понадобится 11 переменных разного типа:

double   open;                  // цена открытия
double   close;                 // цена закрытия
double   high;                  // максимум
double   low;                   // минимум
long     volume;                // тиковый объём
datetime time;                  // время открытия
uchar    type;                  // тип свечи (0-"дожи", 1-бычья, 2-медвежья)
int      height_full;           // общая высота свечи
int      height_body;           // размер тела свечи
int      height_top_shadow;     // размер верхней тени
int      height_bottom_shadow;  // размер нижней тени

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

struct Candle
{
 double   open;                  // цена открытия
 double   close;                 // цена закрытия
 double   high;                  // максимум
 double   low;                   // минимум
 long     volume;                // тиковый объём
 datetime time;                  // время открытия
 uchar    type;                  // тип свечи (0-"дожи", 1-бычья, 2-медвежья)
 int      height_full;           // общая высота свечи
 int      height_body;           // размер тела свечи
 int      height_top_shadow;     // размер верхней тени
 int      height_bottom_shadow;  // размер нижней тени
};

На этом этапе память под структуру не выделяется, так как мы только определили тип. Чтобы получить в распоряжение сам объект структуры, достаточно объявить переменную этого типа:

Candle Bar;

Доступ к элементам структуры

Для доступа к элементам структуры применяется операция принадлежности «точка» (.).  Например:

Bar.volume=iVolume(_Symbol,_Period,1)

Здесь мы присваиваем элементу volume переменной-структуры Bar значение тикового объёма бара с индексом 1 на текущем графике.

Инициализация структур

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

Candle Bar = {0,0,0,0,100,D'2018.03.10 15:08:02',0,0,0,0,12};

Здесь мы объявили переменную Bar и проинициализировали все элементы структуры с первый по одиннадцатый некими значениями.
А так

Candle Bar = {0.1012,0.6321,0.2194,0.1784,100};

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

Candle Bar = {};

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

Candle Bar = {100};

Строго говоря, в нашем случае такая инициализация некорректна, так как элемент time имеет тип datetime, а значение 100 – это сто секунд с первого января 1970 года. Поэтому при подобной инициализации структур необходимо учитывать особенности неявного приведения типов.

Вложенные структуры

Элементы структуры могут быть не только простых типов, но и сложных: строками, статическими и динамическими массивами и структурами. Вложенные структуры определяются отдельно. Покажем на конкретном примере, как можно организовать данные таким образом. В нашей структуре Candle последние четыре элемента содержат размеры как всей свечи, так и её отдельных частей. Можно по этому признаку объединить эти элементы в отдельную структуру:

struct Height
  {
   int               full;           // общая высота свечи
   int               body;           // размер тела свечи
   int               top_shadow;     // размер верхней тени
   int               bottom_shadow;  // размер нижней тени
  };

Тогда в структуре Candle достаточно будет объявить одну переменную типа Height:

struct Candle
  {
   double            open;                  // цена открытия
   double            close;                 // цена закрытия
   double            high;                  // максимум
   double            low;                   // минимум
   long              volume;                // тиковый объём
   datetime          time;                  // время открытия
   uchar             type;                  // тип свечи (0-"дожи", 1-бычья, 2-медвежья)
   Height            height;                // высоты частей свечи
  };

Список инициализаторов также должен содержать вложенный список:

Candle Bar=
  {
   0.0,
   0.0,
   0.0,
   0.0,
   100,
   D'2018.03.10 15:08:02',
   1,
     {
      0,
      0,
      0,
      0
     }
  };

Теперь, чтобы обратиться к размеру тела свечи, точку необходимо использовать дважды:

Bar.height.body = MathAbs(iOpen(_Symbol,_Period,1) - iClose(_Symbol,_Period,1));

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

Обмен данными между структурами

Переменной-структуре можно присвоить значение другой переменной этого же типа, используя оператор присваивания (=). Однако это можно сделать только в отношении структур, у которых все элементы имеют простой тип. В наших примерах структура Height имеет только целочисленные поля, значит, следующий код вполне корректен:

Heigh h1, h2={1,4,1,8};
h1=h2;

В результате значения всех полей переменной h2 скопируются в поля переменной h1.

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

   Candle Bar1, Bar2= {0.0,0.0,0.0,0.0,100,D'2018.03.10 15:08:02',1,{0,0,0,0}};
   Bar1.open                 = Bar2.open;                  // цена открытия
   Bar1.close                = Bar2.close;                 // цена закрытия
   Bar1.high                 = Bar2.high;                  // максимум
   Bar1.low                  = Bar2.low;                   // минимум
   Bar1.volume               = Bar2.volume;                // тиковый объём
   Bar1.time                 = Bar2.time;                  // время открытия
   Bar1.type                 = Bar2.type;                  // тип свечи (0-"дожи", 1-бычья, 2-медвежья)
   Bar1.height.full          = Bar2.height.full;           // общая высота свечи
   Bar1.height.body          = Bar2.height.body;           // размер тела свечи
   Bar1.height.top_shadow    = Bar2.height.top_shadow;     // размер верхней тени
   Bar1.height.bottom_shadow = Bar2.height.bottom_shadow;  // размер нижней тени

Структуры и функции

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

// структура, содержащая координаты точки на плоскости
struct Coordinates
{
 int   x; 
 int   y;
};
// зададим точку с координатами (x=120,y=35)
Coordinates P={120,35};
//+------------------------------------------------------------------+
//| Смещает координаты точки на:                                     |
//| shift_x по горизонтали                                           |
//| shift_y по вертикали                                             |
//+------------------------------------------------------------------+
void MovePoint(Coordinates &point, int shift_x=0, int shift_y=0)
{
  point.x+=shift_x;
  point.y+=shift_y; 
}
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   Print("x=",P.x,"; y=",P.y);
   MovePoint(P,2,15);
   Print("x=",P.x,"; y=",P.y);
   return(INIT_PARAMETERS_INCORRECT);
  }

Результат:

P.x=122; P.y=50
P.x=120; P.y=35

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

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

//+------------------------------------------------------------------+
//| Смещает координаты точки на:                                     |
//| shift_x по горизонтали                                           |
//| shift_y по вертикали                                             |
//| и возвращает структуру с новыми координатами                     |
//+------------------------------------------------------------------+
Coordinates MovePoint(Coordinates &point, int shift_x=0, int shift_y=0)
{
  Coordinates p;
  p.x=point.x+shift_x;
  p.y=point.y+shift_y; 
  return(p);
}
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   Print("P.x=",P.x,"; P.y=",P.y);
   Coordinates MovedPoint=MovePoint(P,2,15);
   Print("MovedPoint.x=",MovedPoint.x,"; MovedPoint.y=",MovedPoint.y);
   Print("P.x=",P.x,"; P.y=",P.y);
 
  return(INIT_PARAMETERS_INCORRECT);
  }

Результат:

P.x=120; P.y=35
MovedPoint.x=122; MovedPoint.y=50
P.x=120; P.y=35

Мы видим, что функция возвратила в переменную MovedPoint рассчитанные значения, но при этом переменная P, переданная в функцию в качестве аргумента, не изменилась. Надо заметить, что возвращать можно только простые структуры, не содержащие массивы или вложенные структуры.

Методы

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

Давайте в нашей структуре Candle определим метод, который будет получать время открытия свечи, искать её в массиве-таймсерии и заполнять поля структуры данными о найденной свече.

struct Height
  {
   int               full;           // общая высота свечи
   int               body;           // размер тела свечи
   int               top_shadow;     // размер верхней тени
   int               bottom_shadow;  // размер нижней тени
  };
struct Candle
  {
   double            open;                  // цена открытия
   double            close;                 // цена закрытия
   double            high;                  // максимум
   double            low;                   // минимум
   long              volume;                // тиковый объём
   datetime          time;                  // время открытия
   uchar             type;                  // тип свечи (0-"дожи", 1-бычья, 2-медвежья)
   Height            height;                // высоты частей свечи

   void              GetCandleParam(datetime open_time)
     {
      // получаем индекс свечи по времени или индекс ближайшей свечи
      int index=iBarShift(_Symbol,_Period,open_time);
      
      // рассчитываем поля структуры
      open                 = iOpen(_Symbol,_Period,index);                  // цена открытия
      close                = iClose(_Symbol,_Period,index);                 // цена закрытия
      high                 = iHigh(_Symbol,_Period,index);                  // максимум
      low                  = iLow(_Symbol,_Period,index);                   // минимум
      volume               = iVolume(_Symbol,_Period,index);                // тиковый объём
      time                 = iTime(_Symbol,_Period,index);                  // время открытия
      height.full          = int((high-low)/_Point);                        // общая высота свечи
      height.body          = int((close-open)/_Point);                      // размер тела свечи
      type                 = height.body>0?1:(height.body<0?2:0);           // тип свечи (0-"дожи", 1-бычья, 2-медвежья)
      height.top_shadow    = int((high-((type<2)?close:open))/_Point);      // размер верхней тени
      height.bottom_shadow = int((((type<2)?open:close)-low)/_Point);       // размер нижней тени
      height.body          = MathAbs(height.body);
     }
  } Bar; // сразу объявим переменную-структуру
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   Bar.GetCandleParam(D'2020.10.02 10:00');
   Print("Цена открытия свечи: ",DoubleToStr(Bar.open,_Digits));
   Print("Цена закрытия свечи: ",DoubleToStr(Bar.close,_Digits));
   Print("Тип свечи: ",Bar.type==0?"дожи":(Bar.type==1?"бычья":"медвежья"));
   Print("Высота свечи: ", IntegerToString(Bar.height.full));
   Print("Время: ", Bar.time);   return(INIT_PARAMETERS_INCORRECT);
}

Спецификаторы доступа

Для того, чтобы ограничить доступ к полям и методам структуры, применяются спецификаторы доступа: private, protected и public. По умолчанию все поля и методы структуры имеют модификатор public, то есть к ним можно обращаться извне. Если не используется наследование, то спецификаторы protected и private равнозначны.

Все поля и методы, объявленные после private: и до public:, доступны только методам структуры.

struct Candle
  {
private:
   double            open;                  // цена открытия
   double            close;                 // цена закрытия
   double            high;                  // максимум
   double            low;                   // минимум
   long              volume;                // тиковый объём
   datetime          time;                  // время открытия
public:
  uchar             type;                  // тип свечи (0-"дожи", 1-бычья, 2-медвежья)
  Height            height;                // высоты частей свечи
  void              GetCandleParam(datetime open_time)
     {
      // получаем индекс свечи по времени или индекс ближайшей свечи
      int index=iBarShift(_Symbol,_Period,open_time);
      
      // рассчитываем поля структуры
      open                 = iOpen(_Symbol,_Period,index);                  // цена открытия
      close                = iClose(_Symbol,_Period,index);                 // цена закрытия
      high                 = iHigh(_Symbol,_Period,index);                  // максимум
      low                  = iLow(_Symbol,_Period,index);                   // минимум
      volume               = iVolume(_Symbol,_Period,index);                // тиковый объём
      time                 = iTime(_Symbol,_Period,index);                  // время открытия
      height.full          = int((high-low)/_Point);                        // общая высота свечи
      height.body          = int((close-open)/_Point);                      // размер тела свечи
      type                 = height.body>0?1:(height.body<0?2:0);           // тип свечи (0-"дожи", 1-бычья, 2-медвежья)
      height.top_shadow    = int((high-((type<2)?close:open))/_Point);      // размер верхней тени
      height.bottom_shadow = int((((type<2)?open:close)-low)/_Point);       // размер нижней тени
      height.body          = MathAbs(height.body);
     }
  }

В этом примере поля структуры open, close, high, volume, time доступны только из метода GetCandleParam().

Пишем советник с использованием структур

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

Алгоритм советника

Алгоритм нашего советника в общем виде будет выглядеть так:

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

Входные параметры

Исходя из алгоритма, входные параметры нашего советника будут такими:

input  double  Lots                = 0.01;     // Фиксированный лот
input  double  Multiplier          = 2.0;      // Множитель
input  int     MaxLegs             = 10;       // Максимальное количество колен
input  int     MinProfit           = 1.0;      // Минимальная прибыль
input  int     MinStep             = 100;      // Минимальное расстояние между ордерами(шаг)
input  int     Magic               = 1100;     // Magic Number (идентификатор ордеров)
input  int     Slippage            = 30;       // Максимально допустимое проскальзывание

Функции советника

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

//+------------------------------------------------------------------+
//| Новый бар на текущем таймфрейме                               |
//+------------------------------------------------------------------+
bool IsNewBar()
  {
   static datetime last_bar=0;
   datetime null_bar=iTime(_Symbol,_Period,0);
   if(last_bar!=null_bar)
     {
      last_bar=null_bar;
      return(true);
     }
   return(false);
  }

Далее напишем функцию, генерирующую торговый сигнал:

//+------------------------------------------------------------------+
//| Возвращает OP_BUY, если сформирован сигнал на покупку,           |
//|            OP_SELL, если сформирован сигнал на продажу и         |
//|            EMPTY, если сигнала на закрытом баре нет              |
//+------------------------------------------------------------------+
int GetSignal()
  {
   if(iClose(_Symbol,_Period,1)<iLow(_Symbol,_Period,2))
      return(OP_BUY);
   if(iHigh(_Symbol,_Period,2)<iClose(_Symbol,_Period,1))
      return(OP_SELL);
   return(EMPTY);
  }

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

//+------------------------------------------------------------------+
//| Возвращает тип первого найденного ордера, который был открыт     |
//| советником по текущему символу                                   |
//+------------------------------------------------------------------+
int GetOpenedOrdersType()
  {
   for(int i=OrdersTotal()-1; i>=0; i--)
      if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES))
         if(OrderSymbol()==_Symbol && OrderMagicNumber()==Magic)
            return(OrderType());
   return(EMPTY);
  }

Следующие две функции возвращают общее количество открытых советником ордеров и прибыль по этим ордерам соответственно:

//+------------------------------------------------------------------+
//| Возвращает общее количество открытых советником                  |
//| ордеров указанного типа по текущему символу                      |
//+------------------------------------------------------------------+
int GetOrdersCount(int type)
  {
   int count=0;
   for(int i=OrdersTotal()-1; i>=0; i--)
      if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES))
         if(OrderSymbol()==_Symbol && OrderMagicNumber()==Magic && OrderType()==type)
            count++;
   return(count);
  }

//+------------------------------------------------------------------+
//| Возвращает прибыль по всем ордерам, открытым советником          |
//+------------------------------------------------------------------+
double GetProfit()
  {
   double profit=0;
   for(int i=OrdersTotal()-1; i>=0; i--)
      if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES))
         if(OrderSymbol()==_Symbol && OrderMagicNumber()==Magic)
            profit+=OrderProfit()+OrderCommission()+OrderSwap();
   return(profit);
  }

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

//+------------------------------------------------------------------+
//| Возвращает цену открытия самого верхнего из                      |
//| открытых ордеров Sell в сетке                                    |
//+------------------------------------------------------------------+
double GetTopSellPrice()
  {
   double price=0;
   for(int i=OrdersTotal()-1; i>=0; i--)
      if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES))
         if(OrderSymbol()==_Symbol && OrderMagicNumber()==Magic && OrderType()==OP_SELL)
            if(price<OrderOpenPrice())
               price=OrderOpenPrice();
   return(price);
  }

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

//+------------------------------------------------------------------+
//| Возвращает цену открытия самого нижнего  из                      |
//| открытых ордеров Buy в сетке                                     |
//+------------------------------------------------------------------+
double GetBottomBuyPrice()
  {
   double price=0;
   for(int i=OrdersTotal()-1; i>=0; i--)
      if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES))
         if(OrderSymbol()==_Symbol && OrderMagicNumber()==Magic && OrderType()==OP_BUY)
            if(OrderOpenPrice()<price || price==0)
               price=OrderOpenPrice();
   return(price);
  }

Лот очередного ордера сетки будем вычислять с помощью следующей функции:

//+------------------------------------------------------------------+
//| Возвращает размер лота, рассчитанный как n-ый член               |
//| геометрической прогрессии                                        |
//+------------------------------------------------------------------+
double GetLot(int n)
  {
   return(NormalizeDouble(Lots*MathPow(MathMax(1,Multiplier),n),2));
  }

Также в советнике нам не обойтись без торговых функций. Первая будет открывать ордер указанным объёмом по текущей рыночной цене в указанном направлении.

//+------------------------------------------------------------------+
//| Открывает сделку лотом lot                                       |
//| в направлении com                                                |
//+------------------------------------------------------------------+
bool SetOrder(double lot, int com)
  {
   MqlTick tick;
   if(SymbolInfoTick(_Symbol,tick))
     {
      for(int i=0; i<5; i++)
        {
         if(OrderSend(_Symbol,com,lot,((com==OP_BUY)?tick.ask:tick.bid),Slippage,0,0,NULL,Magic,0,((com==OP_BUY)?clrBlue:clrRed))>EMPTY)
            return(true);
         Sleep(1000);
        }
     }
   return(false);
  }

Вторая – закрывать все открытые советником ордера:

//+------------------------------------------------------------------+
//| Закрывает все рыночные ордера, открытые советником               |
//| по текущему символу                                              |
//+------------------------------------------------------------------+
void CloseOrders()
  {
   for(int i=OrdersTotal()-1; i>=0; i--)
      if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES))
         if(OrderSymbol()==_Symbol && OrderMagicNumber()==Magic)
           {
            MqlTick tick;
            if(SymbolInfoTick(_Symbol,tick))
              {
               for(int j=0; j<5; j++)
                 {
                  if(OrderClose(OrderTicket(),OrderLots(),((OrderType()==OP_BUY)?tick.bid:tick.ask),Slippage,((OrderType()==OP_BUY)?clrBlue:clrRed)))
                     break;
                  Sleep(1000);
                 }
              }
           }
  }

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

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
// открываем новые ордера и фиксируем прибыль только на открытии нового бара
   if(IsNewBar())
     {
      // получаем новый сигнал
      int Signal=GetSignal();
      // сразу выходим из функции, если на закрытом баре не был сформирован сигнал
      if(Signal<0)
         return;

      // есть ли открытые советником ордера
      int OpenedOrderType=GetOpenedOrdersType();// (!!!)

      // есть открытые ордера и их не равен сигналу
      if(OpenedOrderType!=EMPTY && Signal!=OpenedOrderType)
        {
         // если есть прибыль закрываем ордера
         if(GetProfit()>MinProfit)// (!!!)
           {
            CloseOrders();
            OpenedOrderType=GetOpenedOrdersType();// (!!!)
           }
        }

      // нет открытых ордеров
      if(OpenedOrderType==EMPTY)
        {
         //открываем первый ордер сетки
         SetOrder(Lots,Signal);
         return;
        }

      // открываем усредняющие ордера по сигналу
      if(Signal==OpenedOrderType)
        {
         // рассчитываем количество открытых ордеров для расчёта следующего лота
         int Count=GetOrdersCount(OpenedOrderType);// (!!!)
         double Price;

         switch(OpenedOrderType)
           {
            case OP_BUY:
               Price=GetBottomBuyPrice();// (!!!)
               // проверка на минимальный шаг до самого верхнего ордера BUY
               if(int((Price-Ask)/_Point)>MinStep)
                 {
                  SetOrder(GetLot(Count),OP_BUY);
                 }
               break;

            // проверка на минимальный шаг до самого нижнего ордера SELL
            case OP_SELL:
               Price=GetTopSellPrice();// (!!!)

               if(int((Bid-Price)/_Point)>MinStep)
                 {
                  SetOrder(GetLot(Count),OP_SELL);
                 }
               break;
           }

        }
     }
  }

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

  • GetOpenedOrdersType();
  • GetOrdersCount();
  • GetProfit();
  • GetTopSellPrice();
  • GetBottomBuyPrice().

Вызовы этих функций для наглядности помечены комментарием (!!!). Все эти функции для расчётов перебирают в цикле открытые ордера, отбирая те, что открыты советником и соответствуют тем или иным условиям. В итоге мы многократно сканируем рыночную ситуацию, начиная перебор ордеров сначала с вызовом каждой функции. И это не искусственный пример – подобная архитектура советников встречается довольно часто. Больше всего времени мы тратим именно на обращение к серверу, поэтому возникает вопрос – что, если мы будем получать всю необходимую информацию за один проход? Ответом на этот вопрос будет использование структуры с набором элементов, в которые мы поместим все необходимые данные, перебрав все ордера в цикле только один раз:

struct state
  {
   int               buy_count;         // количество ордеров buy
   int               sell_count;        // количество ордеров sell
   double            buy_bottom_price;  // цена открытия самого нижнего ордера buy
   double            sell_top_price;    // цена открытия самого верхнего ордера sell
   double            profit;            // прибыль всех ордеров

   // метод для сбора информации о состоянии счёта
   // и обновления элементов структуры
   void              Refresh();
  } State;

В структуре присутствует метод Refresh(), который рассчитывает и присваивает значения элементам структуры. Определим тело метода вне структуры. Для этого используем операцию разрешения контекста (::). Контекст – это дескриптор (имя) структуры:

//+------------------------------------------------------------------+
//| Метод собирает информацию о текущем состоянии счёта              |
//| и обновляет соответствующие поля структуры                       |
//+------------------------------------------------------------------+
void state::Refresh(void)
  {
// обнуляем числовые поля структуры
   ZeroMemory(this);

   for(int i=OrdersTotal()-1; i>=0; i--)
      if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES))
         if(OrderSymbol()==_Symbol && OrderMagicNumber()==Magic)
           {
            double OpenPrice=OrderOpenPrice();
            profit+=OrderProfit()+OrderCommission()+OrderSwap();
            switch(OrderType())
              {
               case OP_BUY:
                  buy_count++;
                  if(OpenPrice<buy_bottom_price || buy_bottom_price==0)
                     buy_bottom_price=OpenPrice;
                  break;
               case OP_SELL:
                  sell_count++;
                  if(OpenPrice>sell_top_price || sell_top_price==0)
                     sell_top_price=OpenPrice;
                  break;
              }
           }

  }

Обратите внимание, что в теле метода мы обращаемся к элементам структуры, не используя точку, так как мы использовали операцию разрешения контекста. Числовые поля перед их обновлением в самом начале тела метода обнуляются функцией ZeroMemory() с ключевым словом this, таким образом, структура передаёт ссылку на саму себя.

Основной код советника внутри обработчика OnTick() теперь будет выглядеть так:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   for(int i=0; i<2000; i++)
   {
    double b=MathSqrt(i);
   }
// открываем новые ордера и фиксируем прибыль только на открытии нового бара
   if(IsNewBar())
     {
      // получаем новый сигнал
      int Signal=GetSignal();
      // сразу выходим из функции, если на закрытом баре не был сформирован сигнал
      if(Signal<0)
         return;

      // обновляем структуру
      State.Refresh(); // (!!!)

      // если текущая прибыль больше минимальной и поступил обратный сигнал
      if(State.profit>MinProfit)
         if((Signal==OP_BUY && State.sell_count>0) ||
            (Signal==OP_SELL && State.buy_count>0))
           {
            // закрываем все ордера
            CloseOrders();
            // обновляем структуру
            State.Refresh(); // (!!!)
           }

      // открыты ордера Buy
      if(State.buy_count>0)
        {
         // усредняемся по сигналу, если расстояние до самого нижнего Buy больше минимального шага
         if(Signal==OP_BUY && int((State.buy_bottom_price-Ask)/_Point)>MinStep)
            SetOrder(GetLot(State.buy_count),Signal);
        }
      else
         // открыты ордера Sell
         if(State.sell_count>0)
           {
            // усредняемся по сигналу, если расстояние до самого верхнего Sell больше минимального шага
            if(Signal==OP_SELL && int((Bid-State.sell_top_price)/_Point)>MinStep)
               SetOrder(GetLot(State.sell_count),Signal);
           }
         else
            // нет ордеров, открываем новый по сигналу
            SetOrder(Lots,Signal);
     }
  }

Вызов метода State.Refresh() заменил вызовы пяти функций: GetOpenedOrdersType(), GetOrdersCount()GetProfit(), GetTopSellPrice(), GetBottomBuyPrice(). Следовательно, сократилось и число обращений к серверу, что не могло не сказаться на быстродействии. Вдобавок код стал компактнее.

Заключение

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

Тема на форуме

С уважением, Юрий Лосев aka lsv107
Tlap.com

Уроки по MQL4 , , , ,

Кто и как поставляет ликвидность на Форекс – поставщики и агрегаторы ликвидности

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

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

Узнать больше

В помощь Трейдеру , , ,

Как зарабатывают на Forex

Этот пост – для самых зеленых новичков, которые еще не понимают, как можно зарабатывать на Форекс. 

Вы наверняка уже узнали, что Forex – это международный валютный рынок с триллионными оборотами, посмотрели на непонятные графики с разноцветными кривыми, прочитали, что есть какие-то валютные пары… и поняли, что это все сложно)

Когда я только начинал и читал все эти «умные» статьи о заработке на  Forex с фотографиями мужчин в костюмах, мне было непонятно от слова совсем. Даже было немного стыдно: почему я не могу во всем этом разобраться?

В этом материале я попробую передать самую СУТЬ заработка на валютах так, чтобы даже «я из прошлого» понял.

Далее
В помощь Трейдеру, Новичкам, Обучение , , , ,

MQL4: Пишем советник по стратегии «Пирамидинг»

Здравствуйте, уважаемые форекс-программисты!

Сегодняшний урок по MQL4 мы посвятим созданию торгового эксперта, в котором реализуем построение пирамиды отложенных ордеров и последующее её сопровождение. Давайте сначала разберёмся, что представляет собой стратегия «Пирамидинг», насколько она эффективна и какие подводные камни таит в себе. А затем перейдем к написанию советника с пирамидингом в основе.

MQL4 - Пишем советник по стратегии Пирамидинга

Пирамидинг: основные понятия

Надо сразу отметить, что пирамидинг не является самостоятельной торговой системой. Правильнее назвать его методом увеличения прибыли при торговле по тренду. Основная идея пирамидинга заключается в так называемых «доливках», когда при наличии ярко выраженного тренда открываются новые позиции на откатах или в момент выхода из очередной консолидации. При этом растёт совокупный объём, следовательно, и прибыль. Схема подобных действий напоминает ступенчатую пирамиду, отсюда и название – «пирамидинг».

На приведённом выше рисунке мы можем наблюдать работу пирамиды. Было открыто восемь позиций на покупку по тренду, в итоге все закрылись по общему Stop Loss: последняя сделка с убытком, предпоследняя – в безубытке, а шесть остальных – с прибылью. Может возникнуть вопрос: нельзя ли было открыть самую первую позицию сразу большим лотом, а закрыть по трейлинг-стопу? Дело в том, что мы не могли знать наверняка, пойдет ли цена в нужном нам направлении, и в таком случае рисковали бы всем объёмом. Пирамида же позволяет наращивать совокупный объём постепенно, так как очередная позиция открывается только после того, как предыдущая выйдет в прибыль.

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

Точки входа

Как уже отмечалось ранее, пирамидинг – это метод увеличения прибыли при торговле по тренду. Очевидно, что для диапазонной (флетовой) торговли такой подход не годится и может обернуться полной потерей депозита. Поэтому перед построением пирамиды трейдеру важно определить начало тренда. Решение этой задачи мы оставим за рамками сегодняшнего урока. Отметим лишь, что искать подобные точки входа лучше всего на старших таймфреймах от H1 и выше.

После того, как была совершена первая сделка, и мы убедились, что цена идёт в нужном нам направлении, встаёт другой вопрос: в каких точках следует совершать повторные входы? Самый очевидный способ – входить по основному сигналу, но на меньшем таймфрейме. Можно доливаться при касании скользящей средней, входу или выходу осцилляторов в/из зон перекупленности или перепроданности, при появлении свечных моделей продолжения тренда, на больших объёмах, прорывах консолидации, пробитии или тестировании сильных уровней. Но можно поступить проще и входить в новые сделки с фиксированным шагом в пунктах. Такой шаг можно сделать расширяющимся или сужающимся в зависимости от выбранной тактики. Подобный чисто математический подход к построению пирамиды мы будем использовать в нашем торговом эксперте.

Наращивание объёма позиций

Наиболее консервативным и самым распространённым способом наращивания совокупного объёма является открытие каждой новой позиции фиксированным лотом.

При наличии сильного устойчивого тренда допустимо использовать и более агрессивную методику, когда объем каждой новой позиции будет больше предыдущего. Например, 1; 2; 3; 4 или 1; 2; 4; 8; 16. В этом случае важно вовремя зафиксировать прибыль, иначе если последняя позиция закроется по Stop Loss, образовавшийся убыток может перекрыть всю накопленную до этого прибыль.

Существует и обратный предыдущему способ, при котором объём каждой новой позиции меньше предыдущего. Например, 4; 3; 2; 1. Такой подход вполне обоснован, поскольку каждый тренд имеет тенденцию к затуханию, и с каждой новой ступенькой пирамиды вероятность разворота или сильного отката становится выше. Следовательно, убыток по последним позициям с меньшим лотом будет перекрыт прибылью первых. Недостатком такого способа является недополучение возможной прибыли, хотя риски, конечно же, становятся ниже. В нашем будущем советнике мы реализуем обе методики.

Фиксация прибыли

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

Другой распространённый способ – закрытие пирамиды при достижении определённого уровня прибыли. В этом случае трейдер сам определяет величину прибыли, достаточную для закрытия пирамиды.

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

Общий алгоритм советника

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

Здесь мы видим пирамиду на продажу из Sell Stop ордеров. Объём каждого последующего ордера возрастает в арифметической прогрессии с шагом 0.01.

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

Алгоритм советника можно записать в общем виде с помощью следующей блок-схемы:

Определимся с полным функционалом будущего советника и опишем его входные параметры.

Money Management

Данная секция содержит параметры, относящиеся к управлению капиталом:

Lots – фиксированный/стартовый лот;

LotsMultMode – режим увеличения/уменьшения лота каждой новой позиции пирамиды, может принимать одно из трёх значений: отключено (все позиции будут иметь одинаковый объём, равный параметру Lots); арифметическая (объём будет возрастать в арифметической прогрессии); геометрическая (объём будет возрастать в геометрической прогрессии);

LotsMultiplicatorAr – разность арифметической прогрессии, каждый новый лот увеличивается на данное значение; если параметр равен 0 (нулю), то в качестве разности прогрессии выступает лот первого ордера серии. Используя отрицательные значения данного параметра, лот можно уменьшать;

LotsMultiplicatorGm – знаменатель геометрической прогрессии, каждый новый лот увеличивается в указанное число раз. Если значение меньше единицы, то лот будет уменьшаться. Например, при 1.5 каждый новый лот будет увеличиваться в полтора раза. А чтобы лот в полтора раза уменьшался, потребуется значение 1/1.5=0.67.

Сигнал

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

StochasticPeriod – период стохастика;

StochasticSlowing – замедление;

StochasticLevelDn – уровень перепроданности/уровень перекупленности автоматически (например, для StochasticLevelDn = 20 верхний уровень 100 – StochasticLevelDn = 80).

Общие параметры торговли

DistanceToGrid – на каком расстоянии от цены строить сетку после получения сигнала от стохастика;

AfterSignaBarsCount – сколько баров ждать активации сетки. Так как мы строим сетку в момент входа стохастика в зону перекупленности или перепроданности, мы только предполагаем разворот, который может и не произойти. Поэтому если пирамида не была активирована, её надо удалить спустя какое-то время. С помощью данного параметра можно задать этот контрольный период в барах текущего таймфрейма. Очевидно, что удаление пирамиды также происходит и при поступлении обратного сигнала. Если параметр AfterSignaBarsCount равен нулю, то пирамида удаляется исключительно после формирования обратного сигнала;

Magic – «магический номер», идентификатор ордеров, открытых советником;

Slippage – максимально допустимое проскальзывание.

Параметры сетки

В нашем советнике пирамида будет строиться в виде сетки отложенных ордеров, которая представляет собой установленные на определённом расстоянии друг от друга ордера Buy Stop или Sell Stop. Отложенные ордера такого типа активируются в направлении движения цены, а значит, именно они подходят для трендовой торговли. В данной секции устанавливаются параметры ордеров и всей сетки в целом.

OrdersAmount – количество отложенных ордеров в сетке;

GridStep – шаг сетки, то есть расстояние между отложенными ордерами сетки. Это расстояние может быть как фиксированным, так и увеличивающимся (уменьшающимся) в зависимости от параметра StepMultMode. В случае динамического шага здесь задаётся его стартовое значение;

StepMultMode – режим расширения/уменьшения расстояния между ордерами в сетке по мере удаления от точки входа. Шаг по аналогии с объёмом позиции может изменяться в арифметической или геометрической прогрессии;

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

StepMultiplicatorGm – знаменатель геометрической прогрессии, шаг увеличивается в указанное число раз. Если значение меньше единицы, то шаг будет уменьшаться. Например, при 1.5 шаг будет увеличиваться в полтора раза. Чтобы шаг в полтора раза уменьшался, потребуется значение 1/1.5=0.67;

StopLossRatio – отношение Stop Loss к шагу. Классическая пирамида строится таким образом, что Stop Loss каждого следующего ордера равен цене открытия предыдущего. Однако с помощью этого параметра можно увязать размер Stop Loss с величиной шага;

TakeProfit – общий тейк-профит. Для фиксации прибыли в нашем советнике используется общий Stop Loss, но может случиться так, что будут активированы все ордера пирамиды и цена уйдёт дальше по тренду. Чтобы не терять в таких редких случаях прибыль, используется страховочный Take Profit, иначе нам придётся ждать возвращения цены и срабатывания общего Stop Loss. При этом лучше всё-таки использовать больше ордеров в пирамиде.

Функции советника

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

Функция RefreshState()

Начнём с функции, которая не представлена отдельным блоком в нашей схеме, так как относится к категории служебных, однако ей стоит уделить особое внимание. Функция RefreshState() собирает информацию об открытых советником ордерах и помещает её в переданную структуру state:

// структура для хранения информации об открытых советником ордерах
struct state
  {
   double            BuyTopPrice;         // цена открытия самого верхнего ордера Buy
   double            SellBottomPrice;     // цена открытия самого нижнего ордера Sell

   double            BuyTopSL;            // стоплосс самого верхнего ордера Buy
   double            BuyBottomSL;         // стоплосс самого нижнего ордера Buy
   double            SellTopSL;           // стоплосс самого верхнего ордера Sell
   double            SellBottomSL;        // стоплосс самого нижнего ордера Sell

   datetime          BuyStopStart;        // время установки пирамиды (открытия самого первого ордера Buy Stop в сетке)
   datetime          SellStopStart;       // время установки пирамиды (открытия самого первого ордера Sell Stop в сетке)

   int               BuyCount;            // количество ордеров Buy
   int               SellCount;           // количество ордеров Sell

   int               BuyStopCount;        // количество ордеров Buy Stop
   int               SellStopCount;       // количество ордеров Sell Stop
  };

Опираясь именно на собранную функцией RefreshState() статистику, советник отслеживает текущую обстановку на торговом счёте и определяет, на каком именно шаге алгоритма он находится.

Например, следующий код определяет, что все рыночные ордера пирамиды закрылись по Stop Loss и теперь требуется удалить несработавшие отложенные:

// сетка ордеров закрылась по общему стоп-лоссу, удаляем оставшиеся отложки
   if((State.BuyStopCount>0 && State.BuyStopCount<OrdersAmount && State.BuyCount==0) ||
      (State.SellStopCount>0 && State.SellStopCount<OrdersAmount && State.SellCount==0))
     {
      // удаляем отложенные ордера
      DeletePendingOrders();
     }

Здесь мы сначала проверяем, есть ли вообще отложенные ордера; если есть, то смотрим, меньше ли их количество первоначального значения, то есть получается, что какие-то из них сработали с момента построения сетки. Теперь, если нет открытых рыночных ордеров, можно сделать вывод, что они закрылись по Stop Loss. Следовательно, оставшиеся отложенные ордера являются несработавшими и их надо удалить.

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

      // если сформирован новый бар, опрашиваем индикаторы с помощью вызова функции GetSignal()
      // и записываем результат в переменную TradeSignal
      int TradeSignal=GetSignal();

      // на открытии нового бара нет ни отложенных ни рыночных ордеров
      if(State.BuyCount==0 && State.SellCount==0 && State.BuyStopCount==0 && State.SellStopCount==0)
        {
         // если при этом сформирован торговый сигнал, строим пирамиду указанного типа
         if(TradeSignal!=EMPTY)
            BuildPyramid(TradeSignal);
         // выходим, так как дальнейшая обработка не имеет смысла, так как
         // либо никаких ордеров нет вовсе, либо мы только что построили пирамиду
         return;
        }

Используя информацию об открытых ордерах, можно конструировать и более сложные условия. Следующий код удаляет сетку, если с момента её построения было сформировано больше AfterSignaBarsCount баров или был сформирован противоположный сигнал:

      // если с момента построения сетки было сформировано больше AfterSignaBarsCount баров,
      // или был сформирован противоположный сигнал, удаляем сетку
      if((State.BuyStopCount>0 && State.BuyCount==0 &&
          ((AfterSignaBarsCount>0 && State.BuyStopStart>0 && iTime(_Symbol,_Period,0)-State.BuyStopStart>=BarsInSeconds) || TradeSignal==OP_SELL))
         ||
         (State.SellStopStart>0 && State.SellCount==0 &&
          ((AfterSignaBarsCount>0 && State.SellStopStart>0 && iTime(_Symbol,_Period,0)-State.SellStopStart>=BarsInSeconds) || TradeSignal==OP_BUY)))
        {
         // удаляем отложенные ордера
         DeletePendingOrders();
         return;
        }

Функция GetSignal()

Эта функция опрашивает индикатор и возвращает: OP_BUY (0 – сигнал на покупку), если стохастик входит в зону перепроданности (пересекает нижний сигнальный уровень сверху вниз); OP_SELL (1 – сигнал на продажу), если стохастик входит в зону перекупленности (пересекает верхний сигнальный уровень снизу вверх). Если сигнал не сформирован, функция возвратит EMPTY (-1):

//+------------------------------------------------------------------+
//| Функция возвращает:                                              |
//| OP_BUY (сигнал на покупку), если стохастик входит в зону         |
//| перепроданности (пересекает нижний сигнальный уровень            |
//| сверху вниз)                                                     |
//| OP_SELL (сигнал на продажу), если стохастик входит в зону        |
//| перекупленности (пересекает верхний сигнальный уровень           |
//| снизу вверх)                                                     |
//| EMPTY - сигнала нет                                              |
//+------------------------------------------------------------------+
int GetSignal()
  {
// получаем значение стохастика на баре с индексом 1
   double StochasticOnFirstBar=iStochastic(_Symbol,_Period,StochasticPeriod,1,StochasticSlowing,MODE_SMA,0,MODE_MAIN,1);

// стохастик пересекает нижний сигнальный уровень сверху вниз
   if(StochasticOnFirstBar<StochasticLevelDn)
     {
      // для экономии ресурсов получаем значение стохастика на баре с индексом 2 только после того,
      // как убедились, что первая часть условия выполняется
      if(iStochastic(_Symbol,_Period,StochasticPeriod,1,StochasticSlowing,MODE_SMA,0,MODE_MAIN,2)>StochasticLevelDn)
         return(OP_BUY);
     }
// стохастик пересекает верхний сигнальный уровень снизу вверх
   if(StochasticOnFirstBar>100-StochasticLevelDn)
     {
      // для экономии ресурсов получаем значение стохастика на баре с индексом 2 только после того,
      // как убедились, что первая часть условия выполняется
      if(iStochastic(_Symbol,_Period,StochasticPeriod,1,StochasticSlowing,MODE_SMA,0,MODE_MAIN,2)<100-StochasticLevelDn)
         return(OP_SELL);
     }

// в любом другом случае возвращаем EMPTY
   return(EMPTY);
  }

Функция BuildPyramid()

Данная функция строит пирамиду отложенных ордеров Buy Stop, если переданный в неё параметр signal равен OP_BUY, и пирамиду ордеров Sell Stop, если signal имеет значение OP_SELL.

Сетка строится на расстоянии, равном значению входного параметра DistanceToGrid от цены Ask для ордеров Buy Stop и Bid для сетки Sell Stop. В случае, если это значение меньше минимально разрешённого, сетка строится на расстоянии Sell Stop для данного символа.

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

//+------------------------------------------------------------------+
//| Строит пирамиду из отложенных ордеров                            |
//| Buy Stop, если параметр signal равен OP_BUY                      |
//| Sell Stop, если параметр signal равен OP_SELL                    |
//| на расстоянии DistanceToGrid от текущей цены                     |
//+------------------------------------------------------------------+
void BuildPyramid(int signal)
  {
// определяем корректную величину Stop Level
   int StopLevel=GetTrueStopLevel();

// если расстояние, на котором предполагается строить пирамиду меньше Stop Level,
// то устанавливаем его значение, равным Stop Level
   int Distance=(DistanceToGrid<StopLevel)?StopLevel:DistanceToGrid;
   int Com;//тип отложенного ордера


// проверим корректность лотов перед установкой сетки
   if((Lots<SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN)) ||
      (Lots>SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX)))
     {
      Print("Некорректное значение стартового торгового объёма, пирамида не будет построена");
      return;
     }

   if(LotsMultMode>mlNone)
     {
      double LastOrderLots=NormalizeDouble(GetProgressionMember(LotsMultMode,OrdersAmount-1,((LotsMultMode==mlArithmetical)?LotsMultiplicatorAr:LotsMultiplicatorGm),Lots),2);
      if((LastOrderLots<SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN)) ||
         (LastOrderLots>SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX)))
        {
         Print("Обнаружены некорректные значения торгового объёма, пирамида не будет построена");
         return;
        }
     }

// проверим корректность шага перед выставлением последнего ордера пирамиды
   if((GridStep<StopLevel)  ||
      (StepMultMode>mlNone &&
       (int)GetProgressionMember(StepMultMode,OrdersAmount-1,((StepMultMode==mlArithmetical)?StepMultiplicatorAr:StepMultiplicatorGm),GridStep)<StopLevel))
     {
      Print("Обнаружены некорректные значения шага, пирамида не будет построена");
      return;
     }

// проверим корректность Stop Loss
   if((int(StopLossRatio*GridStep)<StopLevel) ||
      (StepMultMode>mlNone &&
       int(StopLossRatio*GetProgressionMember(StepMultMode,OrdersAmount-1,((StepMultMode==mlArithmetical)?StepMultiplicatorAr:StepMultiplicatorGm),GridStep))<StopLevel))
     {
      Print("Обнаружены некорректные значения Stop Loss, пирамида не будет построена");
      return;
     }

// проверим корректность Take Profit
   if(TakeProfit>0 && TakeProfit<StopLevel)
     {
      Print("Общий Take Profit ",TakeProfit," меньше минимально допустимого значения ", StopLevel,", пирамида не будет построена");
      return;
     }

   double Price, Sign, Step=GridStep;

   MqlTick tick;
   if(!SymbolInfoTick(_Symbol,tick))
      return;

// определяем стартовую цену и тип отложенных ордеров
   if(signal==OP_BUY)
     {
      Price=tick.ask;
      Sign=1.0;
      Com=OP_BUYSTOP;
     }
   else
      if(signal==OP_SELL)
        {
         Price=tick.bid;
         Sign=-1.0;
         Com=OP_SELLSTOP;
        }
      else
         return;

// ордера Buy Stop выставляются выше от цены, а Sell Stop - ниже
   Price+=Sign*Distance*_Point;

   for(int i=0; i<OrdersAmount; i++)
     {
      // вычисляем лот для очередного ордера сетки
      double TradeLots;
      // увеличиваем или уменьшаем текущий лот в зависимости от типа прогрессии
      if(LotsMultMode>mlNone)
         TradeLots=GetProgressionMember(LotsMultMode,i,((LotsMultMode==mlArithmetical)?LotsMultiplicatorAr:LotsMultiplicatorGm),Lots);
      else
         TradeLots=Lots;

      double StopLoss,StopLossInPoints;
      // вычисляем StopLoss пропорционально шагу Step
      StopLossInPoints=StopLossRatio*Step;

      // Stop Loss для ордеров Buy Stop располагается ниже цены открытия, для Sell Stop - выше
      StopLoss=Price+EMPTY*Sign*StopLossInPoints*_Point;

      // пять попыток выставить очередной ордер пирамиды
      for(int j=0; j<5; j++)
        {
         if(OrderSend(_Symbol,Com,NormalizeDouble(TradeLots,2),NormalizeDouble(Price,_Digits),Slippage,NormalizeDouble(StopLoss,_Digits),0,NULL,Magic,0,OrderColor(Com))!=EMPTY)
            break;
         else
            Sleep(1000);
        }

      // увеличиваем или уменьшаем текущий шаг в зависимости от типа прогрессии
      if(StepMultMode>mlNone)
         Step=GetProgressionMember(StepMultMode,i,((StepMultMode==mlArithmetical)?StepMultiplicatorAr:StepMultiplicatorGm),GridStep);

      // ордера Buy Stop выставляются выше от цены, а Sell Stop - ниже
      Price+=Sign*Step*_Point;

     }
// устанавливаем общий тейкпрофит только что выставленным отложенным ордерам
   if(TakeProfit>0)
     {
      double tp=NormalizeDouble(Price+Sign*(TakeProfit<StopLevel?StopLevel:TakeProfit)*_Point,_Digits);

      for(int i=OrdersTotal()-1; i>=0; i--)
         if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES) && OrderMagicNumber()==Magic && OrderSymbol()==_Symbol && OrderType()>OP_SELL)
           {
            if(NormalizeDouble(OrderTakeProfit(),_Digits)!=tp)
              {
               for(int j=0; j<5; j++)
                  if(OrderModify(OrderTicket(),OrderOpenPrice(),OrderStopLoss(),tp,0))
                     break;
                  else
                     Sleep(1000);
              }
           }
     }
  }

Функция CommonStopLoss()

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

//+------------------------------------------------------------------+
//| Обновляет общий стоплосс рыночных ордеров пирамиды, то есть      |
//| устанавливает стоплоссы всех рыночных ордеров на один уровень    |
//+------------------------------------------------------------------+
void CommonStopLoss(state &Orders)
  {
   int type;
   bool refresh;
   double sl;

// из рыночных ордеров имеются только Buy, значит изначально была выставлена пирамида отложенных Buy Stop ордеров
   if(Orders.BuyCount>0 && Orders.SellCount==0)
     {
      type=OP_BUY;
      // стоплосс самого верхнего ордера Buy не совпадает со стоплосс самого нижнего, значит необходимо
      // установить всем ордерам стоплосс самого верхнего, иными словами, "подтянуть" в безубыток
      refresh=(NormalizeDouble(Orders.BuyTopSL,_Digits)!=NormalizeDouble(Orders.BuyBottomSL,_Digits));
      sl=NormalizeDouble(Orders.BuyTopSL,_Digits);
     }
   else
      // из рыночных ордеров имеются только Sell, значит изначально была выставлена пирамида отложенных Sell Stop ордеров
      if(Orders.BuyCount==0 && Orders.SellCount>0)
        {
         type=OP_SELL;
         // стоплосс самого верхнего ордера Sell не совпадает со стоплосс самого нижнего, значит необходимо
         // установить всем ордерам стоплосс самого нижнего, иными словами, "подтянуть" в безубыток
         refresh=(NormalizeDouble(Orders.SellTopSL,_Digits)!=NormalizeDouble(Orders.SellBottomSL,_Digits));
         sl=NormalizeDouble(Orders.SellBottomSL,_Digits);
        }
      else
         return;

   if(refresh)
      for(int i=OrdersTotal()-1; i>=0; i--)
         if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES) && OrderMagicNumber()==Magic && OrderSymbol()==_Symbol && OrderType()==type)
           {
            if(NormalizeDouble(OrderStopLoss(),_Digits)!=sl)
              {
               for(int j=0; j<5; j++)
                  if(OrderModify(OrderTicket(),OrderOpenPrice(),sl,OrderTakeProfit(),0))
                     break;
                  else
                     Sleep(1000);
              }
           }
  }

Более наглядно механизм работы функции можно наблюдать в следующей анимации:

Обработчик OnTick()

Завершая создание торгового эксперта, закодируем алгоритм вызова описанных блоков в соответствии с блок-схемой. Полученный код поместим внутрь обработчика OnTick():

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   state State;

// получаем информацию об открытых советником ордерах и помещаем её в структуру State
   RefreshState(State);

// проверяем, сформировался ли новый бар
   if(IsNewBar())
     {
      // если сформирован новый бар, опрашиваем индикаторы с помощью вызова функции GetSignal()
      // и записываем результат в переменную TradeSignal
      int TradeSignal=GetSignal();

      // на открытии нового бара нет ни отложенных ни рыночных ордеров
      if(State.BuyCount==0 && State.SellCount==0 && State.BuyStopCount==0 && State.SellStopCount==0)
        {
         // если при этом сформирован торговый сигнал, строим пирамиду указанного типа
         if(TradeSignal!=EMPTY)
            BuildPyramid(TradeSignal);
         // выходим, так как дальнейшая обработка не имеет смысла, так как
         // либо никаких ордеров нет вовсе, либо мы только что построили пирамиду
         return;
        }

      // если с момента построения сетки было сформировано больше AfterSignaBarsCount баров,
      // или был сформирован противоположный сигнал, удаляем сетку
      if((State.BuyStopCount>0 && State.BuyCount==0 &&
          ((AfterSignaBarsCount>0 && State.BuyStopStart>0 && iTime(_Symbol,_Period,0)-State.BuyStopStart>=BarsInSeconds) || TradeSignal==OP_SELL))
         ||
         (State.SellStopStart>0 && State.SellCount==0 &&
          ((AfterSignaBarsCount>0 && State.SellStopStart>0 && iTime(_Symbol,_Period,0)-State.SellStopStart>=BarsInSeconds) || TradeSignal==OP_BUY)))
        {
         // удаляем отложенные ордера
         DeletePendingOrders();
         return;
        }
     }

// есть открытые ордера BUY или SELL, значит построена пирамида,
// и какие-то отложенные ордера сработали
   if(State.BuyCount>0 || State.SellCount>0)
     {
      // обновляем общий Stop Loss рыночных ордеров пирамиды, если требуется
      CommonStopLoss(State);
      return;
     }

// сетка ордеров закрылась по общему стоп-лоссу, удаляем оставшиеся отложки
   if((State.BuyStopCount>0 && State.BuyStopCount<OrdersAmount && State.BuyCount==0) ||
      (State.SellStopCount>0 && State.SellStopCount<OrdersAmount && State.SellCount==0))
     {
      // удаляем отложенные ордера
      DeletePendingOrders();
     }
  }

Тесты на истории

Чтобы проверить работоспособность нашего советника и дать оценку его торговому алгоритму, сделаем несколько тестов на исторических данных. Тестировать будем на паре EURUSD с качеством 99.9%. Период тестирования 01.01.2017 – 01.01.2020. Таймфрейм H1 (один час). Начальный депозит 10000 единиц.

Параметры по умолчанию: фиксированный лот 0.01, расстояние до сетки 100 п., шаг фиксированный 300 п., в сетке 15 ордеров, настройки стохастика стандартные, уровень перепроданности 20, отношение Stop Loss к шагу 1.0, ждать срабатывания сетки будем до появления обратного сигнала.

Cо значениями по умолчанию советник не блещет результатами, впрочем, и не сливает. Давайте немного «отодвинем» уровни перепроданности и перекупленности в настройках индикатора, установим параметр StochasticLevelDn на 10 вместо 20. Входы будут реже, но, возможно, точнее. Другие настройки трогать не будем.

На этот раз результаты заметно лучше, а ведь мы изменили только один параметр.

Посмотрим, как советник поведёт себя при увеличении лота в арифметической прогрессии. Для этого выберем в настройках режим «Арифметическая», параметру LotsMultiplicatorAr (разность арифметической прогрессии) присвоим значение 0.01. Можно оставить и ноль, в таком случае параметр автоматически приравнивается к начальному лоту, а он у нас равен 0.01. В итоге первый ордер пирамиды будет иметь объём 0.01, а последний откроется лотом 0.15. Получим следующую картину:

Теперь будем уменьшать лот в арифметической прогрессии от 0.15 до 0.01. Для этого параметру LotsMultiplicatorAr присвоим отрицательное значение -0.01, а размер стартового лота сделаем 0.15:

Исходя из особенностей трендовой торговли, вариант с уменьшением лота выглядит предпочтительнее, поскольку по мере затухания тренда мы уменьшаем и объём наших «доливок». Конечно, в этом случае прибыль ожидаемо больше, но пропорционально растёт и просадка.

Тесты, сделанные в первом приближении, показали работоспособность метода в целом и конкретного советника в частности. Однако окончательные выводы можно делать только по результатам торговли на реальном счёте. В любом случае грамотная оптимизация параметров советнику не повредит.

Заключение

Созданный нами советник реализует математический вариант стратегии «Пирамидинг». Впрочем, более сложные разновидности данного метода слабо поддаются формализации. Всё дело в цене вопроса: стоит ли затраченных усилий учёт многочисленных нюансов построения пирамид, например, по торговым уровням или по фигурам технического анализа, которые надо ещё суметь идентифицировать, или же для советника достаточно сетки с оптимизируемым шагом?

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

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

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

Скачать исходник советника из урока

Тема на форуме

С уважением, Юрий Лосев aka lsv107
Tlap.com



Уроки по MQL4 , , , , ,

MQL4: Массивы и циклы

Здравствуйте, уважаемые коллеги форекс-программисты!

В сегодняшнем уроке, по вашим просьбам, мы подробно разберём работу с массивами и циклами посредством языка MQL4.

Циклы служат для многократного повторения какого-то участка кода, а массивы – для хранения неограниченного количества структур данных с их последующей обработкой в цикле.

Начнём с циклов. Поскольку обращение к элементам массива, как правило, осуществляется с использованием циклов, то научимся сначала работать с ними, а потом перейдем к массивам.

MQL4 программирование - Массивы и циклы

Синтаксис цикла while в MQL4 имеет следующий вид:

while (<условие>)
{
 <тело цикла>;
}

Тело цикла в данном случае будет выполняться, пока истинно условие в круглых скобках. Если это условие на момент выполнения цикла окажется ложным, то тело цикла не выполнится ни одного раза. В качестве примера выведем в журнал терминала целые числа от 1 до 5, каждое с новой строки. Код будет выглядеть так:

int i = 1;
while (i <= 5)
   {
    Print( i );
    i++;
   }

Исходя из условий задачи, мы сначала определили целочисленную переменную i и присвоили ей значение 1. В операторе цикла while проверяем, меньше или равно 5 значение переменной i. Если да, то выполняется тело цикла: значение печатается в журнал и увеличивается на единицу. После первой итерации в журнал выведено число «1», а значение переменной i равно двум. Спустя несколько итераций переменная i стала равна шести, условие i<=5 ложно. Следовательно, тело цикла выполняться больше не будет.

Цикл for

Этот вид цикла работает аналогично уже рассмотренному while. Здесь тело цикла выполняется, пока некое условие истинно. Однако в самом операторе можно задать действия, которые совершаются до начала цикла (например, инициализировать переменную-счётчик), а также после выполнения каждой итерации (увеличение или уменьшение счётчика). Такой вид цикла предназначен прежде всего для ситуаций, когда нам заранее известно число итераций. Синтаксис цикла for в языке MQL4 таков:

for(выражение1; выражение2; выражение3)
{
 <тело цикла>;
}
  • выражение1 – инициализация цикла, как правило, это объявление и инициализация счётчика итераций;
  • выражение2 – условие продолжения цикла, пока оно истинно, тело цикла выполняется;
  • выражение3 – выражение, вычисляемое после каждой итерации, обычно здесь происходит изменение счётчика итераций.

Рассмотрим цикл for на следующем примере: найдём сумму целых чисел от 1 до 1000.

int sum=0; // переменная для суммы чисел
for(int i=1; i<=1000; i++) 
   sum+=i; // прибавляем к текущему значению суммы очередное число 
Print("Сумма чисел от 1 до 100: ",i);

Мы объявили переменную sum для записи в неё текущей суммы чисел и проинициализировали её нулевым значением.

В операторе for переменная i выступает в качестве счётчика итераций. Мы объявили и проинициализировали её начальным значением «1». Нам требуется, чтобы цикл выполнялся 1000 раз, то есть пока переменная i меньше или равна 1000. Условие продолжения цикла примет вид i<=1000. После каждой итерации нам надо увеличить счётчик на единицу. Запишем через точку с запятой i++.

Тело цикла представляет всего один оператор sum+=i, что эквивалентно записи sum=sum+i. Поэтому операторные скобки «{ }» здесь не нужны. В теле цикла значение переменной sum увеличивается на значение переменной i, в которой на текущей итерации содержится очередное число от 1 до 1000. В итоге после тысячи выполнений тела цикла переменная sum будет содержать сумму всех целых чисел от 1 до 1000.

Любое из трех или все выражения в операторе for(выражение1; выражение2; выражение3) могут отсутствовать. Нельзя опускать только разделяющие выражения точки с запятыми, то есть в записи всегда должны быть оба символа «;». Например, запись for(;;) представляет собой бесконечный цикл. Выражение1 и выражение3 могут состоять из нескольких выражений, объединенных оператором запятая «,».

Решим такую задачу. Напечатаем в журнал числа от 1 до 5 и от 5 до 1 одновременно в одном цикле. При этом тело цикла должно состоять только из одного вызова функции Print().

   for(int i=1, j=5; i<=5; i++, j--)
    Print(i," ",j);

Как видите, тело цикла выполняется 5 раз, при этом переменная i изменяется от 1 до 5, а переменная j от 5 до 1. Результат выполнения выглядит следующим образом:

5 1
4 2
3 3
2 4
1 5

Заметьте, что наряду с инкрементом, то есть увеличением переменной i на единицу, здесь мы использовали и декремент, то есть уменьшали на единицу переменную j.

Запишем вывод в цикле пяти целых чисел от 1 до 5 в журнал следующим образом:

int n=1;  
for(;n<=5;)
 {
  Print(n);
  n++;
 }

Здесь мы объявили переменную-счётчик до оператора цикла, а её инкрементацию поместили в тело цикла.

Счётчик в цикле for может увеличиваться или уменьшаться на любое значение, а не только на единицу. Тем самым можно реализовать цикл с шагом. Выведем, например, в журнал все нечётные числа от 1 до 10. Для этого будем увеличивать счётчик после каждой итерации на 2, начиная с единицы:

for(int i=1; i<=10; i+=2)
 Print(i);

В данном случае переменная-счётчик последовательно примет значения 1, 3, 5, 7, 9, 11, а в журнал выведет 1, 3, 5, 7, 9.

Цикл do while

Как отмечалось ранее, если условие продолжения цикла c предусловием (while) изначально ложно, то тело цикла не выполнится ни разу. Однако есть задачи, которые требуют выполнения цикла по крайней мере однократно. Для этих целей существует цикл, тело которого выполнится хотя бы один раз независимо от условия. Синтаксис цикла do while или, как его еще называют, цикл с постусловием в MQL4 имеет следующий вид:

do
{
 <тело цикла>;
}
while (<условие>);

Выполним ту же задачу с выводом целых чисел от 1 до 5 с помощью цикла do while:

int i=1;
do
 {
  Print(i);
  i++;
 }
 while(i<=5);

Сначала мы объявили переменную-счётчик i и инициализировали её значением 1 согласно условию задачи. Затем сразу выполнилось тело цикла. В журнал было выведено значение переменной i, которое на тот момент было равно единице, следом значение переменной i увеличилось на единицу, а уже потом состоялась проверка условия i<=5. Так как 2 меньше 5, то цикл продолжился, пока значение переменной i не стало равняться шести. Обратите внимание, что инкремент счётчика в теле цикла мы производим после вывода его значения в журнал. Если мы будем делать это до вывода в журнал, то нам необходимо уменьшить начальное значение счётчика и изменить условие. В этом случае цикл будет выглядеть так:

int i=0;
do
 {
  i++;
  Print(i);
}
while(i<5);

Любопытно, что в языке MQL4 операцию инкремента можно выполнять прямо в вызове функции, при этом надо помнить, что постфиксный инкремент (i++) увеличивает значение переменной на единицу сразу после использования этой переменной в выражении, а префиксный инкремент (++i) увеличивает значение переменной на единицу непосредственно перед использованием этой переменной в выражении. Теперь наши примеры можно записать компактнее:

int i=1;
do
 Print(i++);
while(i<=5);

Здесь функция Print() выводит в журнал значение переменной i, а следом происходит увеличение i на единицу (инкремент):

int i=0;
do
  Print(++i);
while(i<5);

При вызове функции Print() сначала происходит увеличение переменной i на единицу, а потом значение i выводится в журнал.

Разберём пример посложнее. Напишем советник, который после прикрепления его к графику начинает обрабатывать поступающие тики не сразу, а спустя какое-то время, заданное нами в секундах. При этом советник должен сообщать с помощью комментария на графике, сколько секунд осталось до его запуска. Очевидно, что такую задержку надо поместить внутрь обработчика OnInit(), так как эта функция выполняется при инициализации советника.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
 uint begin=GetTickCount(),// запоминаем значение функции GetTickCount()
      passed_seconds,      // прошло секунд
      pause=10;            // пауза в секундах
 Comment("");              // очищаем комментарий на графике, выводя пустую строку
 
 do
 {
  passed_seconds=(GetTickCount()-begin)/1000;// прошло секунд со старта
  Comment("Советник начнёт работу через:",pause-passed_seconds);
 }
 while(passed_seconds<pause);// тело цикла выполняется пока количество
// прошедших секунд меньше паузы в секундах
 Comment("");              // очищаем комментарий на графике, выводя пустую строку
   
   return(INIT_SUCCEEDED);
  }

В данном коде мы реализовали задержку с выводом оставшегося до запуска времени в секундах при помощи цикла do while. Функция GetTickCount() возвращает количество миллисекунд, прошедших с момента старта системы. Мы использовали её для вычисления количества прошедших и оставшихся секунд.

Операторы break и continue

Операторы break и continue могут использоваться в теле цикла любого типа.

С помощью оператора break можно немедленно прервать выполнение тела цикла без каких-либо дополнительных условий. Управление при этом передаётся первому следующему за циклом оператору. Рассмотрим такую реализацию знакомой нам задачи вывода целых чисел от 1 до 5 в журнал терминала:

 int i=1;
 while(true)
   {
    Print(i);
    i++;
    if(i>5) break;
   }

Здесь мы имеем бесконечный цикл while, так как условие в круглых скобках всегда истинно. В теле цикла осуществляется вывод в журнал текущего значения переменной i и её инкрементация. Как только значение i станет равным шести, оператор break прервёт выполнение цикла. Тут стоит сделать одно важное замечание. Если цикл предполагает большое число итераций или была допущена ошибка, из-за которой цикл стал бесконечным, программа может не отвечать, «зависнуть». Чтобы избежать такой ситуации, в условие продолжения цикла добавляют вызов функции IsStopped(), которая возвращает истину (true), если от терминала поступила команда завершить выполнение mql4-программы (например, была нажата кнопка «Стоп» при работе в тестере). Учитывая это, наш пример будет правильнее записать так:

 while(!IsStopped)
   {
    Print(i);
    i++;
    if(i>5) break;
   }

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

Надо заметить, что язык MQL4 позволяет прерывать цикл внутри функции еще и оператором return. Например:

void PrintNumber()
{
int i=1;
 while(!IsStopped)
   {
    Print(i);
    i++;
    if(i>5) return;
   }
}

В данном случае оператор return прервёт цикл и передаст управление в то место программы, откуда была вызвана функция PrintNumber().

Оператор continue прерывает текущую итерацию цикла и начинает новую. Все операторы в теле цикла, идущие за оператором continue, не будут выполнены. Для иллюстрации работы оператора немного модифицируем нашу задачу с выводом в журнал целых чисел от 1 до 5. Выведем все числа, кроме 2.

for(int i=1; i<=5; i++)
{
 if(i==2) continue;
 Print(i);
}
1
2
3
4
5

Вывод числа «2» будет пропущен.

Оператор continue удобно применять, если тело цикла довольно большое. Можно не усложнять конструкции ветвления внутри цикла, а сразу пропустить все последующие операторы и перейти к новой итерации.

Массивы

Массивы присутствуют практически во всех языках программирования, и MQL4 не стал исключением. По сути, массив – это совокупность переменных одного типа с одним именем, но при этом каждая переменная имеет свой уникальный номер. Например, нам нужны три целочисленные переменные. Мы можем объявить их следующим образом:

int Var1, Var2, Var3;

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

Var1=35;
Var2=8;
Var3=21;

С другой стороны, переменные можно задать как массив из трёх элементов:

int Var[3];

Обращаться же к этим переменным следует по их общему имени и номеру в массиве:

Var[0]=35;
Var[1]=8;
Var[2]=21;

Номер переменной в массиве называют индексом (от латинского index – указательный палец). При объявлении массива в квадратных скобках указывается его размер (количество элементов), а при обращении к переменной – индекс элемента.

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

Статические и динамические массивы

Размер статических массивов устанавливается на этапе компиляции и больше в ходе выполнения программы не меняется. Пример объявления статического массива:

int Array[10];

Здесь мы объявили массив из десяти целочисленных элементов, память под которые выделит компилятор.

Массивы, размер которых можно изменять в процессе выполнения программы, называются динамическими. Вместе с изменением размера массива память либо выделяется, либо освобождается динамически. Пример объявления динамического массива:

int Array[];

Получается, если в квадратных скобках не указан размер массива, то он будет объявлен как динамический. В данном случае мы объявили динамический массив целочисленных элементов. Чтобы использовать массив, надо задать его размер с помощью функции ArrayResize():

ArrayResize(Array,10);

Теперь массив Array состоит из 10 элементов с индексами от 0 до 9.

У данной функции есть третий необязательный параметр reserve_size. С помощью него можно задать размер массива «с запасом». Например, первый вызов функции в виде ArrayResize(Array, 10, 1000) увеличит размер массива до 10 элементов, но физически память под массив будет выделена, как если бы он содержал 1010 элементов. То есть память под 1000 элементов оказывается в резерве. Теперь при увеличении или уменьшении размера массива в пределах 1010 элементов физического распределения памяти не будет. Если же размер увеличить, скажем, до 1011, то выделится еще 1000 резервных элементов, и так далее. Поскольку физическое распределение памяти – процесс достаточно медленный, то в случае, если предполагается частое изменение размера массива в программе, третий параметр функции ArrayResize() будет весьма полезен.

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

int Numbers[5] = {3, 21, 72, 1, 8};

Здесь каждому из пяти элементов с индексами от 0 до 4 присваиваются по порядку значения в фигурных скобках. Можно проинициализировать схожим образом все элементы массива одним значением:

int Numbers[5] = {82};

Все пять элементов массива примут значение 82.

Динамические массивы инициализировать таким образом нельзя. Даже если вы объявите массив:

int Numbers[] = {3, 21, 72, 1, 8};

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

Чтобы инициализировать динамический массив, можно использовать функции ArrayInitialize() или ArrayFill(). Первая заполняет указанным значением весь массив, вторая может заполнить только часть массива. Но таким образом можно инициализировать или заполнить лишь массивы простых типов, массив, например, строк придётся заполнять в цикле, перебирая все элементы по индексу и присваивая каждому нужное значение. Как это сделать, поговорим в следующем разделе.

Использование циклов при работе с массивами

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

Решим задачу.

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

Для обработки слов создадим массив строк и запишем слова в него:

string Words[]={"машина", "пицца", "вода", "ножницы", "кот", "батарейка", "удочка", "гром", "стол", "флаг", "телефон", "зонт", "документ"};
int size=ArraySize(Words);
int n=0;
  
   for(int i=0; i<size; i++)
   {
    if(StringLen(Words[i])>5)
    {
     Print(Words[i]);
     n++;
    }
   }
if(n>0) Print("Количество слов, длина которых больше пяти: ",n);

В этом примере нам понадобилась дополнительная переменная size, в которую мы поместили найденный с помощью функции ArraySize() размер массива. Было бы нерационально на каждой итерации вызывать функцию, чтобы сравнить полученное значение со счётчиком. Хотя в данном случае неважно, в каком направлении обходить элементы массива. Поэтому можно обойтись и без дополнительной переменной, обходя массив в обратном порядке:

int n=0;
for(int i=ArraySize(Words)-1; i>=0; i--)
  if(StringLen(Words[i])>5)
    {
     Print(Words[i]);
     n++;
    }
if(n>0) Print("Количество слов, длина которых больше пяти: ",n);

Следующая задача: удалить элемент с указанным индексом из динамического массива целых чисел.

double Array[]; // подготовим массив для демонстрации работы алгоритма

   int size=21; // зададим ему размер в 21 элемент
   int removed_element=17; // индекс удаляемого элемента

   Print("Новый Размер:", ArrayResize(Array,size));

   for(int i=0; i<size; i++) // заполним массив числами от 100, до 120
      Array[i]=i+100.0;


   if(removed_element<size) // индекс удаляемого элемента не выходит за пределы массива
     {

      Print("Будет удалён элемент с индексом ", removed_element,", имеющий значение ", Array[removed_element]);

      for(int i=removed_element+1; i<size; i++) // сдвинем все элементы за удаляемым влево на один
         Array[i-1]=Array[i];

      size=ArrayResize(Array,size-1); // уменьшим размер массива на единицу, тем самым отсечём ненужный последний элемент

      Print("Новый размер массива: ", size);

      for(int i=size-1; i>=0; i--) // выведем в журнал все элементы массива
        {
         Print("Array[",i,"]=",Array[i]);
        }

     }
   else
      Print("Индекс удаляемого элемента находится за пределами массива");

Передача массива в функцию

Массивы можно передавать в функции только по ссылке, то есть в функцию передаётся только адрес существующего массива, а не его копия. Функция не может вернуть и сам массив. Она только производит действия над массивом в целом или над его отдельными элементами. Таким образом, если аргумент функции представляет собой массив, то перед его именем необходимо писать знак «&» (амперсанд), как знак того, что аргумент передаётся по ссылке, а после имени – пустые квадратные скобки «[]», указывающие на то, что аргумент представляет из себя массив.

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

double Array[]; // подготовим массив для демонстрации работы нашей будущей функции
int size=21;
ArrayResize(Array,size);

for(int i=0; i<size; i++) // заполним массив числами от 100, до 120
      Array[i]=i+100.0;


   if( RemoveFromArray(Array, 17) )
      for(int i=ArraySize(Array)-1; i>=0; i--) // выведем в журнал все элементы массива
         Print("Array[",i,"]=",Array[i]);

Напишем функцию, которая будет удалять указанный элемент.
//+------------------------------------------------------------------+
//| Удаляет элемент динамического массива с указанным индексом       |
//| и возвращает "true", если удаление прошло удачно, в противном    |
//| случае возвращает false                                          |
//+------------------------------------------------------------------+
bool RemoveFromArray(double &array[], int index)
  {
   int size=ArraySize(array);

   if(index<size) // индекс удаляемого элемента не выходит за пределы массива
     {
      for(int i=index+1; i<size; i++) // сдвинем все элементы за удаляемым влево на один
         array[i-1]=array[i];

      size=ArrayResize(array,size-1); // уменьшим размер массива на единицу, тем самым отсечём ненужный последний элемент

      return(true); // возвратим истину, так как удаление прошло успешно
     }
   else
     {
      Print("Индекс удаляемого элемента находится за пределами массива");
      return(false);// возвратим ложь, так как удаление прошло неудачно
     }
  }

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

Многомерные массивы

Массивы, в которых каждый элемент имеет один индекс, называются одномерными. До этого мы рассматривали только одномерные массивы. Если же каждый элемент массива имеет более одного индекса – то мы имеем дело с многомерным массивом.

Запись вида:

int Numbers[5][4];

объявляет двумерный массив или, другими словами, массив пяти массивов по четыре элемента. Визуально двумерный массив можно представить как таблицу, в данном случае из пяти строк и четырёх столбцов. Элементы двумерного массива можно представить как ячейки таблицы. Строка и столбец, на пересечении которых располагается ячейка – это индексы элемента массива. Например, выражение Numbers[2][1]=48 можно интерпретировать так: в ячейку таблицы на третьей строке во втором столбце записали число «48» (помним, что индексация в каждом измерении массива начинается с нуля).

Так как у каждого элемента массива Numbers два индекса, то перебор всех его значений надо осуществлять с помощью двух циклов, причём один будет вложен в другой. Например, следующий код присваивает всем элементам массива значение «10», кроме элемента Numbers[2][1], которому присваивается значение «48»:

for(int i=0; i<5; i++)
  for(int j=0; j<4; j++)
   {
     if(i==2 && j==1)
      Numbers[i][j]=48;
    else
     Numbers[i][j]=10;
   }

Язык MQL4 поддерживает не более четырёх измерений у массива:

double Prices[8][2][5]    // пример трёхмерного массива
double Levels[4][3][1][6] // четырёхмерный массив

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

double Prices[][2][5]; // правильноe объявление трёхмерного динамического массива
double Prices[][][5]; // неправильное объявление

Отсюда следует, что с помощью функции ArrayResize() можно менять только размер первого измерения массива:

double Prices[][2][5];
int new_size = ArrayResize(Prices,10);

Надо учитывать, что в этом случае функция ArrayResize() вернёт общий размер элементов массива, то есть new_size станет равным 100 (10*2*5).

На практике большинство задач, с которыми вам придётся столкнуться, вряд ли потребует использования массивов, размерность которых превышает 2. Двумерные динамические массивы, например, хорошо подходят для обработки табличных данных, поэтому встречаются намного чаще, чем другие многомерные массивы. Однако иногда требуется, чтобы была возможность изменять размер двумерного массива в обоих измерениях. Например, в процессе выполнения программы нам может понадобиться массив Numbers[Rows][Columns], где Rows и Columns – это переменные, но мы можем задать с помощью переменной только размер первого измерения. Обойти это ограничение можно с помощью простого трюка: свернуть оба измерения массива в одно. То есть объявить одномерный динамический массив и установить его размер равным произведению Rows*Columns, а второе измерение определять с помощью простой формулы row*Columns+column, где row и column – координаты ячейки в «виртуальной таблице», а Columns – это размер второго измерения (количество столбцов).

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

struct int2DArray
  {
private:
   int               rows;                                                                // количество строк
   int               columns;                                                             // количество столбцов
   int               Values[];                                                            // одномерный динамический массив
   int               GetIndex(int row, int col) {return(row*columns+col);};               // возвращает индекс ячейки в массиве
public:
   int               Create(int row, int col);                                            // устанавливает массиву размер, необходимый для хранения ячеек
   int               Get(int row, int col) {return(Values[GetIndex(row,col)]);};          // возвращает содержимое ячейки (row,col)
   void              Set(int row, int col, int value) {Values[GetIndex(row,col)]=value;}; // записывает значение в ячейку (row,col)
   int               AddRows(int amount);                                                 // добавляет строку к двумерному массиву
   int               Rows() {return(rows);};                                              // возвращает число строк в массиве
   int               Columns() {return(columns);};                                        // возвращает число строк в массиве
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int int2DArray::Create(int row, int col)
  {
   int size=row*col;
   if(size<=0)
      return(EMPTY);
   rows=row;
   columns=col;

   ArrayFree(Values);
   ArrayResize(Values,size);
   ArrayInitialize(Values,0);
   return(size);
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int int2DArray::AddRows(int amount)
  {
   int new_size=ArrayResize(Values,ArraySize(Values)+amount*columns);
   rows+=amount;
   return(new_size);
  }
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
// Объявим переменную структуры:
   int2DArray Array;

// Зададим размер массива в 10 строк и 5 столбцов:
   int size=Array.Create(10,5);

// Все ячейки проинициализированы нулевым значением, а их общее количество - пятьдесят:
   Print("Размер массива=",size);

// Заполним ячейки (0;3), (5;0), (8;4) какими-то ненулевыми значениями
   Array.Set(0,3,9);
   Array.Set(5,0,18);
   Array.Set(8,4,3);

// Убедимся в том, что заполнились именно те ячейки, индексы которых мы указали в методе Set:
   for(int i=0; i<Array.Rows(); i++)
      for(int j=0; j<Array.Columns(); j++)
        {
         PrintFormat("Array[%d,%d]=%d",i,j,Array.Get(i,j));
        }

// Добавим 2 строки в "массив". Так как общее число столбцов у нас пять, то физически в конец одномерного массива добавятся 2*5=10 элементов.
   Array.AddRows(2);

// Заполним ячейки (10;3), (11;4) какими-то ненулевыми значениями
   Array.Set(10,3,4);
   Array.Set(11,4,31);

// Убедимся в том, что заполнились именно те ячейки, индексы которых мы указали в методе Set:
   for(int i=0; i<Array.Rows(); i++)
      for(int j=0; j<Array.Columns(); j++)
        {
         PrintFormat("Array[%d,%d]=%d",i,j,Array.Get(i,j));
        }

   return(INIT_SUCCEEDED);
  }

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

«Ошибка на единицу»

Одна из самых распространённых ошибок при работе с циклами for и другими, если они используют счётчик – это так называемая «ошибка неучтённой единицы» или «ошибка на единицу». Возникает она из-за неправильного использования операторов сравнения. Например, цикл

for(int i=1; i<=5; i++)
     Print(i);

выполнится 5 раз, а цикл

for(int i=1; i<5; i++)
  Print(i);

будет выполнен всего 4 раза. Во втором случае при значении 5 переменной i условие i<5 будет ложным, значит, тело цикла при этом значении выполнено не будет. Важно помнить, что цикл выполняется только тогда, когда условие его продолжения истинно. «Ошибка неучтённой единицы» наибольшую опасность представляет в контексте работы с массивами. Так как индексация элементов массива начинается с нуля, то индекс последнего элемента на единицу меньше размера самого массива. Если из-за «неучтённой единицы» цикл итерации по массиву выполнится на один раз больше требуемого, то программа прекратит работу из-за критической ошибки. Произойдёт так называемый «выход за пределы массива»:

int Numbers[];
int size=ArrayResize(Numbers,100);
for(int i=0; i<=size; i++) // необходимо использовать строгое условие i<size 
Print(Numbers[i]);

Здесь переменная size равна размеру массива, то есть 100. На последней итерации цикла счётчик i примет значение 100, но элемента с таким индексом в массиве нет, последний имеет индекс 99. Поэтому условие продолжения цикла должно быть строгим – i<size.

Быстрая сортировка составных данных с помощью массива ссылок

Рассмотрим интересный пример использования массивов для построения индексов в стиле базы данных. В арсенале языка MQL4 имеется функция ArraySort() для сортировки числовых массивов по первому измерению. Отсортировать, скажем, массив структур или объектов по какому-то числовому элементу с помощью функции ArraySort() нельзя. Казалось бы, единственный выход – писать функцию сортировки с физическим обменом местами элементов массива структур. Но можно создать двумерный динамический числовой массив, в который достаточно скопировать нужные значения для сортировки, и индекс элементов, которым принадлежат эти данные в массиве-источнике. Отсортировав такой массив по первому измерению, мы можем потом обращаться к нужному элементу в массиве-источнике через индекс во втором измерении. Физической сортировки массива-источника не произойдёт. Давайте рассмотрим подобную сортировку на конкретном примере. Только мы будем сортировать не какой-то массив структур, а информацию об открытых ордерах, даже не копируя её в массив.

Определим критерии сортировки в виде перечисления:

enum field
  {
   fOrderOpenTime,   // Время открытия
   fOrderType,       // Тип
   fOrderLots,       // Объём
   fOrderOpenPrice,  // Цена открытия
   fOrderStopLoss,   // S/L
   fOrderTakeProfit, // T/P
   fOrderCommission, // Комиссия
   fOrderSwap,       // Своп
   fOrderProfit      // Прибыль
  };

Напишем функцию, которая строит индекс по переданному критерию сортировки:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void BuildIndex(field criterion, int &out[])
  {
   double Index[][2];
   int size=OrdersTotal(); // общее количество ордеров
   ArrayResize(Index,size);

   for(int i=size-1; i>=0; i--) // перебор всех ордеров
      if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES))
        {
         Index[i][1]=OrderTicket();

         switch(criterion)
           {
            case fOrderOpenTime:
               Index[i][0]=double(OrderOpenTime());
               break;
            case fOrderType:
               Index[i][0]=OrderType();
               break;
            case fOrderLots:
               Index[i][0]=OrderLots();
               break;
            case fOrderOpenPrice:
               Index[i][0]=OrderOpenPrice();
               break;
            case fOrderStopLoss:
               Index[i][0]=OrderStopLoss();
               break;
            case fOrderTakeProfit:
               Index[i][0]=OrderTakeProfit();
               break;
            case fOrderCommission:
               Index[i][0]=OrderCommission();
               break;
            case fOrderSwap:
               Index[i][0]=OrderSwap();
               break;
            case fOrderProfit:
               Index[i][0]=OrderProfit();
               break;
           }
        }

   ArraySort(Index);// сортируем по первому измерению
   ArrayResize(out,size);

// копируем тикеты в переданный массив в отсортированном порядке
   for(int i=0; i<size; i++)
      out[i]=int(Index[i][1]);
  }

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  { 
   /* Перед вызовом функции надо объявить массив, в который функция поместит ссылки на ордера в отсортированном порядке. Очевидно, что в качестве таких ссылок будут использованы тикеты,– уникальные идентификаторы ордеров. */
int Tickets[];
/* Вызываем функцию. В качестве критерия указываем прибыль ордера, то есть сортировку будем производить по текущей прибыли ордеров от наименьшей к наибольшей: */
BuildIndex(fOrderProfit, Tickets);
// Выведем в журнал информацию об открытых ордерах, отсортированную по прибыли.
 
// перебираем массив тикетов
for(int i = ArraySize(Tickets) -1; i>=0; i--)
     {
      // выбираем ордер с указанным тикетом
      if(OrderSelect(Tickets[i],SELECT_BY_TICKET))
        {
         int digits=int(SymbolInfoInteger(OrderSymbol(),SYMBOL_DIGITS));
        // формируем и выводим в журнал строку с информацией по выбранному ордеру
         Print(OrderTicket()," | ",
               TimeToString(OrderOpenTime())," | ",
               (OrderType()==OP_BUY)?"buy":"sell"," | ",
               DoubleToStr(OrderLots(),2)," | ",
               OrderSymbol()," | ",
               DoubleToStr(OrderOpenPrice(),digits)," | ",
               DoubleToStr(OrderStopLoss(),digits)," | ",
               DoubleToStr(OrderTakeProfit(),digits)," | ",
               DoubleToStr(OrderCommission(),2)," | ",
               DoubleToStr(OrderSwap(),2)," | ",
               DoubleToStr(OrderProfit(),2));
        }
     }
  return(INIT_SUCCEEDED);
}

Фрагмент журнала с результатами работы:

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

Заключение

В этом уроке мы рассмотрели использование циклов при работе с массивами. Заодно вспомнили и закрепили основные сведения о самих массивах и циклах, изучили особенности их реализации в языке MQL4, разобрали типичные примеры и приёмы программирования массивов и циклов.

Тема на форуме

С уважением, Юрий Лосев aka lsv107
Tlap.com

Уроки по MQL4 , , ,

Змеевидная последовательность Фибоначчи – применяем известный инструмент в новых реалиях рынка

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

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

Узнать больше

В помощь Трейдеру , , , ,

MQL4: работа с барами, поиск дивергенции по RSI

Здравствуйте, уважаемые коллеги программисты MQL4!

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

MQL4 - Работа с барами, поиск Дивергенции по RSI

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

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

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

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

Итак, каждый бар имеет цену открытия – цена, поступившая на начало периода; цену закрытия – последняя цена, поступившая за период; максимальную и минимальную цену за период.

Массив баров на графике на всю глубину исторических данных называется таймсерией. Нумерация в этом массиве ведётся справа налево, то есть индекс 0 (ноль) в массиве-таймсерии означает данные текущего бара, который соответствует незавершенному промежутку времени на данном таймфрейме. Размер массива-таймсерии можно получить, вызвав функцию iBars, или обратившись к предопределённой переменной Bars.

Доступ к данным конкретного бара осуществляется с помощью функций:

iOpen – цена открытия бара;

iClose – цена закрытия бара;

iHigh – максимальная цена бара;

iLow – минимальная цена бара;

iTime – время открытия бара;

iVolume – тиковый объём или сколько раз цена меняла значение за время формирования бара.

Примеры работы с таймсериями

Сначала рассмотрим простой пример работы с таймсерией. Найдём на всей доступной истории свечу с наибольшим объёмом и одновременно посчитаем средний размер тел свечей:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void ShowBarsParameters()
  {
   Comment("");
   long VolumeMax=0;  // максимальный объём
   int VolumeMaxIndex=0;// индекс бара с максимальным объёмом

   double BodySum=0;  // суммарная величина тел свечей

   for(int i=Bars-1; i>=0; i--)
     {
      if(VolumeMax<iVolume(NULL,0,i))
        {
         VolumeMax=iVolume(NULL,0,i);
         VolumeMaxIndex=i;
        }
      BodySum+=MathAbs(iOpen(NULL,0,i)-iClose(NULL,0,i));
     }
   Comment("На графике всего: ", Bars," баров","
",
           "Максимальное значение объема ",VolumeMax," получено ",
           iTime(NULL,0,VolumeMaxIndex)," на баре №",VolumeMaxIndex,"
",
           "Средняя величина тел свечей в пунктах: ", int(BodySum/_Point)/Bars);
  } 

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void ShowBarsParametersExt(string symbol, int timeframe)
  {
   Comment("");
   long VolumeMax=0;  // максимальный объём
   int VolumeMaxIndex=0;// индекс бара с максимальным объёмом

   double BodySum=0;  // суммарная величина тел свечей

   for(int i=iBars(symbol,timeframe)-1; i>=0; i--)
     {
      if(VolumeMax<iVolume(symbol,timeframe,i))
        {
         VolumeMax=iVolume(symbol,timeframe,i);
         VolumeMaxIndex=i;
        }
      BodySum+=MathAbs(iOpen(symbol,timeframe,i)-iClose(symbol,timeframe,i));
     }
   Comment("На графике ",symbol,"(",timeframe,") всего: ", iBars(symbol,timeframe)," баров","
",
           "Максимальное значение объема ",VolumeMax," получено ",
            iTime(symbol,timeframe,VolumeMaxIndex)," на баре №",VolumeMaxIndex,"
",
           "Средняя величина тел свечей в пунктах: ", int(BodySum/_Point)/Bars);
  }

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

ShowBarsParameters("EURJPY",PERIOD_H1);

Разметка фракталов Билла Вильямса

Давайте научимся определять простейшие свечные формации. Так как мы собираемся искать дивергенции осциллятора RSI с ценой, нам надо решить задачу с определением локальных экстремумов на графике. По этим опорным точкам можно будет провести трендовую линию, которая покажет направление движения цены. Самым простым способом определения таких точек является разметка фракталов Билла Вильямса. Фракталы представляют собой формации из пяти свечей. Если High центральной свечи расположен выше максимальных цен двух свечей слева и двух свечей справа – имеет место «фрактал вверх»:

Если же Low центрального бара ниже минимумов двух баров слева и двух баров справа, то перед нами «фрактал вниз»:

В терминале есть встроенный индикатор фракталов:

Мы напишем скрипт, который будет делать разметку фракталов на исторических данных, подобно тому, как это делает встроенный индикатор Fractals.

С помощью кнопки в главной панели MetaEditor создадим скрипт «Фракталы.mq4»:

Скрипт более удобен для демонстрации, так как изначально состоит из одной главной функции OnStart(), которая запускается, как только скрипт был добавлен к графику валютной пары. После однократного выполнения OnStart() скрипт выгружается из памяти. Весь рабочий код мы поместим в функции, которые потом можно будет без труда перенести в ваши советники и индикаторы.
//+------------------------------------------------------------------+
//|                                                     Фракталы.mq4 |
//|                        Copyright 2019, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2019, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
  }

Для отображения фракталов будем использовать графические объекты «стрелка» (OBJ_ARROW). Нам понадобится функция, которая будет создавать такие объекты на графике цены. Воспользуемся готовой функцией ArrowCreate из справочника MQL, идущего в комплекте с терминалом:

//+------------------------------------------------------------------+ 
//| Создает стрелку                                                  | 
//+------------------------------------------------------------------+ 
bool ArrowCreate(const long              chart_ID=0,           // ID графика 
                 const string            name="Arrow",         // имя стрелки 
                 const int               sub_window=0,         // номер подокна 
                 datetime                time=0,               // время точки привязки 
                 double                  price=0,              // цена точки привязки 
                 const uchar             arrow_code=252,       // код стрелки 
                 const ENUM_ARROW_ANCHOR anchor=ANCHOR_BOTTOM, // положение точки привязки 
                 const color             clr=clrRed,           // цвет стрелки 
                 const ENUM_LINE_STYLE   style=STYLE_SOLID,    // стиль окаймляющей линии 
                 const int               width=3,              // размер стрелки 
                 const bool              back=false,           // на заднем плане 
                 const bool              selection=false,      // выделить для перемещений 
                 const bool              hidden=true,          // скрыт в списке объектов 
                 const long              z_order=0)            // приоритет на нажатие мышью 

Создадим функцию, которая получает индекс бара и определяет, сформирован ли на нём фрактал:

//+------------------------------------------------------------------+
//| Проверяет, сформирован ли на баре с индексом index               |
//| фрактал                                                          |
//|                                                                  |
//| is_up - эта переменная принимает значение true, если сформирован  |
//| фрактал вверх                                                 |
//| is_down - эта переменная принимает значение true, если сформирован|
//| фрактал вниз                                                  |
//| Если фрактал не сформирован, то обеим переменным                 |
//| присваивается значение false                                     |
//+------------------------------------------------------------------+
void GetFractalType(int index, bool &is_up, bool &is_down)
  {
// сбрасываем значения переменных
   is_up=is_down=false;

// проверяем, выше ли High (максимум) центральной свечи двух предыдущих и двух последующих
   is_up=(iHigh(_Symbol,_Period,index+1)<iHigh(_Symbol,_Period,index) &&
          iHigh(_Symbol,_Period,index+2)<iHigh(_Symbol,_Period,index) &&
          iHigh(_Symbol,_Period,index-1)<iHigh(_Symbol,_Period,index) &&
          iHigh(_Symbol,_Period,index-2)<iHigh(_Symbol,_Period,index));

// проверяем, ниже ли Low (минимум) центральной свечи двух предыдущих и двух последующих
   is_down=(iLow(_Symbol,_Period,index+1)>iLow(_Symbol,_Period,index) &&
            iLow(_Symbol,_Period,index+2)>iLow(_Symbol,_Period,index) &&
            iLow(_Symbol,_Period,index-1)>iLow(_Symbol,_Period,index) &&
            iLow(_Symbol,_Period,index-2)>iLow(_Symbol,_Period,index));
  }

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

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void ShowFractals()
  {
   // пропускаем по два бара в конце и в начале таймсерии
   for(int i=Bars-2; i>1; i--)
     {
      string Name;// имя объекта
      bool IsUp, IsDown;
      // получаем тип фрактала, если он сформирован на данном баре
      GetFractalType(i,IsUp,IsDown);

      // сформирован фрактал вверх
      if(IsUp)
        {
         Name="FRCTL_UP_"+_Symbol+"_"+IntegerToString(iTime(_Symbol,_Period,i));
         // рисуем над High свечи синюю точку
         ArrowCreate(0,Name,0,iTime(_Symbol,_Period,i),iHigh(_Symbol,_Period,i),
         159,ANCHOR_BOTTOM,clrBlue,STYLE_SOLID,2,true,false);
        }
      // сформирован фрактал вниз
      if(IsDown)
        {
         Name="FRCTL_DOWN_"+_Symbol+"_"+IntegerToString(iTime(_Symbol,_Period,i));
         // рисуем под Low свечи красную точку
         ArrowCreate(0,Name,0,iTime(_Symbol,_Period,i),iLow(_Symbol,_Period,i),
         159,ANCHOR_BOTTOM,clrRed,STYLE_SOLID,2,true,false);
        }
     }
  }

Также нам понадобится функция, которая будет удалять графические объекты, которые могли остаться на графике от предыдущего запуска нашего скрипта. Эта функция будет искать графические объекты, имя которых начинается с определённого набора символов (так называемый префикс), и удалять только их. Ранее мы позаботились, чтобы создаваемые объекты имели такой префикс: «FRCTL». Удалять объекты мы будем перед вызовом функции ShowFractals.

//+------------------------------------------------------------------+ 
//| Удаляет графические объекты с префиксом prfx                     | 
//+------------------------------------------------------------------+
void DeleteObjects(string prfx)
  {
   for(int i=ObjectsTotal()-1; i>=0; i--)
     {
      string nm=ObjectName(0,i);
      if(StringSubstr(nm,0,StringLen(prfx))==prfx)
         ObjectDelete(0,nm);
     }
  }

В итоге функция OnStart() будет выглядеть следующим образом:

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   DeleteObjects("FRCTL");
   ShowFractals();
  }

Результат работы нашего скрипта:

Автоматический поиск дивергенции на примере осциллятора RSI

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

Различают медвежью и бычью дивергенции.

Медвежья дивергенция – это ситуация, когда цена формирует всё более высокие максимумы при восходящем тренде, а каждая новая вершина индикатора RSI оказывается ниже предыдущей:

Медвежья дивергенция служит сигналом высокой вероятности смены тренда на нисходящий (медвежий).

Бычья дивергенция – это ситуация, когда цена формирует всё более низкие минимумы при нисходящем тренде, а каждая новая впадина индикатора RSI оказывается выше предыдущей:

Бычья дивергенция служит сигналом высокой вероятности смены тренда на восходящий (бычий).

Реже используются сигналы конвергенции или, как её чаще называют, «скрытой» дивергенции. Конвергенция сигнализирует о продолжении существующего тренда.

Скрытая медвежья дивергенция:

Скрытая бычья дивергенция:

Напишем скрипт, который будет искать на исторических данных оба вида дивергенций: обычную дивергенцию будем отмечать сплошной линией, а скрытую – пунктирной.

С помощью кнопки «Создать» в главной панели MetaEditor создадим скрипт «Дивергенция.mq4».

В этом скрипте нам понадобятся входные параметры, поэтому добавим директиву script_show_inputs:

#property script_show_inputs

Теперь скрипт не будет выполняться сразу после добавления его на график, а откроет окно параметров, подобно советнику или индикатору.

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

input int  RSIPeriod             = 9;     // Период RSI
input bool ShowFracrals          = false; // Показывать фракталы
input bool ShowHiddenDivergence  = false; // Показывать "скрытую" дивергенцию 

Определим строковую константу-префикс для имён графических объектов. Она понадобится нам при поиске объектов для их удаления:

#define PREFIX    "OBJ_DIV_"

Опишем структуру для хранения данных о фрактале и объявим два массива таких структур для фракталов вверх и для фракталов вниз:

struct fractal
  {
   datetime          time;      // время открытия бара, на котором был сформирован фрактал
   double            peak;      // максимум или минимум бара в зависимости от типа фрактала
   double            indicator; // значение индикатора RSI на баре
  } FractalsUp[],FractalsDown[];

Добавим в код функцию DeleteObjects, которую мы написали ранее, и вызовем её в обработчике OnStart(). Тем самым мы удалим с графика объекты, которые могли остаться после предыдущего запуска скрипта.  Установим нулевой размер у массивов фракталов и определим направление индексации в массивах, как в таймсериях (справа налево).

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   // удалим объекты, возможно оставшиеся от предыдущего запуска скрипта
   DeleteObjects(PREFIX);

   // установим нулевой размер у массивов фракталов
   ArrayResize(FractalsUp,0);
   ArrayResize(FractalsDown,0);

   // определим направление индексации в массивах фракталов как в таймсериях(справо налево)
   ArraySetAsSeries(FractalsUp,true);
   ArraySetAsSeries(FractalsDown,true);
  }

//+------------------------------------------------------------------+
//| Удаляет графические объекты с префиксом prfx                     |
//+------------------------------------------------------------------+
void DeleteObjects(string prfx)
  {
   for(int i=ObjectsTotal()-1; i>=0; i--)
     {
      string nm=ObjectName(0,i);
      if(StringSubstr(nm,0,StringLen(prfx))==prfx)
         ObjectDelete(0,nm);
     }
  }

Напишем функцию, которая заполняет массивы FractalsUp[] и FractalsDown[] найденными на истории фракталами. При этом, если параметр ShowFracrals = true, то найденные фракталы будут отображаться на графике цветными точками. Функцию отрисовки графических объектов возьмём из комплектного руководства MQL, а функцию GetFractalType, определяющую тип фракталов – из написанного нами ранее скрипта «Фракталы.mq4».

//+------------------------------------------------------------------+
//| Находит на истории все фракталы и заполняет соответствующие      |
//| массивы фракталов.                                               |
//| В зависимости от параметра ShowFracrals отображает найденные     |
//| фракталы на графике                                              |  
//+------------------------------------------------------------------+  
void ShowFractalsAndFillArrays()
{
// пропускаем по два бара в конце и в начале таймсерии
   int to=Bars-2;
   for(int i=2; i<to; i++)
     {
      string Name;// имя объекта
      bool IsUp, IsDown;
      double rsi;

      // получаем тип фрактала, если он сформирован на данном баре
      GetFractalType(i,IsUp,IsDown);

      // сформирован фрактал вверх
      if(IsUp)
        {
         rsi=iRSI(_Symbol,_Period,RSIPeriod,PRICE_CLOSE,i);
         // также есть максимум на RSI
         if(iRSI(_Symbol,_Period,RSIPeriod,PRICE_CLOSE,i+1)<rsi &&
            iRSI(_Symbol,_Period,RSIPeriod,PRICE_CLOSE,i-1)<rsi)
           {
            // добавляем данные о фрактале в массив
            int ind=AddFractalToArray(FractalsUp,iTime(_Symbol,_Period,i),
            iHigh(_Symbol,_Period,i),rsi);
 
           // рисуем над High свечи синюю точку
            if(ShowFracrals)
            {
              Name=PREFIX+_Symbol+"_FRCTL_UP_"+IntegerToString(FractalsUp[ind].time);
              ArrowCreate(0,Name,0,FractalsUp[ind].time,FractalsUp[ind].peak,159,
              ANCHOR_BOTTOM,clrBlue,STYLE_SOLID,2,true,false);
            }   
           }
        }
      // сформирован фрактал вниз
      if(IsDown)
        {
         rsi=iRSI(_Symbol,_Period,RSIPeriod,PRICE_CLOSE,i);
         // также есть минимум на RSI
         if(iRSI(_Symbol,_Period,RSIPeriod,PRICE_CLOSE,i+1)>rsi && 
            iRSI(_Symbol,_Period,RSIPeriod,PRICE_CLOSE,i-1)>rsi)
           {
            // добавляем данные о фрактале в массив
            int ind=AddFractalToArray(FractalsDown,iTime(_Symbol,_Period,i),
            iLow(_Symbol,_Period,i),rsi);
            
            // рисуем под Low свечи красную точку
            if(ShowFracrals)
            {
             Name=PREFIX+_Symbol+"_FRCTL_DOWN_"+IntegerToString(FractalsDown[ind].time);
             ArrowCreate(0,Name,0,FractalsDown[ind].time,FractalsDown[ind].peak,159,
             ANCHOR_TOP,clrRed,STYLE_SOLID,2,true,false);
            }   
           }
        }
     }
}

После того, как оба массива фракталов заполнены, нам потребуется функция, которая найдёт и разметит линиями возможные дивергенции цены и осциллятора RSI:

//+------------------------------------------------------------------+
//| Ищет и размечает трендовыми линиями дивергенции на               |
//| графике цены и осцилляторе RSI                                   |
//+------------------------------------------------------------------+
void ShowDivergence()
  {
   int size;
   string Name=PREFIX+_Symbol+"TLINE_";
// ----------- рисуем линии медвежьей дивергенции по локальным максимумам
   size=ArraySize(FractalsUp);
   for(int i=0; i<size-1; i++)
     {
      int Style=EMPTY;
      // медвежья дивергенция – цена вверх, RSI вниз
      if(FractalsUp[i].peak>FractalsUp[i+1].peak &&
         FractalsUp[i].indicator<FractalsUp[i+1].indicator)
         Style=STYLE_SOLID;
      else
         // скрытая медвежья дивергенция (конвергенция) – цена вниз, RSI вверх
         if(ShowHiddenDivergence && FractalsUp[i].peak<FractalsUp[i+1].peak &&
            FractalsUp[i].indicator>FractalsUp[i+1].indicator)
            Style=STYLE_DOT;
      if(Style>EMPTY)
        {
         TrendCreate(0,Name+"BEAR_BAR_"+IntegerToString(FractalsUp[i].time),0,
         FractalsUp[i+1].time,FractalsUp[i+1].peak,FractalsUp[i].time,FractalsUp[i].peak,
         clrMaroon,ENUM_LINE_STYLE(Style));
         TrendCreate(0,Name+"BEAR_RSI_"+IntegerToString(FractalsUp[i].time),1,
         FractalsUp[i+1].time,FractalsUp[i+1].indicator,FractalsUp[i].time,
         FractalsUp[i].indicator,clrMaroon,ENUM_LINE_STYLE(Style));
        }
     }
// ----------- рисуем линии бычьей дивергенции по локальным минимумам
   size=ArraySize(FractalsDown);
   for(int i=0; i<size-1; i++)
     {
      int Style=EMPTY;
      // бычья дивергенция – цена вниз, RSI вверх
      if(FractalsDown[i].peak<FractalsDown[i+1].peak &&    
         FractalsDown[i].indicator>FractalsDown[i+1].indicator)
         Style=STYLE_SOLID;
      else
         // скрытая бычья дивергенция (конвергенция) – цена вверх, RSI вниз
         if(ShowHiddenDivergence && FractalsDown[i].peak>FractalsDown[i+1].peak && 
            FractalsDown[i].indicator<FractalsDown[i+1].indicator)
            Style=STYLE_DOT;
      if(Style>EMPTY)
        {
          TrendCreate(0,Name+"BULL_BAR_"+IntegerToString(FractalsDown[i].time),0,
          FractalsDown[i+1].time,FractalsDown[i+1].peak,FractalsDown[i].time,
          FractalsDown[i].peak, clrGreen,ENUM_LINE_STYLE(Style));
          TrendCreate(0,Name+"BULL_RSI_"+IntegerToString(FractalsDown[i].time),1,
          FractalsDown[i+1].time,FractalsDown[i+1].indicator,FractalsDown[i].time,
          FractalsDown[i].indicator,clrGreen,ENUM_LINE_STYLE(Style));
        }
     }
  }

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

Добавим вызов функций в обработчик OnStart():

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   // удалим объекты, возможно оставшиеся от предыдущего запуска скрипта
   DeleteObjects(PREFIX);

   // установим нулевой размер у массивов фракталов
   ArrayResize(FractalsUp,0);
   ArrayResize(FractalsDown,0);

   // определим направление индексации в массивах фракталов как в таймсериях(справо налево)
   ArraySetAsSeries(FractalsUp,true);
   ArraySetAsSeries(FractalsDown,true);
   
   ShowFractalsAndFillArrays();
   ShowDivergence();
  }

Наш скрипт готов. Запустим его на часовом графике EURUSD, предварительно прикрепив к графику индикатор RSI с таким же периодом, как в скрипте:

Скрипт можно запускать и без индикатора RSI. В таком случае линии дивергенции будут построены только на графике цены.

Включим отображение фракталов (цветные точки) и скрытой дивергенции (пунктирные линии):

Заключение

В этом уроке мы узнали, как средствами языка MQL4 можно осуществлять анализ потока цен с помощью баров. Мы научились находить простые пятибарные формации – так называемые фракталы Билла Вильяма – и использовать их для определения локальных экстремумов. В итоге мы написали скрипт для автоматического поиска дивергенции цены и осциллятора RSI. Полученные решения можно применять при создании пользовательских индикаторов и торговых советников.

Тема на форуме

С уважением, Юрий Лосев aka lsv107
Tlap.com

Уроки по MQL4 , , , ,