Scriptong
30-09-2010, 17:31
Вслед за созданием волновой теории Ральфа Нельсона Эллиотта различные ее интерпретации стали появляться как грибы после дождя. В своем большинстве авторы подобных методик даже не упоминали волновую теорию как отправную точку для своих исследований. Одним из таких исследователей стал Джо ДиНаполи, наиболее известный метод торговли которого был описан в книге "Торговля с использованием уровней ДиНаполи".
Уровни ДиНаполи рассчитываются на основании двух завершенных волн в предположении о формировании третьей волны (похоже на классические три волны Р.Н. Эллиотта), противоположной по направлению второй волне. Расчет ведется с использованием соотношений двух соседних чисел ряда Фибоначчи - 0.618 и 1.618. Формализация данного метода приведена в индикаторе Dinapoli_Targets (см. рис. 1).
http://www.forextrade.ru/media/Image/MQLabs/83_ag/figure1.png
Рис. 1. Индикатор Dinapoli_Targets.
В настройках индикатора по умолчанию линия старта белая, а линия второй цели - желтая. Такие цвета подобраны для черного фона графика. Также в оригинальном исполнении индикатор не отображает линии зигзага (не путать с индикатором ZigZag), по трем последним точкам которого производится расчет уровней ДиНаполи. Нахождение экстремумов зигзага производится на основании пробития линий максимума или минимума горизонтальных каналов, состоящих из количества баров, указанного во входном параметре Length (по умолчанию 6). Вершина фиксируется при пробитии нижней границы канала, а впадина - при пробитии верхней границы. К примеру, особая длина волны ВС (см. рис. 1) объясняется отсутствием шестибарного минимума на участке 08:00 28.09.2010 - 01:00 30.09.2010. Второй входной параметр индикатора barn указывает номер бара, с которого начинается расчет экстремумов зигзага. Смысл параметра заключается в том, чтобы на указанном отрезке можно было найти три точки зигзага. Оптимальное значение barn формально вычислить невозможно, т.к. длина волны может быть какой угодно большой, и даже значение 300, используемое по умолчанию, не дает гарантии, что все три точки будут найдены.
На рис. 1 заметно одно из главных отличий методики ДиНаполи от классической трактовки трех волн Эллиотта: вторая волна ВС намного больше первой волны АВ, что не мешает ожиданию нисходящей третьей волны, размер которой скорее можно причислить к коррекции, чем к импульсной волне. В этом плане действительно нельзя утверждать, что ДиНаполи является последователем Эллиотта.
Уровень старта - это 31.8% (возможно, ошибка и требуется применить значение 38.2) длины волны АВ, которая вычтена из значения точки С. Уровень стопа располагается выше точки С на расстоянии двух спрэдов, а уровень первой цели - разность между значением точки С и 61.8% длины волны АВ. Для расчета цели 2 используется 100% волны АВ, для цели 3 - 161.8%.
Основной проблемой следования показаниям индикатора Dinapoli_Targets является частое указание им линии старта на уровне, который уже был пройден ценой, что не дает возможности сразу же открыть сделку. В таких случаях остается только надеяться, что цена вернется к начальному уровню без достижения уровня одной из целей. Второй, не менее важной, проблемой является неудобство для пользователя в том плане, что показания индикатора не привязаны к началу формирования бара и могут изменить указание направления торговли в любой момент времени. А это предполагает неусыпный контроль показаний индикатора.
Обе описанные проблемы решаются автоматизацией торгового процесса. Исходя из того, что индикатор отображает три вида целей, можно фиксировать прибыль последовательно на трех указанных уровнях. Технически это производится частичным закрытием одной сделки. Эквивалентом частичного закрытия одной большой сделки является автоматическое закрытие трех различных сделок (суммарный объем трех сделок равен объему одной большой сделки) на стороне брокера (профит), открытых на одном уровне. Этот подход предпочтительнее по той причине, что при достижении уровня цели сделка будет закрыта брокером и не зависит от наличия связи. Тот факт, что каждая из трех сделок может быть открыта по различной цене вследствие колебаний рынка, особого влияния на ход торгового процесса не окажет.
Начнем создание советника с функции GetSignal, задачей которой является расчет уровней ДиНаполи. Сразу замечаем, что для расчета значений уровней необходимо выполнение цикла с количеством итераций, равного значению barn. Если учесть, что исполнение цикла необходимо на каждом новом тике цены, то получим программу с пониженным быстродействием. Выходом из ситуации является одноразовый полный расчет всех экстремумов зигзага при запуске советника или после длительного перерыва в его работе, а при непрерывной работе произведение обновления значений точек А, В и С. Это приведет к исключению цикла из кода функции GetSignal:
//+-------------------------------------------------------------------------------------+
//| Генерация сигналов покупки и продажи |
//+-------------------------------------------------------------------------------------+
void GetSignal()
{
Signal = 0;
// - 1 - ======================== Слежение за появлением нового свинга ==================
// - 1.1 - ===================== Определение пробития канала ===========================
double LL = Low[iLowest(NULL, 0, MODE_LOW, Length, 1)]; // Нижняя граница канала
double HH = High[iHighest(NULL, 0, MODE_HIGH, Length, 1)]; // Верхняя граница канала
if (Low[0] < LL && High[0] > HH) // Если произошло пробитие в двух границ сразу
{
Swing = 2; // Свинг не определен
if (Swing_n == 1) LastHigh = Time[0]; // Если предыдущий свинг восходящий, то
// обновляется максимум
if (Swing_n == -1) LastLow = Time[0]; // если предыдущий свинг нисходящий, то
} // обновляется минимум
else // Если не было пробития обеих границ канала
{
if (Low[0] < LL) Swing = -1; // Пробита нижняя - свинг вниз
if (High[0] > HH) Swing = 1; // Пробита верхняя - свинг вверх
}
// - 1.1 - ============================= Окончание блока ===============================
// - 1.2 - ============================ Найден новый свинг =============================
if (Swing != Swing_n && Swing_n != 0) // Новый свинг при наличии предыдущего
{
if (Swing == 2) // Свинг не определен (пробиты обе границы)
{
Swing = -Swing_n; // Ориентируемся по предыдущему свингу, инвертируя направление
BH = High[0]; // Обновляем экстремумы
BL = Low[0];
}
SWA = SWB; // Смещение точек вперед во времени. Бывшая точка В становится точкой А,
SWB = SWC; // а бывшая точка С становится точкой В
if (Swing == 1) // Если новый (формирующийся) свинг восходящий, то
{
SWC = BL; // сформированный свинг - нисходящий. Точка С - минимум
LastSwing = LastLow; // Время формирования нисходящего свинга
}
if (Swing == -1) // Если новый (формирующийся) свинг нисходящий, то
{
SWC = BH; // сформированный свинг - восходящий. Точка С - максимум
LastSwing = LastHigh; // Время формирования восходящего свинга
}
BH = High[0]; // Обновляем экстремумы
BL = Low[0];
}
// - 1.2 - ============================= Окончание блока ===============================
// - 1.3 - ===================== Слежение за формирующимся свингом =====================
if (Swing == 1) // Свинг восходящий
if (High[0] >= BH) // Если свинг обновлен, то
{
BH = High[0]; // обновляем максимум
LastHigh = Time[0]; // и запоминаем время обновления
}
if (Swing == -1) // Свинг нисходящий
if (Low[0]<= BL) // Если свинг обновлен, то
{
BL = Low[0]; // обновляем минимум
LastLow = Time[0]; // и запоминаем время обновления
}
Swing_n = Swing;
// - 1.3 - ============================= Окончание блока ===============================
// - 1.4 - ================= Расчет целей по найденным точкам А, В и С =================
Target[0] = NP((SWB - SWA)*0.618 + SWC);
Target[1] = NP(SWB - SWA + SWC);
Target[2] = NP((SWB - SWA)*1.618 + SWC);
// - 1.4 - ============================= Окончание блока ===============================
// - 1 - ========================== Окончание блока =====================================
if (LastSwing == LastSignal) // Если новый свинг не сформирован, то и сигнал
return; // формировать не нужно
LastSignal = LastSwing; // Если найден новый законченный свинг, то
// запомним его время
// - 2 - ======================== Генерация сигнала покупки =============================
if (SWB > SWC) // Точка В выше точки С, значит, ожидается восходящая третья волна
{
Start = NP((SWB - SWA)*0.318 + SWC + Spread); // Уровень входа
Stop = NP(SWC - 2*Spread); // Уровень стоп-приказа
Signal = 1; // Сигнал к покупке
}
// - 2 - ========================== Окончание блока =====================================
// - 3 - ======================== Генерация сигнала продажи =============================
if (SWB < SWC) // Точка В ниже точки С, значит, ожидается нисходящая третья волна
{
Start = NP((SWB - SWA)*0.318 + SWC - Spread); // Уровень входа
Stop = NP(SWC + 2*Spread); // Уровень стоп-приказа
Signal = -1; // Сигнал к продаже
}
// - 3 - ========================== Окончание блока =====================================
}
Первый блок функции, за исключением нескольких преобразований, повторяет код основного цикла индикатора Dinapoli_Targets. Вложенный блок 1.1 определяет границы горизонтального канала (переменные LL и HH) и отслеживает момент их пробития. Наряду с одиночным пробитием верхней или нижней границы учитывается вариант пробития обеих границ канала одной свечей. В таком спорном случае принимается сторона формирующегося свинга (свинг - линия, соединяющая соседние максимум и минимум зигзага): если свинг восходящий, то обновляется вершина, если свинг нисходящий, то обновляется впадина. Время формирования последнего экстремума формирующегося свинга сохраняется в переменных LastHigh и LastLow в соответствии с типом свинга. Если одна свеча пробила лишь одну из границ канала, то обновляется значение переменной Swing, отвечающей за направление формирующегося свинга (1 - восходящий, -1 - нисходящий, 0 или 2 - не определен).
Вложенный блок 1.2 выполняется в случае, если обнаружено пробитие границы канала. Определяется это по неравенству направления текущего (Swing) и предыдущего (Swing_n) свингов. В этом случае необходимо зафиксировать новую точку С, в то время как предыдущая точка С станет точкой В, предыдущая точка В - точкой А, а предыдущая точка А будет исключена из анализа. Текущие значения точек хранятся в переменных SWА, SWВ и SWС. Значение новой точки С определяется как последний минимум (переменная BL), если формируется восходящий свинг, и как последний максимум (переменная BH), если формируется нисходящий свинг.
Функцией вложенного блока 1.3 является обновление значений переменных BL и BH во время формирования свинга. Вместе с ними происходит обновление значений переменных LasLow и LastHigh. В блоке 1.4 производится расчет значений целей Target1, Target2 и Target3.
С окончанием блока 1 проверяется "свежесть" точки С. Для этого значение переменной LastSwing, в которой хранится время открытия бара, соответствующего точке С, сравнивается со значением переменной LastSignal, в которой хранится время последней обработанной точки С. Если значения не совпадают, то можно говорить о формировании новой точки С. При совпадении значений функция GetSignal прерывается, что оставляет нулевое значение переменной Signal.
Блоки 2 и 3 алгоритмически идентичны. Если значение точки В больше значения точки С, то свинг ВС - нисходящий, что означает ожидание восходящего свинга, т.е. предполагается рост цены. В этом случае подается сигнал к совершению покупки с расчетом значений Stop и Start. Если значение точки В меньше значения точки С, то свинг ВС - восходящий, что означает ожидание падения цены; сигнал к продаже.
Для подготовки большинства значений переменных, использующихся в теле функции GetSignal, используется функция SwingCalculation. Обращение к ней производится в двух случаях: старт эксперта и долгое отсутствие связи с сервером брокера. В результате такого подхода скорость выполнения программы значительно увеличивается, т.к. функция SwingCalculation занимает значительное процессорное время. Особенно это заметно при тестировании эксперта в тестере стратегий.
Код SwingCalculation в рамках статьи рассматриваться не будет, т.к. практически полностью повторяет код первого блока GetSignal. Отличие заключается в начальной инициализации переменных и использовании цикла, проходящего barn баров для расчета как можно большего числа экстремумов.
Рассмотрим, как производится управление дочерними функциями в теле главной функции start:
//+-------------------------------------------------------------------------------------+
//| Функция start эксперта |
//+-------------------------------------------------------------------------------------+
int start()
{
// - 1 - ========================== Можно ли работать эксперту? =========================
if (!Activate || FatalError) return(0);
// - 1 - ============================= Окончание блока ==================================
// - 2 - =============== Получение рыночных условий и слежение за связью ================
if (!IsTesting()) // Если производится тестирование, то слежение не требуется
{
Tick = MarketInfo(Symbol(), MODE_TICKSIZE); // минимальный тик
Spread = ND(MarketInfo(Symbol(), MODE_SPREAD)*Point); // текущий спрэд
StopLevel = ND(MarketInfo(Symbol(), MODE_STOPLEVEL)*Point);//текущий уровень стопов
FreezeLevel = ND(MarketInfo(Symbol(), MODE_FREEZELEVEL)*Point);// уровень заморозки
if (Time[0] - LastConnect > Period()*60 || LastBars < Bars-1) // Последний
SwingsCalculation();// посчитанный бар был более одной свечи назад или изменилось
// количество баров
LastConnect = Time[0]; // Обновляем время последнего сбора информации
}
// - 2 - ============================= Окончание блока ==================================
// - 3 - ========================== Расчет сигналов открытия и закрытия =================
if (High[0] >= High[1] || Low[0] <= Low[1] ||// Произошло пробитие последней свечи или
CalcSignal != LastSignal) // по последнему сигналу закончены не все операции
{
GetSignal(); // Расчет текущего свинга
if (CalcSignal != LastSignal) // Если найден новый свинг
if (!Trade()) // Установка ордеров, открытие и закрытие сделок
return(0);
CalcSignal = LastSignal; // Все операции завершены успешно
}
// - 3 - ============================= Окончание блока ==================================
return(0);
}
Второй блок функции start выполняется только при работе советника онлайн. Кроме обновления информации о рыночном окружении (тик, спрэд, своп, уровень заморозки) здесь отслеживается такая нештатная ситуация, как обрыв связи. Обрыв связи определяется по двум признакам: отсутствие прихода котировок в течение времени больше, чем один периода графика и изменение количества баров больше, чем на один. В обоих случаях происходит вызов функции SwingCalculation, производящей пересчет экстремумов зигзага. Очевидно, что к первому признаку отсутствия связи будет относиться каждый понедельник, т.к. котировки не поступают в течение субботы и воскресенья, а это больше, чем один период графика вплоть до дневного. Второй признак отсутствия связи необходим в случае возобновления связи. Если внимательно следить за процессом возобновления связи после долгого ее обрыва, то можно заметить, что сначала поступает самая свежая котировка без обновления пропущенного периода истории. Это может привести к получению неправильных данных. Поэтому в момент окончания подкачки всей пропущенной истории (изменится количество баров) будет произведен очередной пересчет значений индикатора Dinapoli_Targets.
Третий блок реализует еще один аспект увеличения быстродействия советника - ограничивает количество вызовов функции GetSignal. Этот подход основан на том утверждении, что для появления нового экстремума зигзага требуется выход цены за пределы предыдущей свечи, т.е. чтобы текущая цена была ниже минимума или выше максимума свечи 1. В этом случае также учитывается, что после предыдущего расчета сигнала были выполнены не все торговые операции. Поэтому к критериям выхода цены за границы предыдущей свечи добавляется проверка окончания всех торговых операций путем сравнения времени последней сформированной точки С (LastSignal) со значением переменной CalcSignal. В CalcSignal сохраняется время последней обработанной точки С.
Выполнение торговых операций возложено на функцию Trade, которая, по традиции, делится на два алгоритмически подобных блока:
//+-------------------------------------------------------------------------------------+
//| Открытие позиций |
//+-------------------------------------------------------------------------------------+
bool Trade()
{
// - 1 - ==================== Открытие длинных позиций ==================================
if (Signal > 0)
{
// - 1.1 - ================== Поиск ордеров и проверка ордеров Sell =================
FindOrders(); // Сбор информации по своим ордерам
if (!CheckSells()) // Удаление отложенных Sell-ордеров, модификация уровня профита
return(False); // и закрытие сделок Sell. При неудаче ждем следующего тика
// - 1.1 - ============================ Окончание блока =============================
// Наибольшее значение, достигнутое ценой после точки С
double HighValue = High[iHighest(NULL, 0, MODE_HIGH, iBarShift(NULL, 0, LastSwing))];
for (int i = 0; i < 3; i++) // Проверка трех Buy-ордеров
// - 1.2 - ===================== Buy-ордер номер i существует =======================
if (BuyTicket[i] > 0) // Если ордер заданного номера существует
{
RefreshRates();
if (OrderSelect(BuyTicket[i], SELECT_BY_TICKET) && OrderCloseTime() == 0)
if (OrderType() > 0) // По условиям отложенный ордер от предыдущего сигнала
{ // существовать не может. Поэтому удаляем его при
if (!DeleteOrder()) // нахождении.
return(False);
}
else // Если рыночный ордер, то проверить правильность
{ // стопа и профита.
if (Target[i] - Bid < StopLevel || // Если выше профита или близко к нему,
Bid - Stop < StopLevel) // то закрыть. Если ниже стопа или
{ // близко к нему, то закрыть.
if (!CloseOrder())
return(False);
}
else// Если стоп и профит от цены далеко, то перемещение на новые значения
if (!CheckSLTP(Target[i]))
return(False);
}
}
// - 1.2 - ============================ Окончание блока =============================
// - 1.3 - ===================== Buy-ордер номер i не существует ====================
else // Ордер заданного номера не существует - открыть:
{
if (HighValue >= Target[i]) continue; // Если цена уже достигла цели, то ничего
RefreshRates(); // не делаем
if (Start - Ask > StopLevel) // 1) Если Ask ниже цены Start - Buy Stop
{
if (OpenOrderCorrect(OP_BUYSTOP, Lots[i], NP(Start+Spread), NP(Stop),
NP(Target[i]), i) != 0)
return(False);
}
else
if (Ask - Start > StopLevel) // 2) Если Ask выше цены Start - BuyLimit
{
if (OpenOrderCorrect(OP_BUYLIMIT, Lots[i], NP(Start+Spread), NP(Stop),
NP(Target[i]), i) != 0)
return(False);
}
else // 3) Если Ask близко к цене Start - Buy
if (OpenOrderCorrect(OP_BUY, Lots[i], NP(Ask), NP(Stop),
NP(Target[i]), i) != 0)
return(False);
}
// - 1.3 - ============================ Окончание блока =============================
}
// - 1 - ================================ Окончание блока ===============================
// - 2 - ==================== Открытие короткой позиции =================================
if (Signal < 0)
{
// - 2.1 - ================== Поиск ордеров и проверка ордеров Buy ==================
FindOrders(); // Сбор информации по своим ордерам
if (!CheckBuys()) // Удаление отложенных Buy-ордеров, модификация уровней стопа,
return(False); // профита и закрытие сделок Buy. При неудаче ждем следующего тика
// - 2.1 - ============================ Окончание блока =============================
// Наименьшее значение, достигнутое ценой после точки С
double LowValue = Low[iLowest(NULL, 0, MODE_LOW, iBarShift(NULL, 0, LastSwing))];
for (i = 0; i < 3; i++) // Проверка трех Sell-ордеров
// - 2.2 - ===================== Sell-ордер номер i существует ======================
if (SellTicket[i] > 0) // Если ордер заданного номера существует
{
RefreshRates();
if (OrderSelect(SellTicket[i], SELECT_BY_TICKET) && OrderCloseTime() == 0)
if (OrderType() > 1) // По условиям отложенный ордер от предыдущего
{ // сигнала существовать не может. Поэтому удаляем
if (!DeleteOrder()) // его при нахождении.
return(False);
}
else // Если рыночный ордер, то проверить правильность
{ // стопа и профита.
if (Ask - Target[i] < StopLevel || // Если ниже профита или близко к нему,
Stop - Ask < StopLevel) // то закрыть. Если выше стопа или
{ // близко к нему, то закрыть.
if (!CloseOrder())
return(False);
}
else// Если стоп и профит от цены далеко, то перемещение на новые значения
if (!CheckSLTP(Target[i]))
return(False);
}
}
// - 2.2 - ============================ Окончание блока =============================
// - 2.3 - ===================== Sell-ордер номер i не существует ===================
else // Ордер заданного номера не существует - открыть:
{
if (LowValue <= Target[i]) continue; // Если цена уже достигла цели, то ничего
RefreshRates(); // не делаем
if (Bid - Start > StopLevel) // 1) Bid выше цены Start - Sell Stop
{
if (OpenOrderCorrect(OP_SELLSTOP, Lots[i], NP(Start), NP(Stop + Spread),
NP(Target[i] + Spread), i) != 0)
return(False);
}
else
if (Start - Bid > StopLevel) // 2) Bid ниже цены Start - Sell Limit
{
if (OpenOrderCorrect(OP_SELLLIMIT, Lots[i], NP(Start), NP(Stop + Spread),
NP(Target[i] + Spread),i) != 0)
return(False);
}
else // 3) Bid близко к цене Start - Sell
if (OpenOrderCorrect(OP_SELL, Lots[i], NP(Bid), NP(Stop + Spread),
NP(Target[i] + Spread), i) != 0)
return(False);
}
// - 2.3 - ============================ Окончание блока =============================
}
// - 2 - ==================== Окончание блока ===========================================
return(True);
}
Логика советника в отношении уровней Dinapoli должна позволять обрабатывать все проблемные ситуации: нахождение текущей цены дальше точки входа (уровень Start) или дальше каждой из целей (Target1, Target2 и Target3). Так, если уровень входа находится не в непосредственной близости от текущей цены, то советник должен установить отложенный ордер по цене Start (лимитный или стоповый, в зависимости от ситуации). При этом противоположные ордера также должны быть модифицированы в соответствии с новыми условиями. Все это учитывается в логике функции Trade и ее дочерних функций.
При получении сигнала к покупке (Signal > 0, блок 1) действия эксперта должны быть следующими:
1) Проверка существования ордеров Sell-типа, к которым относятся Sell, Sell Stop и Sell Limit. Если обнаружены отложенные ордера, то их следует удалить. Если же обнаружены открытые сделки, то уровень их закрытия необходимо переместить на уровень открытия сделок Buy. При нахождении уровня Start в непосредственной близости от цены (что исключает модификацию уровня стопа или профита), сделка закрывается. Если Start находится выше текущей цены, то на ее значение перемещается уровень стопа, а если Start находится ниже текущей цены, то на ее значение перемещается уровень профита. Эти действия выполняются при помощи вызова функции CheckSells, которая будет возвращать значение False до тех пор, пока не выполнит все возложенные на нее обязанности.
2) Случай существования ордеров Buy-типа, к которым относятся Buy, Buy Stop и Buy Limit. Так как количество направленных в одну сторону ордеров советника достигает числа 3, то проверка проводится при помощи цикла из трех итераций. Если обнаружен отложенный ордер, то он должен быть удален, т.к. существование отложенных ордеров от предыдущего сигнала к покупкам исключено. Это означает лишь наличие какой-либо ошибки технического плана. Если обнаружена сделка Buy, то изменяются уровни ее стопа и профита (вызов функции CheckSLTP), т.к. сделка не успела закрыться за время существования сигнала к продажам. В случае невозможности изменения уровней стопа и профита на новые значения из-за их близости или прохождения ценой соответствующего значения, сделка принудительно закрывается (вызов функции CloseOrder). Все эти действия выполняются во вложенном блоке 1.2.
3) Случай отсутствия ордера Buy-типа. Производится открытие ордера, при условии, что за время, прошедшее после формирования точки С, не была достигнута соответствующая цель (1, 2 или 3). Если цена Start намного ниже текущей цены, то устанавливается ордер BuyLimit, если намного выше - BuyStop. При нахождении текущей цены в непосредственной близости от уровня Start происходит открытие сделки по рыночной цене. Такие действия выполняет блок 1.3.
Подобный алгоритм с учетом специфики коротких позиций заложен в блоке 2. Перейдем к рассмотрению дочерних функций функции Trade: CheckBuys (аналог - CheckSell) и CheckSLTP.
//+-------------------------------------------------------------------------------------+
//| Поиск своих ордеров Buy-типа (BuyStop, BuyLimit, Buy). Если найден отложенный |
//| ордер, то он удаляется. Если обнаружена сделка, то ее профит или стоп, в зависимости|
//| от положения уровня Start, перемещается на уровень Start. |
//+-------------------------------------------------------------------------------------+
bool CheckBuys()
{
// - 1 - ============================ Выбор ордера ======================================
for (int i = 0; i < 3; i++) // Используется весь список ордеров Buy
if (BuyTicket[i] > 0) // Ордер найден
if (OrderSelect(BuyTicket[i], SELECT_BY_TICKET) && // Убедимся, что ордер выбран и
OrderCloseTime() == 0) // находится в списке открытых
// - 1 - =========================== Окончание блока ====================================
// - 2 - ====================== Операции с рыночным ордером =============================
if (OrderType() == OP_BUY) // Текущая сделка - длинная
{
RefreshRates(); // Обновление значений Bid и Ask
if (Start <= Bid) // Если Start ниже Bid, то будет изменен стоп позиции
{
if (Bid - Start <= StopLevel) // Если цена слищком близко к Start, то
{
if (!CloseOrder()) // закрываем сделку
return(False);
}
else
if (!ModifySL(NP(Start))) // Если не удалось изменить уровень
return(False); // стопа, то вернем ошибку
}
else
if (Start > Bid) // Если Start выше Bid, то будет изменен профит позиции
{
if (Start - Bid <= StopLevel) // Если цена слищком близко к Start, то
{
if (!CloseOrder()) // закрываем сделку
return(False);
}
else
if (!ModifyTP(NP(Start))) // Если не удалось изменить уровень
return(False); // профита, то вернем ошибку
}
}
// - 2 - =========================== Окончание блока ====================================
// - 3 - ====================== Операции с отложенным ордером ===========================
else
if (!DeleteOrder()) // Ордер должен быть удален
return(False);
// - 3 - =========================== Окончание блока ====================================
return(True); // Все действия завершены успешно
}
Вспомним, что функция CheckBuys вызывается только при получении сигнала к продаже. Поэтому ее предназначением является ликвидация ордеров Buy-типа.
В первом блоке происходит выбор нужного ордера Buy-типа. Если ордер успешно выбран, то выполняется второй или третий блок, в зависимости от того, рыночный это ордер или отложенный.
Если ордер рыночный, то выполняется блок 2. В нем происходит оценка удаленности уровня Start от текущей цены. Если Start выше уровня Bid, то производится изменение уровня профита, который будет указывать на уровень Start. Если модификация профита невозможна по причине близости нового уровня, то рыночный ордер закрывается. Если Start ниже Bid, то производится изменение уровня стоп-приказа. Если изменение стоп-приказа невозможно, то рыночный ордер закрывается.
В случае выбора отложенного ордера исполняется блок 3, где при помощи вызова функции DeleteOrder происходит его удаление.
Алгоритмически подобна данной функции функция CheckSells. Поэтому следующей рассмотрим функцию CheckSLTP:
//+-------------------------------------------------------------------------------------+
//| Проверка правильности параметров рыночного ордера |
//+-------------------------------------------------------------------------------------+
bool CheckSLTP(double Target)
{
double SL = NP(OrderStopLoss()); // Текущие значения стопа и профита
double TP = NP(OrderTakeProfit());
double ComSL = Stop; // Требуемые значения стопа и профита
double ComTP = Target;
if (OrderType() == OP_SELL) // если сделка короткая, то к требуемым значениям нужно
{
ComSL += Spread; // добавить спрэд
ComTP += Spread;
}
if (MathAbs(ComSL - SL) >= Tick) // Если старый стоп не равен новому, то изменяем стоп
SL = NP(ComSL);
if (MathAbs(TP - ComTP) >= Tick) // если старый профит не равен новому, то меняем профит
TP = NP(ComTP);
if (MathAbs(SL - OrderStopLoss()) >= Tick || // Если нужно изменить стоп или
MathAbs(TP - OrderTakeProfit()) >= Tick) // профит
if (WaitForTradeContext()) // Свободен ли торговый поток?
{
if (!OrderModify(OrderTicket(), 0, SL, TP, 0)) // модификация стоп или профита, или
return(False); // обоих вместе
}
else
return(False);
return(True); // Модификация успешно завершена
}
Функция едина для ордеров Buy и Sell. Поэтому сравнение производится в различными значениями - для Sell уровень стоп-приказа и профита отличается от аналогичных уровней Buy на один спрэд. Если сравниваемое значение (ComSL или ComTP) не равно текущему значению (SL или TP), то текущее значение параметра изменяется на новое. Логика функции построена таким образом, что за одно обращение к серверу функции OrderModify производится изменение как одного, так и двух параметров сразу. Если на одном из шагов была получена ошибка, то функция вернет значение False. При успешном выполнении всех операций будет возвращено значение True.
Тестирование советника
Напоминаю, что методика тестирования экспертов была немного изменена. Об этом можно прочесть в статье Объем сделки и вероятность ее успешности (http://fxtrade.ru/mqlabs/16.09.2010-mqlabs-obem-sdelki-i-veroyatnost-ee-uspeshnosti), раздел "Тестирование советника". Там же можно взять файлы истории котировок за 2009 и 2010 годы. К ним были выпущены корректирующие файлы по EURUSD и USDCHF, которые можно взять по следующим ссылкам: EURUSD (http://www.forextrade.ru/media/Image/MQLabs/82_ag/EURUSD1.zip) и USDCHF (http://www.forextrade.ru/media/Image/MQLabs/82_ag/USDCHF1.zip).
Так как советник оперирует тремя сделками одного типа одновременно, то для каждого номера (1, 2 или 3), которые различаются по размеру профита, объем устанавливается индивидуально. По умолчанию сделка с наименьшим профитом (цель 1) обладает наибольшим объемом (параметр LotsSmall), со средним профитом (цель 2) - средним объемом (параметр LotsMedium), с наибольшим профитом (цель 3) - наименьшим объемом (параметр LotsLarge). Все эти данные могут быть изменены по усмотрению пользователя.
Тестирование советника Dinapoli_Expert производилось на участке с 01.01.2009 до 25.09.2010. Период графика - Н1. Из входных параметров эксперта только один был изменяемым. Это параметр Length. Все остальные параметры имели значения по умолчанию. Результаты тестирования приведены на рис. 2 - 5.
http://www.forextrade.ru/media/Image/MQLabs/83_ag/EURUSD.gif
Рис. 2. Результаты тестирования эксперта Dinapoli_Expert на валютной паре EURUSD.
EURUSD. Значение параметра Length равно 50. Кривая баланса ясно дает понять, что советник неустойчив на данной валютной паре. Не менее наглядно это выражается в цифрах: чистая прибыль 5649 долларов против максимальной просадки 7112 долларов, что приводит к фактору восстановления менее единицы.
http://www.forextrade.ru/media/Image/MQLabs/83_ag/USDCHF.gif
Рис. 3. Результаты тестирования эксперта Dinapoli_Expert на валютной паре USDCHF.
USDCHF. Значение параметра Length равно 31. Стабильности в кривой баланса не ощущается, что к доверию не располагает. Позитивом является лишь рост кривой ближе к концу тестирования. Чистая прибыль 4590 долларов против максимальной просадки 1908 долларов. Фактор восстановления 2.4.
http://www.forextrade.ru/media/Image/MQLabs/83_ag/GBPUSD.gif
Рис. 4. Результаты тестирования эксперта Dinapoli_Expert на валютной паре GBPUSD.
GBPUSD. Значение параметра Length равно 47. Нисходящая форма кривой наиболее полно характеризует применение уровней ДиНаполи совместно с фунтом. Стоит заметить, что это один из лучших результатов, показанных советником Dinapoli_Expert на валютной паре GBPUSD.
http://www.forextrade.ru/media/Image/MQLabs/83_ag/USDJPY.gif
Рис. 5. Результаты тестирования эксперта Dinapoli_Expert на валютной паре USDJPY.
USDJPY. Значение параметра Length равно 8. Наиболее приемлемый из сегодняшних результатов. В пользу доверия к результатам говорит достаточно большое количество совершенных сделок (более 1000) и стабильный рост кривой баланса, слегка подпорченный в начале тестирования. В цифрах успешность стратегии применительно к йене не так очевидна: чистая прибыль 5294 доллара против максимальной просадки 2494 доллара. Фактор восстановления 2.12.
Все результаты были представлены с учетом реального применения стратегии. То есть во всех случаях минимальный рабочий депозит для текущих настроек эксперта предполагается 10 000 долларов. При работе с другим размером депозита объемы позиций должны быть соответствующим образом изменены.
Доработка стратегии для использования в AutoGraf 4.0
В настроечные параметры AutoGraf из разработанного эксперта перейдет четыре входных параметра: LotsSmall - AT_1, LotsMedium - AT_2, LotsLarge - AT_3 и Length - AT_4. Настройка объема позиции при помощи панели настроек AutoGraf (параметр Lot) для данной стратегии невозможна.
Запуск стратегии Dinapoli_Expert в среде AutoGraf 4.0 состоит из следующих шагов:
Получить файл по ссылке Файлы стратегий для AutoGraf 4.0 (http://www.forextrade.ru/media/Image/MQLabs/83_ag/AG_Dinapoli.zip) и распаковать полученный архив в папку MT4\experts\libraries (с перезаписью файлов AG_AT.ex4 и AG_AT.mq4). Запустить AutoGraf. Для работы стратегии в ключе приведенных результатов в окне настроек AutoGraf (закладка "Входные параметры") установить нужные значения параметров AT_1 - AT_4. Полное повторение результатов при этом не гарантируется. Выбрать стратегию №5. Для этого необходимо передвинуть вверх значок So и среди названий стратегий найти значок S5, который также потянуть вверх. Запустить функцию автоматической торговли, передвинув значок AT в верхнее положение.
Советник Dinapoli (http://www.forextrade.ru/media/Image/MQLabs/83_ag/Dinapoli_Expert.mq4)
Индикатор Dinapoli Targets (http://www.forextrade.ru/media/Image/MQLabs/83_ag/DinapoliTargets.mq4)
Файлы стратегий для AutoGraf 4.0 (http://www.forextrade.ru/media/Image/MQLabs/83_ag/AG_Dinapoli.zip)
Развернутые результаты тестирования эксперта (http://www.forextrade.ru/media/Image/MQLabs/83_ag/Test.zip)
Использование полученного советника рекомендуется только в полуавтоматическом режиме под присмотром трейдера и после всестороннего изучения слабых и сильных сторон стратегии.
Уровни ДиНаполи рассчитываются на основании двух завершенных волн в предположении о формировании третьей волны (похоже на классические три волны Р.Н. Эллиотта), противоположной по направлению второй волне. Расчет ведется с использованием соотношений двух соседних чисел ряда Фибоначчи - 0.618 и 1.618. Формализация данного метода приведена в индикаторе Dinapoli_Targets (см. рис. 1).
http://www.forextrade.ru/media/Image/MQLabs/83_ag/figure1.png
Рис. 1. Индикатор Dinapoli_Targets.
В настройках индикатора по умолчанию линия старта белая, а линия второй цели - желтая. Такие цвета подобраны для черного фона графика. Также в оригинальном исполнении индикатор не отображает линии зигзага (не путать с индикатором ZigZag), по трем последним точкам которого производится расчет уровней ДиНаполи. Нахождение экстремумов зигзага производится на основании пробития линий максимума или минимума горизонтальных каналов, состоящих из количества баров, указанного во входном параметре Length (по умолчанию 6). Вершина фиксируется при пробитии нижней границы канала, а впадина - при пробитии верхней границы. К примеру, особая длина волны ВС (см. рис. 1) объясняется отсутствием шестибарного минимума на участке 08:00 28.09.2010 - 01:00 30.09.2010. Второй входной параметр индикатора barn указывает номер бара, с которого начинается расчет экстремумов зигзага. Смысл параметра заключается в том, чтобы на указанном отрезке можно было найти три точки зигзага. Оптимальное значение barn формально вычислить невозможно, т.к. длина волны может быть какой угодно большой, и даже значение 300, используемое по умолчанию, не дает гарантии, что все три точки будут найдены.
На рис. 1 заметно одно из главных отличий методики ДиНаполи от классической трактовки трех волн Эллиотта: вторая волна ВС намного больше первой волны АВ, что не мешает ожиданию нисходящей третьей волны, размер которой скорее можно причислить к коррекции, чем к импульсной волне. В этом плане действительно нельзя утверждать, что ДиНаполи является последователем Эллиотта.
Уровень старта - это 31.8% (возможно, ошибка и требуется применить значение 38.2) длины волны АВ, которая вычтена из значения точки С. Уровень стопа располагается выше точки С на расстоянии двух спрэдов, а уровень первой цели - разность между значением точки С и 61.8% длины волны АВ. Для расчета цели 2 используется 100% волны АВ, для цели 3 - 161.8%.
Основной проблемой следования показаниям индикатора Dinapoli_Targets является частое указание им линии старта на уровне, который уже был пройден ценой, что не дает возможности сразу же открыть сделку. В таких случаях остается только надеяться, что цена вернется к начальному уровню без достижения уровня одной из целей. Второй, не менее важной, проблемой является неудобство для пользователя в том плане, что показания индикатора не привязаны к началу формирования бара и могут изменить указание направления торговли в любой момент времени. А это предполагает неусыпный контроль показаний индикатора.
Обе описанные проблемы решаются автоматизацией торгового процесса. Исходя из того, что индикатор отображает три вида целей, можно фиксировать прибыль последовательно на трех указанных уровнях. Технически это производится частичным закрытием одной сделки. Эквивалентом частичного закрытия одной большой сделки является автоматическое закрытие трех различных сделок (суммарный объем трех сделок равен объему одной большой сделки) на стороне брокера (профит), открытых на одном уровне. Этот подход предпочтительнее по той причине, что при достижении уровня цели сделка будет закрыта брокером и не зависит от наличия связи. Тот факт, что каждая из трех сделок может быть открыта по различной цене вследствие колебаний рынка, особого влияния на ход торгового процесса не окажет.
Начнем создание советника с функции GetSignal, задачей которой является расчет уровней ДиНаполи. Сразу замечаем, что для расчета значений уровней необходимо выполнение цикла с количеством итераций, равного значению barn. Если учесть, что исполнение цикла необходимо на каждом новом тике цены, то получим программу с пониженным быстродействием. Выходом из ситуации является одноразовый полный расчет всех экстремумов зигзага при запуске советника или после длительного перерыва в его работе, а при непрерывной работе произведение обновления значений точек А, В и С. Это приведет к исключению цикла из кода функции GetSignal:
//+-------------------------------------------------------------------------------------+
//| Генерация сигналов покупки и продажи |
//+-------------------------------------------------------------------------------------+
void GetSignal()
{
Signal = 0;
// - 1 - ======================== Слежение за появлением нового свинга ==================
// - 1.1 - ===================== Определение пробития канала ===========================
double LL = Low[iLowest(NULL, 0, MODE_LOW, Length, 1)]; // Нижняя граница канала
double HH = High[iHighest(NULL, 0, MODE_HIGH, Length, 1)]; // Верхняя граница канала
if (Low[0] < LL && High[0] > HH) // Если произошло пробитие в двух границ сразу
{
Swing = 2; // Свинг не определен
if (Swing_n == 1) LastHigh = Time[0]; // Если предыдущий свинг восходящий, то
// обновляется максимум
if (Swing_n == -1) LastLow = Time[0]; // если предыдущий свинг нисходящий, то
} // обновляется минимум
else // Если не было пробития обеих границ канала
{
if (Low[0] < LL) Swing = -1; // Пробита нижняя - свинг вниз
if (High[0] > HH) Swing = 1; // Пробита верхняя - свинг вверх
}
// - 1.1 - ============================= Окончание блока ===============================
// - 1.2 - ============================ Найден новый свинг =============================
if (Swing != Swing_n && Swing_n != 0) // Новый свинг при наличии предыдущего
{
if (Swing == 2) // Свинг не определен (пробиты обе границы)
{
Swing = -Swing_n; // Ориентируемся по предыдущему свингу, инвертируя направление
BH = High[0]; // Обновляем экстремумы
BL = Low[0];
}
SWA = SWB; // Смещение точек вперед во времени. Бывшая точка В становится точкой А,
SWB = SWC; // а бывшая точка С становится точкой В
if (Swing == 1) // Если новый (формирующийся) свинг восходящий, то
{
SWC = BL; // сформированный свинг - нисходящий. Точка С - минимум
LastSwing = LastLow; // Время формирования нисходящего свинга
}
if (Swing == -1) // Если новый (формирующийся) свинг нисходящий, то
{
SWC = BH; // сформированный свинг - восходящий. Точка С - максимум
LastSwing = LastHigh; // Время формирования восходящего свинга
}
BH = High[0]; // Обновляем экстремумы
BL = Low[0];
}
// - 1.2 - ============================= Окончание блока ===============================
// - 1.3 - ===================== Слежение за формирующимся свингом =====================
if (Swing == 1) // Свинг восходящий
if (High[0] >= BH) // Если свинг обновлен, то
{
BH = High[0]; // обновляем максимум
LastHigh = Time[0]; // и запоминаем время обновления
}
if (Swing == -1) // Свинг нисходящий
if (Low[0]<= BL) // Если свинг обновлен, то
{
BL = Low[0]; // обновляем минимум
LastLow = Time[0]; // и запоминаем время обновления
}
Swing_n = Swing;
// - 1.3 - ============================= Окончание блока ===============================
// - 1.4 - ================= Расчет целей по найденным точкам А, В и С =================
Target[0] = NP((SWB - SWA)*0.618 + SWC);
Target[1] = NP(SWB - SWA + SWC);
Target[2] = NP((SWB - SWA)*1.618 + SWC);
// - 1.4 - ============================= Окончание блока ===============================
// - 1 - ========================== Окончание блока =====================================
if (LastSwing == LastSignal) // Если новый свинг не сформирован, то и сигнал
return; // формировать не нужно
LastSignal = LastSwing; // Если найден новый законченный свинг, то
// запомним его время
// - 2 - ======================== Генерация сигнала покупки =============================
if (SWB > SWC) // Точка В выше точки С, значит, ожидается восходящая третья волна
{
Start = NP((SWB - SWA)*0.318 + SWC + Spread); // Уровень входа
Stop = NP(SWC - 2*Spread); // Уровень стоп-приказа
Signal = 1; // Сигнал к покупке
}
// - 2 - ========================== Окончание блока =====================================
// - 3 - ======================== Генерация сигнала продажи =============================
if (SWB < SWC) // Точка В ниже точки С, значит, ожидается нисходящая третья волна
{
Start = NP((SWB - SWA)*0.318 + SWC - Spread); // Уровень входа
Stop = NP(SWC + 2*Spread); // Уровень стоп-приказа
Signal = -1; // Сигнал к продаже
}
// - 3 - ========================== Окончание блока =====================================
}
Первый блок функции, за исключением нескольких преобразований, повторяет код основного цикла индикатора Dinapoli_Targets. Вложенный блок 1.1 определяет границы горизонтального канала (переменные LL и HH) и отслеживает момент их пробития. Наряду с одиночным пробитием верхней или нижней границы учитывается вариант пробития обеих границ канала одной свечей. В таком спорном случае принимается сторона формирующегося свинга (свинг - линия, соединяющая соседние максимум и минимум зигзага): если свинг восходящий, то обновляется вершина, если свинг нисходящий, то обновляется впадина. Время формирования последнего экстремума формирующегося свинга сохраняется в переменных LastHigh и LastLow в соответствии с типом свинга. Если одна свеча пробила лишь одну из границ канала, то обновляется значение переменной Swing, отвечающей за направление формирующегося свинга (1 - восходящий, -1 - нисходящий, 0 или 2 - не определен).
Вложенный блок 1.2 выполняется в случае, если обнаружено пробитие границы канала. Определяется это по неравенству направления текущего (Swing) и предыдущего (Swing_n) свингов. В этом случае необходимо зафиксировать новую точку С, в то время как предыдущая точка С станет точкой В, предыдущая точка В - точкой А, а предыдущая точка А будет исключена из анализа. Текущие значения точек хранятся в переменных SWА, SWВ и SWС. Значение новой точки С определяется как последний минимум (переменная BL), если формируется восходящий свинг, и как последний максимум (переменная BH), если формируется нисходящий свинг.
Функцией вложенного блока 1.3 является обновление значений переменных BL и BH во время формирования свинга. Вместе с ними происходит обновление значений переменных LasLow и LastHigh. В блоке 1.4 производится расчет значений целей Target1, Target2 и Target3.
С окончанием блока 1 проверяется "свежесть" точки С. Для этого значение переменной LastSwing, в которой хранится время открытия бара, соответствующего точке С, сравнивается со значением переменной LastSignal, в которой хранится время последней обработанной точки С. Если значения не совпадают, то можно говорить о формировании новой точки С. При совпадении значений функция GetSignal прерывается, что оставляет нулевое значение переменной Signal.
Блоки 2 и 3 алгоритмически идентичны. Если значение точки В больше значения точки С, то свинг ВС - нисходящий, что означает ожидание восходящего свинга, т.е. предполагается рост цены. В этом случае подается сигнал к совершению покупки с расчетом значений Stop и Start. Если значение точки В меньше значения точки С, то свинг ВС - восходящий, что означает ожидание падения цены; сигнал к продаже.
Для подготовки большинства значений переменных, использующихся в теле функции GetSignal, используется функция SwingCalculation. Обращение к ней производится в двух случаях: старт эксперта и долгое отсутствие связи с сервером брокера. В результате такого подхода скорость выполнения программы значительно увеличивается, т.к. функция SwingCalculation занимает значительное процессорное время. Особенно это заметно при тестировании эксперта в тестере стратегий.
Код SwingCalculation в рамках статьи рассматриваться не будет, т.к. практически полностью повторяет код первого блока GetSignal. Отличие заключается в начальной инициализации переменных и использовании цикла, проходящего barn баров для расчета как можно большего числа экстремумов.
Рассмотрим, как производится управление дочерними функциями в теле главной функции start:
//+-------------------------------------------------------------------------------------+
//| Функция start эксперта |
//+-------------------------------------------------------------------------------------+
int start()
{
// - 1 - ========================== Можно ли работать эксперту? =========================
if (!Activate || FatalError) return(0);
// - 1 - ============================= Окончание блока ==================================
// - 2 - =============== Получение рыночных условий и слежение за связью ================
if (!IsTesting()) // Если производится тестирование, то слежение не требуется
{
Tick = MarketInfo(Symbol(), MODE_TICKSIZE); // минимальный тик
Spread = ND(MarketInfo(Symbol(), MODE_SPREAD)*Point); // текущий спрэд
StopLevel = ND(MarketInfo(Symbol(), MODE_STOPLEVEL)*Point);//текущий уровень стопов
FreezeLevel = ND(MarketInfo(Symbol(), MODE_FREEZELEVEL)*Point);// уровень заморозки
if (Time[0] - LastConnect > Period()*60 || LastBars < Bars-1) // Последний
SwingsCalculation();// посчитанный бар был более одной свечи назад или изменилось
// количество баров
LastConnect = Time[0]; // Обновляем время последнего сбора информации
}
// - 2 - ============================= Окончание блока ==================================
// - 3 - ========================== Расчет сигналов открытия и закрытия =================
if (High[0] >= High[1] || Low[0] <= Low[1] ||// Произошло пробитие последней свечи или
CalcSignal != LastSignal) // по последнему сигналу закончены не все операции
{
GetSignal(); // Расчет текущего свинга
if (CalcSignal != LastSignal) // Если найден новый свинг
if (!Trade()) // Установка ордеров, открытие и закрытие сделок
return(0);
CalcSignal = LastSignal; // Все операции завершены успешно
}
// - 3 - ============================= Окончание блока ==================================
return(0);
}
Второй блок функции start выполняется только при работе советника онлайн. Кроме обновления информации о рыночном окружении (тик, спрэд, своп, уровень заморозки) здесь отслеживается такая нештатная ситуация, как обрыв связи. Обрыв связи определяется по двум признакам: отсутствие прихода котировок в течение времени больше, чем один периода графика и изменение количества баров больше, чем на один. В обоих случаях происходит вызов функции SwingCalculation, производящей пересчет экстремумов зигзага. Очевидно, что к первому признаку отсутствия связи будет относиться каждый понедельник, т.к. котировки не поступают в течение субботы и воскресенья, а это больше, чем один период графика вплоть до дневного. Второй признак отсутствия связи необходим в случае возобновления связи. Если внимательно следить за процессом возобновления связи после долгого ее обрыва, то можно заметить, что сначала поступает самая свежая котировка без обновления пропущенного периода истории. Это может привести к получению неправильных данных. Поэтому в момент окончания подкачки всей пропущенной истории (изменится количество баров) будет произведен очередной пересчет значений индикатора Dinapoli_Targets.
Третий блок реализует еще один аспект увеличения быстродействия советника - ограничивает количество вызовов функции GetSignal. Этот подход основан на том утверждении, что для появления нового экстремума зигзага требуется выход цены за пределы предыдущей свечи, т.е. чтобы текущая цена была ниже минимума или выше максимума свечи 1. В этом случае также учитывается, что после предыдущего расчета сигнала были выполнены не все торговые операции. Поэтому к критериям выхода цены за границы предыдущей свечи добавляется проверка окончания всех торговых операций путем сравнения времени последней сформированной точки С (LastSignal) со значением переменной CalcSignal. В CalcSignal сохраняется время последней обработанной точки С.
Выполнение торговых операций возложено на функцию Trade, которая, по традиции, делится на два алгоритмически подобных блока:
//+-------------------------------------------------------------------------------------+
//| Открытие позиций |
//+-------------------------------------------------------------------------------------+
bool Trade()
{
// - 1 - ==================== Открытие длинных позиций ==================================
if (Signal > 0)
{
// - 1.1 - ================== Поиск ордеров и проверка ордеров Sell =================
FindOrders(); // Сбор информации по своим ордерам
if (!CheckSells()) // Удаление отложенных Sell-ордеров, модификация уровня профита
return(False); // и закрытие сделок Sell. При неудаче ждем следующего тика
// - 1.1 - ============================ Окончание блока =============================
// Наибольшее значение, достигнутое ценой после точки С
double HighValue = High[iHighest(NULL, 0, MODE_HIGH, iBarShift(NULL, 0, LastSwing))];
for (int i = 0; i < 3; i++) // Проверка трех Buy-ордеров
// - 1.2 - ===================== Buy-ордер номер i существует =======================
if (BuyTicket[i] > 0) // Если ордер заданного номера существует
{
RefreshRates();
if (OrderSelect(BuyTicket[i], SELECT_BY_TICKET) && OrderCloseTime() == 0)
if (OrderType() > 0) // По условиям отложенный ордер от предыдущего сигнала
{ // существовать не может. Поэтому удаляем его при
if (!DeleteOrder()) // нахождении.
return(False);
}
else // Если рыночный ордер, то проверить правильность
{ // стопа и профита.
if (Target[i] - Bid < StopLevel || // Если выше профита или близко к нему,
Bid - Stop < StopLevel) // то закрыть. Если ниже стопа или
{ // близко к нему, то закрыть.
if (!CloseOrder())
return(False);
}
else// Если стоп и профит от цены далеко, то перемещение на новые значения
if (!CheckSLTP(Target[i]))
return(False);
}
}
// - 1.2 - ============================ Окончание блока =============================
// - 1.3 - ===================== Buy-ордер номер i не существует ====================
else // Ордер заданного номера не существует - открыть:
{
if (HighValue >= Target[i]) continue; // Если цена уже достигла цели, то ничего
RefreshRates(); // не делаем
if (Start - Ask > StopLevel) // 1) Если Ask ниже цены Start - Buy Stop
{
if (OpenOrderCorrect(OP_BUYSTOP, Lots[i], NP(Start+Spread), NP(Stop),
NP(Target[i]), i) != 0)
return(False);
}
else
if (Ask - Start > StopLevel) // 2) Если Ask выше цены Start - BuyLimit
{
if (OpenOrderCorrect(OP_BUYLIMIT, Lots[i], NP(Start+Spread), NP(Stop),
NP(Target[i]), i) != 0)
return(False);
}
else // 3) Если Ask близко к цене Start - Buy
if (OpenOrderCorrect(OP_BUY, Lots[i], NP(Ask), NP(Stop),
NP(Target[i]), i) != 0)
return(False);
}
// - 1.3 - ============================ Окончание блока =============================
}
// - 1 - ================================ Окончание блока ===============================
// - 2 - ==================== Открытие короткой позиции =================================
if (Signal < 0)
{
// - 2.1 - ================== Поиск ордеров и проверка ордеров Buy ==================
FindOrders(); // Сбор информации по своим ордерам
if (!CheckBuys()) // Удаление отложенных Buy-ордеров, модификация уровней стопа,
return(False); // профита и закрытие сделок Buy. При неудаче ждем следующего тика
// - 2.1 - ============================ Окончание блока =============================
// Наименьшее значение, достигнутое ценой после точки С
double LowValue = Low[iLowest(NULL, 0, MODE_LOW, iBarShift(NULL, 0, LastSwing))];
for (i = 0; i < 3; i++) // Проверка трех Sell-ордеров
// - 2.2 - ===================== Sell-ордер номер i существует ======================
if (SellTicket[i] > 0) // Если ордер заданного номера существует
{
RefreshRates();
if (OrderSelect(SellTicket[i], SELECT_BY_TICKET) && OrderCloseTime() == 0)
if (OrderType() > 1) // По условиям отложенный ордер от предыдущего
{ // сигнала существовать не может. Поэтому удаляем
if (!DeleteOrder()) // его при нахождении.
return(False);
}
else // Если рыночный ордер, то проверить правильность
{ // стопа и профита.
if (Ask - Target[i] < StopLevel || // Если ниже профита или близко к нему,
Stop - Ask < StopLevel) // то закрыть. Если выше стопа или
{ // близко к нему, то закрыть.
if (!CloseOrder())
return(False);
}
else// Если стоп и профит от цены далеко, то перемещение на новые значения
if (!CheckSLTP(Target[i]))
return(False);
}
}
// - 2.2 - ============================ Окончание блока =============================
// - 2.3 - ===================== Sell-ордер номер i не существует ===================
else // Ордер заданного номера не существует - открыть:
{
if (LowValue <= Target[i]) continue; // Если цена уже достигла цели, то ничего
RefreshRates(); // не делаем
if (Bid - Start > StopLevel) // 1) Bid выше цены Start - Sell Stop
{
if (OpenOrderCorrect(OP_SELLSTOP, Lots[i], NP(Start), NP(Stop + Spread),
NP(Target[i] + Spread), i) != 0)
return(False);
}
else
if (Start - Bid > StopLevel) // 2) Bid ниже цены Start - Sell Limit
{
if (OpenOrderCorrect(OP_SELLLIMIT, Lots[i], NP(Start), NP(Stop + Spread),
NP(Target[i] + Spread),i) != 0)
return(False);
}
else // 3) Bid близко к цене Start - Sell
if (OpenOrderCorrect(OP_SELL, Lots[i], NP(Bid), NP(Stop + Spread),
NP(Target[i] + Spread), i) != 0)
return(False);
}
// - 2.3 - ============================ Окончание блока =============================
}
// - 2 - ==================== Окончание блока ===========================================
return(True);
}
Логика советника в отношении уровней Dinapoli должна позволять обрабатывать все проблемные ситуации: нахождение текущей цены дальше точки входа (уровень Start) или дальше каждой из целей (Target1, Target2 и Target3). Так, если уровень входа находится не в непосредственной близости от текущей цены, то советник должен установить отложенный ордер по цене Start (лимитный или стоповый, в зависимости от ситуации). При этом противоположные ордера также должны быть модифицированы в соответствии с новыми условиями. Все это учитывается в логике функции Trade и ее дочерних функций.
При получении сигнала к покупке (Signal > 0, блок 1) действия эксперта должны быть следующими:
1) Проверка существования ордеров Sell-типа, к которым относятся Sell, Sell Stop и Sell Limit. Если обнаружены отложенные ордера, то их следует удалить. Если же обнаружены открытые сделки, то уровень их закрытия необходимо переместить на уровень открытия сделок Buy. При нахождении уровня Start в непосредственной близости от цены (что исключает модификацию уровня стопа или профита), сделка закрывается. Если Start находится выше текущей цены, то на ее значение перемещается уровень стопа, а если Start находится ниже текущей цены, то на ее значение перемещается уровень профита. Эти действия выполняются при помощи вызова функции CheckSells, которая будет возвращать значение False до тех пор, пока не выполнит все возложенные на нее обязанности.
2) Случай существования ордеров Buy-типа, к которым относятся Buy, Buy Stop и Buy Limit. Так как количество направленных в одну сторону ордеров советника достигает числа 3, то проверка проводится при помощи цикла из трех итераций. Если обнаружен отложенный ордер, то он должен быть удален, т.к. существование отложенных ордеров от предыдущего сигнала к покупкам исключено. Это означает лишь наличие какой-либо ошибки технического плана. Если обнаружена сделка Buy, то изменяются уровни ее стопа и профита (вызов функции CheckSLTP), т.к. сделка не успела закрыться за время существования сигнала к продажам. В случае невозможности изменения уровней стопа и профита на новые значения из-за их близости или прохождения ценой соответствующего значения, сделка принудительно закрывается (вызов функции CloseOrder). Все эти действия выполняются во вложенном блоке 1.2.
3) Случай отсутствия ордера Buy-типа. Производится открытие ордера, при условии, что за время, прошедшее после формирования точки С, не была достигнута соответствующая цель (1, 2 или 3). Если цена Start намного ниже текущей цены, то устанавливается ордер BuyLimit, если намного выше - BuyStop. При нахождении текущей цены в непосредственной близости от уровня Start происходит открытие сделки по рыночной цене. Такие действия выполняет блок 1.3.
Подобный алгоритм с учетом специфики коротких позиций заложен в блоке 2. Перейдем к рассмотрению дочерних функций функции Trade: CheckBuys (аналог - CheckSell) и CheckSLTP.
//+-------------------------------------------------------------------------------------+
//| Поиск своих ордеров Buy-типа (BuyStop, BuyLimit, Buy). Если найден отложенный |
//| ордер, то он удаляется. Если обнаружена сделка, то ее профит или стоп, в зависимости|
//| от положения уровня Start, перемещается на уровень Start. |
//+-------------------------------------------------------------------------------------+
bool CheckBuys()
{
// - 1 - ============================ Выбор ордера ======================================
for (int i = 0; i < 3; i++) // Используется весь список ордеров Buy
if (BuyTicket[i] > 0) // Ордер найден
if (OrderSelect(BuyTicket[i], SELECT_BY_TICKET) && // Убедимся, что ордер выбран и
OrderCloseTime() == 0) // находится в списке открытых
// - 1 - =========================== Окончание блока ====================================
// - 2 - ====================== Операции с рыночным ордером =============================
if (OrderType() == OP_BUY) // Текущая сделка - длинная
{
RefreshRates(); // Обновление значений Bid и Ask
if (Start <= Bid) // Если Start ниже Bid, то будет изменен стоп позиции
{
if (Bid - Start <= StopLevel) // Если цена слищком близко к Start, то
{
if (!CloseOrder()) // закрываем сделку
return(False);
}
else
if (!ModifySL(NP(Start))) // Если не удалось изменить уровень
return(False); // стопа, то вернем ошибку
}
else
if (Start > Bid) // Если Start выше Bid, то будет изменен профит позиции
{
if (Start - Bid <= StopLevel) // Если цена слищком близко к Start, то
{
if (!CloseOrder()) // закрываем сделку
return(False);
}
else
if (!ModifyTP(NP(Start))) // Если не удалось изменить уровень
return(False); // профита, то вернем ошибку
}
}
// - 2 - =========================== Окончание блока ====================================
// - 3 - ====================== Операции с отложенным ордером ===========================
else
if (!DeleteOrder()) // Ордер должен быть удален
return(False);
// - 3 - =========================== Окончание блока ====================================
return(True); // Все действия завершены успешно
}
Вспомним, что функция CheckBuys вызывается только при получении сигнала к продаже. Поэтому ее предназначением является ликвидация ордеров Buy-типа.
В первом блоке происходит выбор нужного ордера Buy-типа. Если ордер успешно выбран, то выполняется второй или третий блок, в зависимости от того, рыночный это ордер или отложенный.
Если ордер рыночный, то выполняется блок 2. В нем происходит оценка удаленности уровня Start от текущей цены. Если Start выше уровня Bid, то производится изменение уровня профита, который будет указывать на уровень Start. Если модификация профита невозможна по причине близости нового уровня, то рыночный ордер закрывается. Если Start ниже Bid, то производится изменение уровня стоп-приказа. Если изменение стоп-приказа невозможно, то рыночный ордер закрывается.
В случае выбора отложенного ордера исполняется блок 3, где при помощи вызова функции DeleteOrder происходит его удаление.
Алгоритмически подобна данной функции функция CheckSells. Поэтому следующей рассмотрим функцию CheckSLTP:
//+-------------------------------------------------------------------------------------+
//| Проверка правильности параметров рыночного ордера |
//+-------------------------------------------------------------------------------------+
bool CheckSLTP(double Target)
{
double SL = NP(OrderStopLoss()); // Текущие значения стопа и профита
double TP = NP(OrderTakeProfit());
double ComSL = Stop; // Требуемые значения стопа и профита
double ComTP = Target;
if (OrderType() == OP_SELL) // если сделка короткая, то к требуемым значениям нужно
{
ComSL += Spread; // добавить спрэд
ComTP += Spread;
}
if (MathAbs(ComSL - SL) >= Tick) // Если старый стоп не равен новому, то изменяем стоп
SL = NP(ComSL);
if (MathAbs(TP - ComTP) >= Tick) // если старый профит не равен новому, то меняем профит
TP = NP(ComTP);
if (MathAbs(SL - OrderStopLoss()) >= Tick || // Если нужно изменить стоп или
MathAbs(TP - OrderTakeProfit()) >= Tick) // профит
if (WaitForTradeContext()) // Свободен ли торговый поток?
{
if (!OrderModify(OrderTicket(), 0, SL, TP, 0)) // модификация стоп или профита, или
return(False); // обоих вместе
}
else
return(False);
return(True); // Модификация успешно завершена
}
Функция едина для ордеров Buy и Sell. Поэтому сравнение производится в различными значениями - для Sell уровень стоп-приказа и профита отличается от аналогичных уровней Buy на один спрэд. Если сравниваемое значение (ComSL или ComTP) не равно текущему значению (SL или TP), то текущее значение параметра изменяется на новое. Логика функции построена таким образом, что за одно обращение к серверу функции OrderModify производится изменение как одного, так и двух параметров сразу. Если на одном из шагов была получена ошибка, то функция вернет значение False. При успешном выполнении всех операций будет возвращено значение True.
Тестирование советника
Напоминаю, что методика тестирования экспертов была немного изменена. Об этом можно прочесть в статье Объем сделки и вероятность ее успешности (http://fxtrade.ru/mqlabs/16.09.2010-mqlabs-obem-sdelki-i-veroyatnost-ee-uspeshnosti), раздел "Тестирование советника". Там же можно взять файлы истории котировок за 2009 и 2010 годы. К ним были выпущены корректирующие файлы по EURUSD и USDCHF, которые можно взять по следующим ссылкам: EURUSD (http://www.forextrade.ru/media/Image/MQLabs/82_ag/EURUSD1.zip) и USDCHF (http://www.forextrade.ru/media/Image/MQLabs/82_ag/USDCHF1.zip).
Так как советник оперирует тремя сделками одного типа одновременно, то для каждого номера (1, 2 или 3), которые различаются по размеру профита, объем устанавливается индивидуально. По умолчанию сделка с наименьшим профитом (цель 1) обладает наибольшим объемом (параметр LotsSmall), со средним профитом (цель 2) - средним объемом (параметр LotsMedium), с наибольшим профитом (цель 3) - наименьшим объемом (параметр LotsLarge). Все эти данные могут быть изменены по усмотрению пользователя.
Тестирование советника Dinapoli_Expert производилось на участке с 01.01.2009 до 25.09.2010. Период графика - Н1. Из входных параметров эксперта только один был изменяемым. Это параметр Length. Все остальные параметры имели значения по умолчанию. Результаты тестирования приведены на рис. 2 - 5.
http://www.forextrade.ru/media/Image/MQLabs/83_ag/EURUSD.gif
Рис. 2. Результаты тестирования эксперта Dinapoli_Expert на валютной паре EURUSD.
EURUSD. Значение параметра Length равно 50. Кривая баланса ясно дает понять, что советник неустойчив на данной валютной паре. Не менее наглядно это выражается в цифрах: чистая прибыль 5649 долларов против максимальной просадки 7112 долларов, что приводит к фактору восстановления менее единицы.
http://www.forextrade.ru/media/Image/MQLabs/83_ag/USDCHF.gif
Рис. 3. Результаты тестирования эксперта Dinapoli_Expert на валютной паре USDCHF.
USDCHF. Значение параметра Length равно 31. Стабильности в кривой баланса не ощущается, что к доверию не располагает. Позитивом является лишь рост кривой ближе к концу тестирования. Чистая прибыль 4590 долларов против максимальной просадки 1908 долларов. Фактор восстановления 2.4.
http://www.forextrade.ru/media/Image/MQLabs/83_ag/GBPUSD.gif
Рис. 4. Результаты тестирования эксперта Dinapoli_Expert на валютной паре GBPUSD.
GBPUSD. Значение параметра Length равно 47. Нисходящая форма кривой наиболее полно характеризует применение уровней ДиНаполи совместно с фунтом. Стоит заметить, что это один из лучших результатов, показанных советником Dinapoli_Expert на валютной паре GBPUSD.
http://www.forextrade.ru/media/Image/MQLabs/83_ag/USDJPY.gif
Рис. 5. Результаты тестирования эксперта Dinapoli_Expert на валютной паре USDJPY.
USDJPY. Значение параметра Length равно 8. Наиболее приемлемый из сегодняшних результатов. В пользу доверия к результатам говорит достаточно большое количество совершенных сделок (более 1000) и стабильный рост кривой баланса, слегка подпорченный в начале тестирования. В цифрах успешность стратегии применительно к йене не так очевидна: чистая прибыль 5294 доллара против максимальной просадки 2494 доллара. Фактор восстановления 2.12.
Все результаты были представлены с учетом реального применения стратегии. То есть во всех случаях минимальный рабочий депозит для текущих настроек эксперта предполагается 10 000 долларов. При работе с другим размером депозита объемы позиций должны быть соответствующим образом изменены.
Доработка стратегии для использования в AutoGraf 4.0
В настроечные параметры AutoGraf из разработанного эксперта перейдет четыре входных параметра: LotsSmall - AT_1, LotsMedium - AT_2, LotsLarge - AT_3 и Length - AT_4. Настройка объема позиции при помощи панели настроек AutoGraf (параметр Lot) для данной стратегии невозможна.
Запуск стратегии Dinapoli_Expert в среде AutoGraf 4.0 состоит из следующих шагов:
Получить файл по ссылке Файлы стратегий для AutoGraf 4.0 (http://www.forextrade.ru/media/Image/MQLabs/83_ag/AG_Dinapoli.zip) и распаковать полученный архив в папку MT4\experts\libraries (с перезаписью файлов AG_AT.ex4 и AG_AT.mq4). Запустить AutoGraf. Для работы стратегии в ключе приведенных результатов в окне настроек AutoGraf (закладка "Входные параметры") установить нужные значения параметров AT_1 - AT_4. Полное повторение результатов при этом не гарантируется. Выбрать стратегию №5. Для этого необходимо передвинуть вверх значок So и среди названий стратегий найти значок S5, который также потянуть вверх. Запустить функцию автоматической торговли, передвинув значок AT в верхнее положение.
Советник Dinapoli (http://www.forextrade.ru/media/Image/MQLabs/83_ag/Dinapoli_Expert.mq4)
Индикатор Dinapoli Targets (http://www.forextrade.ru/media/Image/MQLabs/83_ag/DinapoliTargets.mq4)
Файлы стратегий для AutoGraf 4.0 (http://www.forextrade.ru/media/Image/MQLabs/83_ag/AG_Dinapoli.zip)
Развернутые результаты тестирования эксперта (http://www.forextrade.ru/media/Image/MQLabs/83_ag/Test.zip)
Использование полученного советника рекомендуется только в полуавтоматическом режиме под присмотром трейдера и после всестороннего изучения слабых и сильных сторон стратегии.