Scriptong
06-02-2010, 11:56
Стратегия, которую сегодня рассмотрим, в плане управления рисками очень похожа на стратегию "ЗигЗаг и статистика" (http://fxtrade.ru/mqlabs/12.11.2009-zigzag-i-statistika). Но это единственное сходство, техника открытия и закрытия позиций совсем другая, да и теоретическое обоснование стратегии тоже другое.
Название торговой системы довольно поэтическое - "Летучая мышь". Это одна из распространенных тактик, но, как оказалось, на ее основе советники еще не создавались. Соответственно, результатов тестирования за сколько-нибудь серьезный исторический промежуток найти тоже не удалось. Именно этот пробел в истории торговых систем и будет сегодня заполнен.
Основным поставщиком сигналов системы выступает индикатор ATR_The_bat. Правда, есть у этого индикатора и другие названия, например, ATR_Stops. Как следует из названия, в основе индикатора лежит стандартный технический индикатор MT4 - Average True Range (ATR). От каждой свечи вверх и вниз откладывается расстояние, соответствующее текущему значению ATR, умноженное на множитель Factor. Чаще всего применяют четырех- или пятикратное значение ATR. В результате выходит что-то наподобие канала, с тем условием, что его стенки могут лишь сходиться, а не расходиться. В полученном канале цена движется до тех пор, пока не достигнет одной из стенок. Если зафиксировано пробитие границы канала, то противоположная граница принимает новое расчетное значение без оглядки на предыдущие.
Именно пробитие одной из границ канала является сигналом для совершения сделки (см. рис. 1)
http://www.forextrade.ru/media/Image/MQLabs/52_ag/figure_1.gif
Рис. 1 - Пробитие границы канала и открытие сделок.
Чтобы не упустить момент входа, на видимой границе канала (индикатор ATR_The_bat все время отображает только одну границу) будем устанавливать отложенный ордер. Когда он сработает, появится противоположная граница канала, которая будет являться уровнем стоп-приказа для текущей позиции.
Между двумя полученными уровнями располагаем объект "линии Фибоначчи" и определяем два дополнительных уровня - 61.8% и 38.2%. Это уровни для установки дополнительных лимитных ордеров, которые в случае благоприятного развития событий значительно увеличат полученную прибыль, а в неблагоприятном случае увеличат убыток, но далеко не на сумму потенциальной прибыли. Как вы, наверное, догадались, уровень стоп-приказов у всех ордеров ставится на один и тот же уровень.
С движением цены в сторону прибыли первичной сделки уровень поддержки, отображаемый индикатором ATR_The_bat, повышается, и в какой-то момент может стать выше цены открытия одного из лимитных ордеров. В этом случае лимитный ордер следует удалить, так как невозможно установить стоп-приказ ордера Buy Limit выше уровня его открытия.
Цель каждого из ордеров можно настраивать отдельно, но наиболее выгодным является закрытие всех позиций на одном уровне. В системе рекомендуется значение 161.8 от ширины канала. В этом случае выходит, что у самого нижнего ордера Buy Limit соотношение прибыль/риск очень высокое. Поэтому рекомендуется увеличивать объем этого ордера в два раза, по сравнению с предыдущим ордером.
Точно такие же условия открытия сделок применяются в случае пробития нижней границы канала, когда первичная позиция короткая, а вспомогательные отложенные ордера имеют тип Sell Limit.
Как видно, система не очень сложная, хотя именно вручную исполнять ее затруднительно из-за необходимости все время находится недалеко от компьютера. Здесь как раз и проявится "усидчивость" автоматических торговых систем, которым неведома усталость. И пусть простота системы очевидна, запрограммировать ее не так уж и легко.
Сложности возникают не в генерации сигналов, а именно в части управления ордерами. Поэтому сигнальную часть сложной не назовешь:
//+-------------------------------------------------------------------------------------+
//| Расчет уровней для каждого направления торговли |
//+-------------------------------------------------------------------------------------+
void GetSignal()
{
// - 1 - ============================= Инициализация переменных =========================
BuyLevel = 0; SellLevel = 0;
// - 1 - ================================== Окончание блока =============================
// - 2 - =========== Поиск последнего пробитого уровня ATR ==============================
int i = iBarShift(Symbol(), 0, LastCountTime);// Начинаем с последнего посчитанного бара
while (i > 0)
{
double ApplyPrice = GetPrice(i);
double ATRF = iATR(Symbol(), 0, ATRPeriod, i)*Factor;
if (Dir == 0) // если направление не определено (первый подход к GetSignal), то
{
DnLevel = ApplyPrice - ATRF; // рассчитываем оба уровня - верхний и нижний
UpLevel = ApplyPrice + ATRF;
}
if (Dir >= 0) // При наличии восходящего движения следим за пробитием поддержки
if (Low <= DnLevel) // пробита ли поддержка?
{
Dir = -1; // если пробита, то меняем текущее направление движения на нисходящее
DnLevel = 0;
LastPierce = Time[i]; // запоминаем время пробития
}
else
DnLevel = MathMax(ApplyPrice - ATRF, DnLevel); // или высчитываем новый уровень
if (Dir <= 0) // При наличии нисходящего движения следим за пробитием сопротивления
if (High[i] >= UpLevel) // пробито ли сопротивление
{
Dir = 1; // если пробито, то меняем направление движения на восходящее
UpLevel = 1000;
LastPierce = Time[i]; // запоминаем время пробития
i++;
}
else
UpLevel = MathMin(ApplyPrice + ATRF, UpLevel); // или рассчитываем новый уровень
i--;
}
// - 2 - ================================== Окончание блока =============================
LastCountTime = Time[i]; // Последний посчитанный бар запоминаем
// - 3 - ================= Отображение верхнего уровня ATR выше цены ====================
if (Dir < 0) // текущее направление нисходящее
{
ShowLevel(UpLevel, Red); // отображаем уровень выше цены
BuyLevel = UpLevel; // это и будет уровнем открытия Buy Stop
}
// - 3 - ================================== Окончание блока =============================
// - 4 - ================== Отображение нижнего уровня ATR ==============================
if (Dir > 0) // текущее направление восходящее
{
ShowLevel(DnLevel, Blue); // отображаем уровень ниже цены
SellLevel = DnLevel; // это и будет уровнем открытия Sell Stop
}
// - 4 - ================================== Окончание блока =============================
}
Как давно уже повелось, уровни открытия сделок задают переменные BuyLevel и SellLevel, которые инициализируются в первом блоке.
Второй блок в точности повторяет алгоритм индикатора ATR_The_bat, избавляя от необходимости расчета значений индикатора через функцию iCustom. Такой подход позволяет немного ускорить работу советника. Главным в работе второго блока является определение текущего направления движения цен, что находит отражение в значении флага Dir. Его значение сохраняется от одного бара к другому. Поэтому, в совокупности с переменной LastCountBar, сохраняющей время открытия последнего посчитанного бара, они помогают осуществить еще одну часть оптимизации расчетов - не пересчитывать несколько сот или даже тысяч баров заново при каждом вызове функции GetSignal. Полный пересчет производится лишь после инициализации советника.
Третий и четвертый блоки, по сути, одинаковы. Разница лишь в сигналах открытия сделок и цвете линии сопротивления/поддержки.
Функция start, благодаря переносу всех основных действий в другие функции, никаких затруднений не вызывает:
//+-------------------------------------------------------------------------------------+
//| Функция START эксперта |
//+-------------------------------------------------------------------------------------+
int start()
{
// - 1 - == Разрешено ли советнику работать? ===========================================
if (!Activate || FatalError) // Отключается работа советника, если функция
return(0); // init завершилась с ошибкой или имела место фатальная ошибка
// - 1 - == Окончание блока ============================================================
// - 2 - == Сбор информации об условиях торговли ========================================
Spread = ND(MarketInfo(Symbol(), MODE_SPREAD)*Point); // текущий спрэд
StopLevel = ND(MarketInfo(Symbol(), MODE_STOPLEVEL)*Point); // текущий уровень стопов
FreezeLevel = ND(MarketInfo(Symbol(), MODE_FREEZELEVEL)*Point); // уровень заморозки
// - 2 - === Окончание блока ============================================================
// - 3 - ================ Расчет уровней открытия длинных и коротких позиций ============
if (LastBar == Time[0])
return(0);
if (LastSignal != Time[0])
{
GetSignal();
LastSignal = Time[0];
}
// - 3 - ================================ Окончание блока ==============================
// - 4 - =========== Изменение стопов позиций и цены открытия отложенных ордеров ========
FindOrders(); // Нахождение всех ордеров и позиций, сортировка по массивам тикетов
if (!CheckOrders())
return(0);
// - 4 - ========================== Окончание блока =====================================
LastBar = Time[0];
return(0);
}
В третьем блоке один раз за бар вызывается расчет уровней поддержки и сопротивления, а также производится слежение за тем, чтобы советник в целом не "перерабатывал".
В четвертом блоке расположены вызовы двух главных функций советника - FindOrders (поиск и подсчет ордеров советника) и CheckOrders (абсолютно все операции по изменению уровней стопов, цен открытия отложенных ордеров, удалению и установке ордеров).
Начнем с функции попроще - FindOrders:
//+-------------------------------------------------------------------------------------+
//| Нахождение всех своих ордеров и позиций. |
//+-------------------------------------------------------------------------------------+
void FindOrders()
{
ArrayInitialize(BuyTickets, -1); // "обнуление" тикетов
ArrayInitialize(SellTickets, -1); // "обнуление" тикетов
Buys = 0; Sells = 0; // Счетчики кол-ва своих ордеров и позиций
FirstBuyLevel = 0; FirstSellLevel = 0; // Уровни пробития
// - 1 - ============================ Выбираем свои ордера и позиции ====================
for (int i = 0; i < OrdersTotal(); i++)
if (OrderSelect(i, SELECT_BY_POS))
if (OrderSymbol() == Symbol() && MathFloor(OrderMagicNumber()/100) == MagicNumber)
// - 1 - ================================ Окончание блока ===============================
{
// - 2 - =============== Сортировка ордеров и позиций по номерам тикетов ================
int ID = MathMod(OrderMagicNumber(), 100);
if (MathMod(OrderType(), 2) == 0) // Найден BUY-ордер
{
BuyTickets[ID] = OrderTicket();
BuyTime[ID] = MathMax(BuyTime[ID], OrderOpenTime());
Buys++;
if (ID == 0 && OrderType() == OP_BUY)
FirstBuyLevel = OrderOpenPrice() - Spread;
}
else // найден Sell-ордер
{
SellTickets[ID] = OrderTicket();
SellTime[ID] = MathMax(SellTime[ID], OrderOpenTime());
Sells++;
if (ID == 0 && OrderType() == OP_SELL)
FirstSellLevel = OrderOpenPrice();
}
// - 2 - ================================ Окончание блока ===============================
}
}
Так как советник оперирует несколькими одновременно открытыми позициями, то ему необходимо различать свои же ордера между собой. Для этого каждому из трех возможных ордеров присваивается порядковый номер - 0, 1 или 2. Он прибавляется к младшему разряду поля MagicNumber, в то время как в старшие разряды (значение выше 100) записывается идентификатор советника, задаваемый в одноименном входном параметре MagicNumber. Таким образом, значение параметра MagicNumber умножается на 100, а затем уже к нему добавляется порядковый номер ордера. Чтобы впоследствии определить идентификатор советника, нужно значение поля MagicNumber ордера разделить на 100 без остатка и сравнить с идентификатором. Соответственно, для определения порядкового номера ордера нужно взять остаток от деления значения поля MagicNumber на 100.
Таким образом, функция FindOrders сортирует ордера по массивам BuyTickets и SellTickets, сохраняя тикеты ордеров под порядковыми номерами. Точно также сортируются времена открытия последних ордеров каждого типа в массивы SellTime и BuyTime.
Переменные FirstBuyLevel и FirstSellLevel содержат значение уровней открытия первичной длинной и короткой сделок соответственно. Они будут использованы для расчета уровней Фибоначчи.
В виду объемности функции CheckOrders, рассмотрим ее, разбив на два блока - работа в восходящем и работа в нисходящем движении:
// - 1 - ==================== Активен сигнал открытия длинных позиций ===================
if (SellLevel > 0)
{
// - 1.1 - ============== Проверка наличия основной позиции и ее стопа ==============
if (BuyTickets[0] > 0 && OrderSelect(BuyTickets[0], SELECT_BY_TICKET) &&
OrderCloseTime() == 0)
{
if (OrderType() > 1) // Если это до сих пор ордер, то удаляем его
{
OrderDelete(OrderTicket());
return(false); // Не все операции закончены
}
if (MathAbs(OrderStopLoss()-SellLevel+Tick) >= Tick) // Стоп нужно изменить
{
if (Bid - SellLevel+Tick > StopLevel)
if (WaitForTradeContext())
OrderModify(OrderTicket(), 0, NP(SellLevel-Tick), OrderTakeProfit(), 0);
return(false); // Не все операции закончены
}
if (OrderTakeProfit() == 0) // Если профит еще не установлен, то установим
{
double TP = (FirstBuyLevel - SellLevel)*(FiboTarget1/100.0) + SellLevel;
if (TP - Bid <= StopLevel)
TP = Bid + StopLevel + Tick;
if (WaitForTradeContext())
OrderModify(OrderTicket(), 0, OrderStopLoss(), NP(TP), 0);
return(False);
}
}
else
if (BuyTime[0]<LastPierce &&// Последняя первичная позиция была открыта до пробития
TimeCurrent() - LastPierce < 2*60*Period()) // и еще не поздно открыть позицию
{
OpenOrderCorrect(OP_BUY, Lots1, NP(Ask), NP(SellLevel-Tick), 0,
100*MagicNumber); // Открываем BUY
return(false); // Не все операции закончены
}
// - 1.1 - ============================= Окончание блока ============================
// - 1.2 - ============ Проверка наличия второго ордера/позиции и ее стопа ==========
if (!CheckAddingBuy(1, Fibo2, FiboTarget2, Lots2))
return(False);
// - 1.2 - ============================= Окончание блока ============================
// - 1.3 - ============ Проверка наличия третьего ордера/позиции и ее стопа =========
if (!CheckAddingBuy(2, Fibo3, FiboTarget3, Lots3))
return(False);
// - 1.3 - ============================= Окончание блока ============================
// - 1.4 - ===== Установка первичного ордера SellStop и проверка цены открытия ======
if (SellTickets[0] > 0 && OrderSelect(SellTickets[0], SELECT_BY_TICKET) &&
OrderCloseTime() == 0)
{
if (OrderType() > 1)
if (MathAbs(OrderOpenPrice()-SellLevel+Tick)>=Tick) //Цену открытия нужно менять
{
if (Bid - SellLevel + Tick > StopLevel)
if (WaitForTradeContext())
OrderModify(OrderTicket(), NP(SellLevel - Tick), 0, 0, 0);
return(false); // Не все операции закончены
}
}
else // Первичного SellStop нет - устанавливаем
{
OpenOrderCorrect(OP_SELLSTOP, Lots1, NP(SellLevel-Tick), 0, 0,
100*MagicNumber); // Открываем Sell Stop
return(false); // Не все операции закончены
}
// - 1.4 - ============================= Окончание блока ============================
// - 1.5 - ================== Удаление оставшихся ордеров Sell Limit ================
if (!DeleteOrder(SellTickets[1]) || !DeleteOrder(SellTickets[2]))
return(False);
// - 1.5 - ============================= Окончание блока ============================
}
// - 1 - ================================ Окончание блока ===============================
Для понимания всего этого огромного скопления команд нужно, прежде всего, понять, что должен делать эксперт в том или ином случае. Мы в данный момент рассматриваем всего два развития событий: есть сигнал нисходящего движения и есть сигнал восходящего движения. Первый блок описывает случай наличия восходящего движения, то есть, когда линия ATR_The_bat находится под ценой, и у нас есть лишь уровень входа для продаж (SellLevel > 0). Алгоритм действий выходит такой (номера пунктов соответствуют номерам вложенных блоков):
1.1. Первичная позиция BUY должна быть открыта. То есть отложенный ордер должен сработать. Если ордер так и не сработал (это свидетельствует о том, что был перерыв в работе советника), то нужно его удалить и открыть позицию с рынка. В блоке это делается так: удаляется ордер и происходит окончание работы советника на текущем тике. К следующему тику не будет ни позиции, ни ордера, что приведет к автоматическому открытию позиции с рынка (часть блока после else). Если же позиция благополучно открыта, то проверяется правильность уровня стоп-приказа (соответствие изменяющейся линии поддержки) и установка уровня профита. После установки профита его правильность в будущем не проверяется.
1.2. Сюда выполнение дойдет лишь после полной проверки первичной позиции. Проверяется, существует ли вторая позиция или ордер Buy Limit. Для этого используется функция CheckAddingBuy (будет рассмотрена ниже). Если все операции со второй позицией завершены, и нет надобности к ней возвращаться, то переходим к блоку 1.3.
1.3. Аналогично проверяется третья, последняя, позиция, которая по умолчанию оперирует двойным лотом. Если все завершилось успешно, то переходим к 1.4.
1.4. На уровень стопа длинных позиций необходимо установить отложенный ордер Sell Stop, который будет первичным при развороте движения цены. Если ордер не существует, то производится его установка. Уровни стоп-приказа и профита на данном этапе рассчитать невозможно. Поэтому они с нулями. Если же ордер уже существует, то проверяется равенство цены его открытия уровню стоп-приказа длинных позиций, чтобы закрытие длинных и открытие короткой произошло по одной цене. Когда и в этом блоке все завершилось успешно, переходим к 1.5.
1.5. Если к моменту открытия длинных позиций остались не сработавшие ордера SellLimit, то их необходимо удалить. Этим занимается функция DeleteOrder. Ее код тривиален и здесь разобран не будет.
По точно такому же алгоритму, только с заменой Buy на Sell и Sell на Buy происходит работа второго блока функции CheckOrders:
// - 2 - =================== Активен сигнал открытия коротких позиций ===================
if (BuyLevel > 0)
{
// - 2.1 - ============== Проверка наличия основной позиции и ее стопа ==============
if (SellTickets[0] > 0 && OrderSelect(SellTickets[0], SELECT_BY_TICKET) &&
OrderCloseTime() == 0)
{
if (OrderType() > 1) // Если это до сих пор ордер, то удаляем его
{
if (WaitForTradeContext())
OrderDelete(OrderTicket());
return(false); // Не все операции закончены
}
if (MathAbs(OrderStopLoss()-BuyLevel-Spread-Tick) >= Tick) // Стоп нужно изменить
{
if (BuyLevel - Bid > StopLevel)
if (WaitForTradeContext())
OrderModify(OrderTicket(), 0, NP(BuyLevel + Spread + Tick),
OrderTakeProfit(), 0);
return(false); // Не все операции закончены
}
if (OrderTakeProfit() == 0) // Если профит еще не установлен, то установим
{
TP = BuyLevel - (BuyLevel - FirstSellLevel)*(FiboTarget1/100.0);
if (Ask - TP <= StopLevel)
TP = Ask - StopLevel - Tick;
if (WaitForTradeContext())
OrderModify(OrderTicket(), 0, OrderStopLoss(), NP(TP), 0);
return(False);
}
}
else
if (SellTime[0]<LastPierce &&//Последняя первичная позиция была открыта до пробития
TimeCurrent() - LastPierce < 2*60*Period()) // и еще не поздно открыть позицию
{
OpenOrderCorrect(OP_SELL, Lots1, NP(Bid), NP(BuyLevel + Spread + Tick), 0,
100*MagicNumber); // Открываем SELL
return(false); // Не все операции закончены
}
// - 2.1 - ============================= Окончание блока ============================
// - 2.2 - ============ Проверка наличия второго ордера/позиции и ее стопа ==========
if (!CheckAddingSell(1, Fibo2, FiboTarget2, Lots2))
return(False);
// - 2.2 - ============================= Окончание блока ============================
// - 2.3 - ============ Проверка наличия третьего ордера/позиции и ее стопа =========
if (!CheckAddingSell(2, Fibo3, FiboTarget3, Lots3))
return(False);
// - 2.3 - ============================= Окончание блока ============================
// - 2.4 - ===== Установка первичного ордера BuyStop и проверка цены открытия =======
if (BuyTickets[0] > 0 && OrderSelect(BuyTickets[0], SELECT_BY_TICKET) &&
OrderCloseTime() == 0)
{
if (OrderType() > 1)
if (MathAbs(OrderOpenPrice()-BuyLevel-Spread-Tick) >= Tick)//Цену открытия нужно
{ // изменить
if (BuyLevel + Tick - Bid > StopLevel)
if (WaitForTradeContext())
OrderModify(OrderTicket(), NP(BuyLevel + Spread + Tick), 0, 0, 0);
return(false); // Не все операции закончены
}
}
else // Первичного SellStop нет - устанавливаем
{
OpenOrderCorrect(OP_BUYSTOP, Lots1, NP(BuyLevel + Spread + Tick), 0, 0,
100*MagicNumber); // Открываем BuyStop
return(false); // Не все операции закончены
}
// - 2.4 - ============================= Окончание блока ============================
// - 2.5 - ================== Удаление оставшихся ордеров Buy Limit =================
if (!DeleteOrder(BuyTickets[1]) || !DeleteOrder(BuyTickets[2]))
return(False);
// - 2.5 - ============================= Окончание блока ============================
}
// - 2 - ================================ Окончание блока ===============================
return(True);
}
Каждая часть блока 2 соответствует описанной выше части блока 1.
Последняя функция, требующая пояснений, CheckAddingBuy (ее аналог CheckAddingSell для коротких позиций):
//+-------------------------------------------------------------------------------------+
//| Вспомогательная функция для проверки дополнительных BUY-ордеров |
//+-------------------------------------------------------------------------------------+
bool CheckAddingBuy(int Number, double Fibo, double Target, double volume)
{
if (BuyTickets[Number] > 0 && OrderSelect(BuyTickets[Number], SELECT_BY_TICKET) &&
OrderCloseTime() == 0)
{
// Ордер найден, проверяем уровень стопа
if (MathAbs(OrderStopLoss()-SellLevel+Tick) >= Tick) // Стоп нужно изменить
{
if ((OrderType() == OP_BUY && Bid - SellLevel + Tick > StopLevel) || // расстояние
(OrderType() == OP_BUYLIMIT && // от цены до нового стопа или от открытия до
OrderOpenPrice() - SellLevel + Tick > StopLevel)) // нового стопа достаточное
{
if (WaitForTradeContext())
OrderModify(OrderTicket(), OrderOpenPrice(), NP(SellLevel - Tick),
OrderTakeProfit(), 0);
}
else//Если расстояние недостаточное, то в случае присутствия BUYLIMIT удаляем его
if (OrderType() == OP_BUYLIMIT)
if (WaitForTradeContext())
OrderDelete(OrderTicket());
return(false); // Не все операции закончены
}
}
else // Ордера/позиции нет
if (BuyTime[Number] < LastPierce && // Последняя вторичная позиция открыта до пробития
TimeCurrent() - LastPierce < 2*60*Period()) // и еще не поздно установить ордер
{
double Price = (FirstBuyLevel - SellLevel)*(Fibo/100.0) + SellLevel + Spread;
double TP = (FirstBuyLevel - SellLevel)*(Target/100.0) + SellLevel;
if (Price - SellLevel + Tick < StopLevel || // Цена открытия изначально ниже стопа
SellLevel-Tick >= FirstBuyLevel)//или уровень стопа выше цены первичной позиции
return(True); // поэтому больше сюда на этом сигнале не вернемся
if (Ask - Price > StopLevel)
OpenOrderCorrect(OP_BUYLIMIT, volume, NP(Price), NP(SellLevel - Tick),
NP(TP), 100*MagicNumber+Number);
return(False); // Не все операции закончены
}
return(True);
}
Функцию условно можно разделить на два блока: присутствует ордер Buy Limit (или позиция Buy), и нет ордера/позиции. Если ордер или позиция есть, то проверяется правильность его/ее стоп-приказа и при необходимости изменяется. В случае если имеется ордер Buy Limit, то проверяется расстояние от цены открытия до уровня нового стоп-приказа. Если это расстояние слишком мало (меньше, чем уровень минимального стопа), то ордер удаляется и больше не устанавливается.
В случае отсутствия ордера или позиции, производится попытка установки ордера Buy Limit. Эта попытка не будет предпринята, если со времени последнего пробития уровня ATR (это время хранится в переменной LastPierce) прошло больше двух свечей (на графике Н1 - больше двух часов).
Оперируя перечисленными функциями, советник воспроизводит стратегию "Летучая мышь". Это позволяет произвести тестирование стратегии. Для получения необходимого и достаточного количества сделок (200), возьмем исторический период 01.01.2009 - 30.01.2010 и таймфрейм Н1. Для каждой валютной пары возьмем наиболее подходящие ей период индикатора ATR (ATRPeriod) и коэффициент умножения (Factor). Все остальные параметры одинаковы и равны значениям, принятым по умолчанию (см. рис. 2 - 5).
http://www.forextrade.ru/media/Image/MQLabs/52_ag/EURUSD.gif
Рис. 2. - График кривой баланса, получаемый при тестировании советника на валютной паре EURUSD.
EURUSD. Однозначно лучший из сегодняшних результатов (ATRPeriod = 7, Factor = 4). Чистая прибыль 5388 долларов против максимальной просадки 1428 долларов (ФВ = 3.77). В пользу стратегии также свидетельствует поступательное движение кривой баланса вверх, где небольшие спады обязательно чередуются с подъемами. В дополнение отмечаем почти двукратное превосходство средней прибыльной сделки над средней убыточной - 113.91 против 65.60.
http://www.forextrade.ru/media/Image/MQLabs/52_ag/USDCHF.gif
Рис. 3. - График кривой баланса, получаемый при тестировании советника на валютной паре USDCHF.
Неровный характер кривой баланса, показанный на валютной паре [I]USDCHF (ATRPeriod = 23, Factor = 5) не дает уверенности в стратегии. Тем не менее, чистая прибыль 4735 долларов против 1581 доллар максимальной просадки (ФВ = 2.99).
http://www.forextrade.ru/media/Image/MQLabs/52_ag/GBPUSD.gif
Рис. 4. - График кривой баланса, получаемый при тестировании советника на валютной паре GBPUSD.
Единственная валютная пара, которой нельзя занести стратегию в актив - GBPUSD (ATRPeriod = 12, Factor = 5). Да, на определенном участке стратегия отработала просто "на ура", дав за 70 сделок почти 7000 долларов. Но в итоге половина прибыли была растеряна (чистая прибыль 4122), а максимальная просадка выросла до катастрофических размеров (4124).
http://www.forextrade.ru/media/Image/MQLabs/52_ag/USDJPY.gif
Рис. 5. - График кривой баланса, получаемый при тестировании советника на валютной паре USDJPY.
Последнее время все большее количество стратегий удачно вписывается в концепцию валютной пары USDJPY. В данном случае ей можно уверенно присудить победу в номинации "самый лучший вид кривой баланса". К тому же, чистая прибыль составила приличные 4275 долларов против максимальной просадки 1364 доллара (ФВ = 3.13). При более подробном анализе работы стратегии на йене, можно заметить, что чем шире зона локальной просадки, тем стремительнее происходит последующее поглощение убытков (то есть получение прибыли). Вот уж поистине - "терпи и надейся".
Доработка стратегии для использования в AutoGraf 4.0
Из всех рассматриваемых до сих пор стратегий не было еще ни одной, которая бы не использовала данные панели инструментов AutoGraf 4.0. Но, в виду обилия входных параметров (даже настроек объемов сделок не одна, а три), придется все их определить в качестве внешних переменных самого AutoGraf. Они будут занимать параметры AT_1 - AT_12. Так, AT_1 - период ATR, AT_2 - цена, от которой считаются границы канала, AT_3 - множитель Factor. Следующие три параметра AT_4 - AT_6 соответствуют объемам каждого из ордеров 1, 2 и 3. AT_7 и AT_8 - уровни открытия вторичного и третичного ордеров в процентах сетки Фибоначчи. И последние три параметра AT_9 - AT_11 - цели каждого из ордеров, соответственно 1-го, 2-го и 3-го.
Для запуска советника из-под AutoGraf 4.0 совершите такие шаги:
Воспользуйтесь ссылкой Файлы стратегий для Autograf 4.0 (http://www.forextrade.ru/media/Image/MQLabs/52_ag/AG_NightBat.zip) и полученный архив распакуйте в папку MT4\experts\libraries. Запустите AutoGraf. Для получения похожих с тестами результатов работы (на примере EURUSD) выставьте значения AT_1 = 7, AT_2 = 0, AT_3 = 4, AT_4 = 0.1, AT_5 = 0.1, AT_6 = 0.2, AT_7 = 61.8, AT_8 = 38.2, AT_9 = 161.8, AT_10 = 161.8 и AT_11 - 161.8. Выберите стратегию №3. Для этого передвиньте вверх значок So и среди названий стратегий найдите значок S3, который также потяните вверх. Запустите функцию автоматической торговли, передвинув значок AT в верхнее положение.
Использование полученного советника рекомендуется только в полуавтоматическом режиме под присмотром трейдера и после всестороннего изучения слабых и сильных сторон стратегии.
Советник NightBat (http://www.forextrade.ru/media/Image/MQLabs/52_ag/NightBat.mq4)
Советник ATR_The_bat (http://www.forextrade.ru/media/Image/MQLabs/52_ag/ATR_The_bat.mq4)
Файлы стратегии для AutoGraf 4.0 (http://www.forextrade.ru/media/Image/MQLabs/52_ag/AG_NightBat.zip)
Развернутые результаты тестирования эксперта (http://www.forextrade.ru/media/Image/MQLabs/52_ag/Test.zip)
Название торговой системы довольно поэтическое - "Летучая мышь". Это одна из распространенных тактик, но, как оказалось, на ее основе советники еще не создавались. Соответственно, результатов тестирования за сколько-нибудь серьезный исторический промежуток найти тоже не удалось. Именно этот пробел в истории торговых систем и будет сегодня заполнен.
Основным поставщиком сигналов системы выступает индикатор ATR_The_bat. Правда, есть у этого индикатора и другие названия, например, ATR_Stops. Как следует из названия, в основе индикатора лежит стандартный технический индикатор MT4 - Average True Range (ATR). От каждой свечи вверх и вниз откладывается расстояние, соответствующее текущему значению ATR, умноженное на множитель Factor. Чаще всего применяют четырех- или пятикратное значение ATR. В результате выходит что-то наподобие канала, с тем условием, что его стенки могут лишь сходиться, а не расходиться. В полученном канале цена движется до тех пор, пока не достигнет одной из стенок. Если зафиксировано пробитие границы канала, то противоположная граница принимает новое расчетное значение без оглядки на предыдущие.
Именно пробитие одной из границ канала является сигналом для совершения сделки (см. рис. 1)
http://www.forextrade.ru/media/Image/MQLabs/52_ag/figure_1.gif
Рис. 1 - Пробитие границы канала и открытие сделок.
Чтобы не упустить момент входа, на видимой границе канала (индикатор ATR_The_bat все время отображает только одну границу) будем устанавливать отложенный ордер. Когда он сработает, появится противоположная граница канала, которая будет являться уровнем стоп-приказа для текущей позиции.
Между двумя полученными уровнями располагаем объект "линии Фибоначчи" и определяем два дополнительных уровня - 61.8% и 38.2%. Это уровни для установки дополнительных лимитных ордеров, которые в случае благоприятного развития событий значительно увеличат полученную прибыль, а в неблагоприятном случае увеличат убыток, но далеко не на сумму потенциальной прибыли. Как вы, наверное, догадались, уровень стоп-приказов у всех ордеров ставится на один и тот же уровень.
С движением цены в сторону прибыли первичной сделки уровень поддержки, отображаемый индикатором ATR_The_bat, повышается, и в какой-то момент может стать выше цены открытия одного из лимитных ордеров. В этом случае лимитный ордер следует удалить, так как невозможно установить стоп-приказ ордера Buy Limit выше уровня его открытия.
Цель каждого из ордеров можно настраивать отдельно, но наиболее выгодным является закрытие всех позиций на одном уровне. В системе рекомендуется значение 161.8 от ширины канала. В этом случае выходит, что у самого нижнего ордера Buy Limit соотношение прибыль/риск очень высокое. Поэтому рекомендуется увеличивать объем этого ордера в два раза, по сравнению с предыдущим ордером.
Точно такие же условия открытия сделок применяются в случае пробития нижней границы канала, когда первичная позиция короткая, а вспомогательные отложенные ордера имеют тип Sell Limit.
Как видно, система не очень сложная, хотя именно вручную исполнять ее затруднительно из-за необходимости все время находится недалеко от компьютера. Здесь как раз и проявится "усидчивость" автоматических торговых систем, которым неведома усталость. И пусть простота системы очевидна, запрограммировать ее не так уж и легко.
Сложности возникают не в генерации сигналов, а именно в части управления ордерами. Поэтому сигнальную часть сложной не назовешь:
//+-------------------------------------------------------------------------------------+
//| Расчет уровней для каждого направления торговли |
//+-------------------------------------------------------------------------------------+
void GetSignal()
{
// - 1 - ============================= Инициализация переменных =========================
BuyLevel = 0; SellLevel = 0;
// - 1 - ================================== Окончание блока =============================
// - 2 - =========== Поиск последнего пробитого уровня ATR ==============================
int i = iBarShift(Symbol(), 0, LastCountTime);// Начинаем с последнего посчитанного бара
while (i > 0)
{
double ApplyPrice = GetPrice(i);
double ATRF = iATR(Symbol(), 0, ATRPeriod, i)*Factor;
if (Dir == 0) // если направление не определено (первый подход к GetSignal), то
{
DnLevel = ApplyPrice - ATRF; // рассчитываем оба уровня - верхний и нижний
UpLevel = ApplyPrice + ATRF;
}
if (Dir >= 0) // При наличии восходящего движения следим за пробитием поддержки
if (Low <= DnLevel) // пробита ли поддержка?
{
Dir = -1; // если пробита, то меняем текущее направление движения на нисходящее
DnLevel = 0;
LastPierce = Time[i]; // запоминаем время пробития
}
else
DnLevel = MathMax(ApplyPrice - ATRF, DnLevel); // или высчитываем новый уровень
if (Dir <= 0) // При наличии нисходящего движения следим за пробитием сопротивления
if (High[i] >= UpLevel) // пробито ли сопротивление
{
Dir = 1; // если пробито, то меняем направление движения на восходящее
UpLevel = 1000;
LastPierce = Time[i]; // запоминаем время пробития
i++;
}
else
UpLevel = MathMin(ApplyPrice + ATRF, UpLevel); // или рассчитываем новый уровень
i--;
}
// - 2 - ================================== Окончание блока =============================
LastCountTime = Time[i]; // Последний посчитанный бар запоминаем
// - 3 - ================= Отображение верхнего уровня ATR выше цены ====================
if (Dir < 0) // текущее направление нисходящее
{
ShowLevel(UpLevel, Red); // отображаем уровень выше цены
BuyLevel = UpLevel; // это и будет уровнем открытия Buy Stop
}
// - 3 - ================================== Окончание блока =============================
// - 4 - ================== Отображение нижнего уровня ATR ==============================
if (Dir > 0) // текущее направление восходящее
{
ShowLevel(DnLevel, Blue); // отображаем уровень ниже цены
SellLevel = DnLevel; // это и будет уровнем открытия Sell Stop
}
// - 4 - ================================== Окончание блока =============================
}
Как давно уже повелось, уровни открытия сделок задают переменные BuyLevel и SellLevel, которые инициализируются в первом блоке.
Второй блок в точности повторяет алгоритм индикатора ATR_The_bat, избавляя от необходимости расчета значений индикатора через функцию iCustom. Такой подход позволяет немного ускорить работу советника. Главным в работе второго блока является определение текущего направления движения цен, что находит отражение в значении флага Dir. Его значение сохраняется от одного бара к другому. Поэтому, в совокупности с переменной LastCountBar, сохраняющей время открытия последнего посчитанного бара, они помогают осуществить еще одну часть оптимизации расчетов - не пересчитывать несколько сот или даже тысяч баров заново при каждом вызове функции GetSignal. Полный пересчет производится лишь после инициализации советника.
Третий и четвертый блоки, по сути, одинаковы. Разница лишь в сигналах открытия сделок и цвете линии сопротивления/поддержки.
Функция start, благодаря переносу всех основных действий в другие функции, никаких затруднений не вызывает:
//+-------------------------------------------------------------------------------------+
//| Функция START эксперта |
//+-------------------------------------------------------------------------------------+
int start()
{
// - 1 - == Разрешено ли советнику работать? ===========================================
if (!Activate || FatalError) // Отключается работа советника, если функция
return(0); // init завершилась с ошибкой или имела место фатальная ошибка
// - 1 - == Окончание блока ============================================================
// - 2 - == Сбор информации об условиях торговли ========================================
Spread = ND(MarketInfo(Symbol(), MODE_SPREAD)*Point); // текущий спрэд
StopLevel = ND(MarketInfo(Symbol(), MODE_STOPLEVEL)*Point); // текущий уровень стопов
FreezeLevel = ND(MarketInfo(Symbol(), MODE_FREEZELEVEL)*Point); // уровень заморозки
// - 2 - === Окончание блока ============================================================
// - 3 - ================ Расчет уровней открытия длинных и коротких позиций ============
if (LastBar == Time[0])
return(0);
if (LastSignal != Time[0])
{
GetSignal();
LastSignal = Time[0];
}
// - 3 - ================================ Окончание блока ==============================
// - 4 - =========== Изменение стопов позиций и цены открытия отложенных ордеров ========
FindOrders(); // Нахождение всех ордеров и позиций, сортировка по массивам тикетов
if (!CheckOrders())
return(0);
// - 4 - ========================== Окончание блока =====================================
LastBar = Time[0];
return(0);
}
В третьем блоке один раз за бар вызывается расчет уровней поддержки и сопротивления, а также производится слежение за тем, чтобы советник в целом не "перерабатывал".
В четвертом блоке расположены вызовы двух главных функций советника - FindOrders (поиск и подсчет ордеров советника) и CheckOrders (абсолютно все операции по изменению уровней стопов, цен открытия отложенных ордеров, удалению и установке ордеров).
Начнем с функции попроще - FindOrders:
//+-------------------------------------------------------------------------------------+
//| Нахождение всех своих ордеров и позиций. |
//+-------------------------------------------------------------------------------------+
void FindOrders()
{
ArrayInitialize(BuyTickets, -1); // "обнуление" тикетов
ArrayInitialize(SellTickets, -1); // "обнуление" тикетов
Buys = 0; Sells = 0; // Счетчики кол-ва своих ордеров и позиций
FirstBuyLevel = 0; FirstSellLevel = 0; // Уровни пробития
// - 1 - ============================ Выбираем свои ордера и позиции ====================
for (int i = 0; i < OrdersTotal(); i++)
if (OrderSelect(i, SELECT_BY_POS))
if (OrderSymbol() == Symbol() && MathFloor(OrderMagicNumber()/100) == MagicNumber)
// - 1 - ================================ Окончание блока ===============================
{
// - 2 - =============== Сортировка ордеров и позиций по номерам тикетов ================
int ID = MathMod(OrderMagicNumber(), 100);
if (MathMod(OrderType(), 2) == 0) // Найден BUY-ордер
{
BuyTickets[ID] = OrderTicket();
BuyTime[ID] = MathMax(BuyTime[ID], OrderOpenTime());
Buys++;
if (ID == 0 && OrderType() == OP_BUY)
FirstBuyLevel = OrderOpenPrice() - Spread;
}
else // найден Sell-ордер
{
SellTickets[ID] = OrderTicket();
SellTime[ID] = MathMax(SellTime[ID], OrderOpenTime());
Sells++;
if (ID == 0 && OrderType() == OP_SELL)
FirstSellLevel = OrderOpenPrice();
}
// - 2 - ================================ Окончание блока ===============================
}
}
Так как советник оперирует несколькими одновременно открытыми позициями, то ему необходимо различать свои же ордера между собой. Для этого каждому из трех возможных ордеров присваивается порядковый номер - 0, 1 или 2. Он прибавляется к младшему разряду поля MagicNumber, в то время как в старшие разряды (значение выше 100) записывается идентификатор советника, задаваемый в одноименном входном параметре MagicNumber. Таким образом, значение параметра MagicNumber умножается на 100, а затем уже к нему добавляется порядковый номер ордера. Чтобы впоследствии определить идентификатор советника, нужно значение поля MagicNumber ордера разделить на 100 без остатка и сравнить с идентификатором. Соответственно, для определения порядкового номера ордера нужно взять остаток от деления значения поля MagicNumber на 100.
Таким образом, функция FindOrders сортирует ордера по массивам BuyTickets и SellTickets, сохраняя тикеты ордеров под порядковыми номерами. Точно также сортируются времена открытия последних ордеров каждого типа в массивы SellTime и BuyTime.
Переменные FirstBuyLevel и FirstSellLevel содержат значение уровней открытия первичной длинной и короткой сделок соответственно. Они будут использованы для расчета уровней Фибоначчи.
В виду объемности функции CheckOrders, рассмотрим ее, разбив на два блока - работа в восходящем и работа в нисходящем движении:
// - 1 - ==================== Активен сигнал открытия длинных позиций ===================
if (SellLevel > 0)
{
// - 1.1 - ============== Проверка наличия основной позиции и ее стопа ==============
if (BuyTickets[0] > 0 && OrderSelect(BuyTickets[0], SELECT_BY_TICKET) &&
OrderCloseTime() == 0)
{
if (OrderType() > 1) // Если это до сих пор ордер, то удаляем его
{
OrderDelete(OrderTicket());
return(false); // Не все операции закончены
}
if (MathAbs(OrderStopLoss()-SellLevel+Tick) >= Tick) // Стоп нужно изменить
{
if (Bid - SellLevel+Tick > StopLevel)
if (WaitForTradeContext())
OrderModify(OrderTicket(), 0, NP(SellLevel-Tick), OrderTakeProfit(), 0);
return(false); // Не все операции закончены
}
if (OrderTakeProfit() == 0) // Если профит еще не установлен, то установим
{
double TP = (FirstBuyLevel - SellLevel)*(FiboTarget1/100.0) + SellLevel;
if (TP - Bid <= StopLevel)
TP = Bid + StopLevel + Tick;
if (WaitForTradeContext())
OrderModify(OrderTicket(), 0, OrderStopLoss(), NP(TP), 0);
return(False);
}
}
else
if (BuyTime[0]<LastPierce &&// Последняя первичная позиция была открыта до пробития
TimeCurrent() - LastPierce < 2*60*Period()) // и еще не поздно открыть позицию
{
OpenOrderCorrect(OP_BUY, Lots1, NP(Ask), NP(SellLevel-Tick), 0,
100*MagicNumber); // Открываем BUY
return(false); // Не все операции закончены
}
// - 1.1 - ============================= Окончание блока ============================
// - 1.2 - ============ Проверка наличия второго ордера/позиции и ее стопа ==========
if (!CheckAddingBuy(1, Fibo2, FiboTarget2, Lots2))
return(False);
// - 1.2 - ============================= Окончание блока ============================
// - 1.3 - ============ Проверка наличия третьего ордера/позиции и ее стопа =========
if (!CheckAddingBuy(2, Fibo3, FiboTarget3, Lots3))
return(False);
// - 1.3 - ============================= Окончание блока ============================
// - 1.4 - ===== Установка первичного ордера SellStop и проверка цены открытия ======
if (SellTickets[0] > 0 && OrderSelect(SellTickets[0], SELECT_BY_TICKET) &&
OrderCloseTime() == 0)
{
if (OrderType() > 1)
if (MathAbs(OrderOpenPrice()-SellLevel+Tick)>=Tick) //Цену открытия нужно менять
{
if (Bid - SellLevel + Tick > StopLevel)
if (WaitForTradeContext())
OrderModify(OrderTicket(), NP(SellLevel - Tick), 0, 0, 0);
return(false); // Не все операции закончены
}
}
else // Первичного SellStop нет - устанавливаем
{
OpenOrderCorrect(OP_SELLSTOP, Lots1, NP(SellLevel-Tick), 0, 0,
100*MagicNumber); // Открываем Sell Stop
return(false); // Не все операции закончены
}
// - 1.4 - ============================= Окончание блока ============================
// - 1.5 - ================== Удаление оставшихся ордеров Sell Limit ================
if (!DeleteOrder(SellTickets[1]) || !DeleteOrder(SellTickets[2]))
return(False);
// - 1.5 - ============================= Окончание блока ============================
}
// - 1 - ================================ Окончание блока ===============================
Для понимания всего этого огромного скопления команд нужно, прежде всего, понять, что должен делать эксперт в том или ином случае. Мы в данный момент рассматриваем всего два развития событий: есть сигнал нисходящего движения и есть сигнал восходящего движения. Первый блок описывает случай наличия восходящего движения, то есть, когда линия ATR_The_bat находится под ценой, и у нас есть лишь уровень входа для продаж (SellLevel > 0). Алгоритм действий выходит такой (номера пунктов соответствуют номерам вложенных блоков):
1.1. Первичная позиция BUY должна быть открыта. То есть отложенный ордер должен сработать. Если ордер так и не сработал (это свидетельствует о том, что был перерыв в работе советника), то нужно его удалить и открыть позицию с рынка. В блоке это делается так: удаляется ордер и происходит окончание работы советника на текущем тике. К следующему тику не будет ни позиции, ни ордера, что приведет к автоматическому открытию позиции с рынка (часть блока после else). Если же позиция благополучно открыта, то проверяется правильность уровня стоп-приказа (соответствие изменяющейся линии поддержки) и установка уровня профита. После установки профита его правильность в будущем не проверяется.
1.2. Сюда выполнение дойдет лишь после полной проверки первичной позиции. Проверяется, существует ли вторая позиция или ордер Buy Limit. Для этого используется функция CheckAddingBuy (будет рассмотрена ниже). Если все операции со второй позицией завершены, и нет надобности к ней возвращаться, то переходим к блоку 1.3.
1.3. Аналогично проверяется третья, последняя, позиция, которая по умолчанию оперирует двойным лотом. Если все завершилось успешно, то переходим к 1.4.
1.4. На уровень стопа длинных позиций необходимо установить отложенный ордер Sell Stop, который будет первичным при развороте движения цены. Если ордер не существует, то производится его установка. Уровни стоп-приказа и профита на данном этапе рассчитать невозможно. Поэтому они с нулями. Если же ордер уже существует, то проверяется равенство цены его открытия уровню стоп-приказа длинных позиций, чтобы закрытие длинных и открытие короткой произошло по одной цене. Когда и в этом блоке все завершилось успешно, переходим к 1.5.
1.5. Если к моменту открытия длинных позиций остались не сработавшие ордера SellLimit, то их необходимо удалить. Этим занимается функция DeleteOrder. Ее код тривиален и здесь разобран не будет.
По точно такому же алгоритму, только с заменой Buy на Sell и Sell на Buy происходит работа второго блока функции CheckOrders:
// - 2 - =================== Активен сигнал открытия коротких позиций ===================
if (BuyLevel > 0)
{
// - 2.1 - ============== Проверка наличия основной позиции и ее стопа ==============
if (SellTickets[0] > 0 && OrderSelect(SellTickets[0], SELECT_BY_TICKET) &&
OrderCloseTime() == 0)
{
if (OrderType() > 1) // Если это до сих пор ордер, то удаляем его
{
if (WaitForTradeContext())
OrderDelete(OrderTicket());
return(false); // Не все операции закончены
}
if (MathAbs(OrderStopLoss()-BuyLevel-Spread-Tick) >= Tick) // Стоп нужно изменить
{
if (BuyLevel - Bid > StopLevel)
if (WaitForTradeContext())
OrderModify(OrderTicket(), 0, NP(BuyLevel + Spread + Tick),
OrderTakeProfit(), 0);
return(false); // Не все операции закончены
}
if (OrderTakeProfit() == 0) // Если профит еще не установлен, то установим
{
TP = BuyLevel - (BuyLevel - FirstSellLevel)*(FiboTarget1/100.0);
if (Ask - TP <= StopLevel)
TP = Ask - StopLevel - Tick;
if (WaitForTradeContext())
OrderModify(OrderTicket(), 0, OrderStopLoss(), NP(TP), 0);
return(False);
}
}
else
if (SellTime[0]<LastPierce &&//Последняя первичная позиция была открыта до пробития
TimeCurrent() - LastPierce < 2*60*Period()) // и еще не поздно открыть позицию
{
OpenOrderCorrect(OP_SELL, Lots1, NP(Bid), NP(BuyLevel + Spread + Tick), 0,
100*MagicNumber); // Открываем SELL
return(false); // Не все операции закончены
}
// - 2.1 - ============================= Окончание блока ============================
// - 2.2 - ============ Проверка наличия второго ордера/позиции и ее стопа ==========
if (!CheckAddingSell(1, Fibo2, FiboTarget2, Lots2))
return(False);
// - 2.2 - ============================= Окончание блока ============================
// - 2.3 - ============ Проверка наличия третьего ордера/позиции и ее стопа =========
if (!CheckAddingSell(2, Fibo3, FiboTarget3, Lots3))
return(False);
// - 2.3 - ============================= Окончание блока ============================
// - 2.4 - ===== Установка первичного ордера BuyStop и проверка цены открытия =======
if (BuyTickets[0] > 0 && OrderSelect(BuyTickets[0], SELECT_BY_TICKET) &&
OrderCloseTime() == 0)
{
if (OrderType() > 1)
if (MathAbs(OrderOpenPrice()-BuyLevel-Spread-Tick) >= Tick)//Цену открытия нужно
{ // изменить
if (BuyLevel + Tick - Bid > StopLevel)
if (WaitForTradeContext())
OrderModify(OrderTicket(), NP(BuyLevel + Spread + Tick), 0, 0, 0);
return(false); // Не все операции закончены
}
}
else // Первичного SellStop нет - устанавливаем
{
OpenOrderCorrect(OP_BUYSTOP, Lots1, NP(BuyLevel + Spread + Tick), 0, 0,
100*MagicNumber); // Открываем BuyStop
return(false); // Не все операции закончены
}
// - 2.4 - ============================= Окончание блока ============================
// - 2.5 - ================== Удаление оставшихся ордеров Buy Limit =================
if (!DeleteOrder(BuyTickets[1]) || !DeleteOrder(BuyTickets[2]))
return(False);
// - 2.5 - ============================= Окончание блока ============================
}
// - 2 - ================================ Окончание блока ===============================
return(True);
}
Каждая часть блока 2 соответствует описанной выше части блока 1.
Последняя функция, требующая пояснений, CheckAddingBuy (ее аналог CheckAddingSell для коротких позиций):
//+-------------------------------------------------------------------------------------+
//| Вспомогательная функция для проверки дополнительных BUY-ордеров |
//+-------------------------------------------------------------------------------------+
bool CheckAddingBuy(int Number, double Fibo, double Target, double volume)
{
if (BuyTickets[Number] > 0 && OrderSelect(BuyTickets[Number], SELECT_BY_TICKET) &&
OrderCloseTime() == 0)
{
// Ордер найден, проверяем уровень стопа
if (MathAbs(OrderStopLoss()-SellLevel+Tick) >= Tick) // Стоп нужно изменить
{
if ((OrderType() == OP_BUY && Bid - SellLevel + Tick > StopLevel) || // расстояние
(OrderType() == OP_BUYLIMIT && // от цены до нового стопа или от открытия до
OrderOpenPrice() - SellLevel + Tick > StopLevel)) // нового стопа достаточное
{
if (WaitForTradeContext())
OrderModify(OrderTicket(), OrderOpenPrice(), NP(SellLevel - Tick),
OrderTakeProfit(), 0);
}
else//Если расстояние недостаточное, то в случае присутствия BUYLIMIT удаляем его
if (OrderType() == OP_BUYLIMIT)
if (WaitForTradeContext())
OrderDelete(OrderTicket());
return(false); // Не все операции закончены
}
}
else // Ордера/позиции нет
if (BuyTime[Number] < LastPierce && // Последняя вторичная позиция открыта до пробития
TimeCurrent() - LastPierce < 2*60*Period()) // и еще не поздно установить ордер
{
double Price = (FirstBuyLevel - SellLevel)*(Fibo/100.0) + SellLevel + Spread;
double TP = (FirstBuyLevel - SellLevel)*(Target/100.0) + SellLevel;
if (Price - SellLevel + Tick < StopLevel || // Цена открытия изначально ниже стопа
SellLevel-Tick >= FirstBuyLevel)//или уровень стопа выше цены первичной позиции
return(True); // поэтому больше сюда на этом сигнале не вернемся
if (Ask - Price > StopLevel)
OpenOrderCorrect(OP_BUYLIMIT, volume, NP(Price), NP(SellLevel - Tick),
NP(TP), 100*MagicNumber+Number);
return(False); // Не все операции закончены
}
return(True);
}
Функцию условно можно разделить на два блока: присутствует ордер Buy Limit (или позиция Buy), и нет ордера/позиции. Если ордер или позиция есть, то проверяется правильность его/ее стоп-приказа и при необходимости изменяется. В случае если имеется ордер Buy Limit, то проверяется расстояние от цены открытия до уровня нового стоп-приказа. Если это расстояние слишком мало (меньше, чем уровень минимального стопа), то ордер удаляется и больше не устанавливается.
В случае отсутствия ордера или позиции, производится попытка установки ордера Buy Limit. Эта попытка не будет предпринята, если со времени последнего пробития уровня ATR (это время хранится в переменной LastPierce) прошло больше двух свечей (на графике Н1 - больше двух часов).
Оперируя перечисленными функциями, советник воспроизводит стратегию "Летучая мышь". Это позволяет произвести тестирование стратегии. Для получения необходимого и достаточного количества сделок (200), возьмем исторический период 01.01.2009 - 30.01.2010 и таймфрейм Н1. Для каждой валютной пары возьмем наиболее подходящие ей период индикатора ATR (ATRPeriod) и коэффициент умножения (Factor). Все остальные параметры одинаковы и равны значениям, принятым по умолчанию (см. рис. 2 - 5).
http://www.forextrade.ru/media/Image/MQLabs/52_ag/EURUSD.gif
Рис. 2. - График кривой баланса, получаемый при тестировании советника на валютной паре EURUSD.
EURUSD. Однозначно лучший из сегодняшних результатов (ATRPeriod = 7, Factor = 4). Чистая прибыль 5388 долларов против максимальной просадки 1428 долларов (ФВ = 3.77). В пользу стратегии также свидетельствует поступательное движение кривой баланса вверх, где небольшие спады обязательно чередуются с подъемами. В дополнение отмечаем почти двукратное превосходство средней прибыльной сделки над средней убыточной - 113.91 против 65.60.
http://www.forextrade.ru/media/Image/MQLabs/52_ag/USDCHF.gif
Рис. 3. - График кривой баланса, получаемый при тестировании советника на валютной паре USDCHF.
Неровный характер кривой баланса, показанный на валютной паре [I]USDCHF (ATRPeriod = 23, Factor = 5) не дает уверенности в стратегии. Тем не менее, чистая прибыль 4735 долларов против 1581 доллар максимальной просадки (ФВ = 2.99).
http://www.forextrade.ru/media/Image/MQLabs/52_ag/GBPUSD.gif
Рис. 4. - График кривой баланса, получаемый при тестировании советника на валютной паре GBPUSD.
Единственная валютная пара, которой нельзя занести стратегию в актив - GBPUSD (ATRPeriod = 12, Factor = 5). Да, на определенном участке стратегия отработала просто "на ура", дав за 70 сделок почти 7000 долларов. Но в итоге половина прибыли была растеряна (чистая прибыль 4122), а максимальная просадка выросла до катастрофических размеров (4124).
http://www.forextrade.ru/media/Image/MQLabs/52_ag/USDJPY.gif
Рис. 5. - График кривой баланса, получаемый при тестировании советника на валютной паре USDJPY.
Последнее время все большее количество стратегий удачно вписывается в концепцию валютной пары USDJPY. В данном случае ей можно уверенно присудить победу в номинации "самый лучший вид кривой баланса". К тому же, чистая прибыль составила приличные 4275 долларов против максимальной просадки 1364 доллара (ФВ = 3.13). При более подробном анализе работы стратегии на йене, можно заметить, что чем шире зона локальной просадки, тем стремительнее происходит последующее поглощение убытков (то есть получение прибыли). Вот уж поистине - "терпи и надейся".
Доработка стратегии для использования в AutoGraf 4.0
Из всех рассматриваемых до сих пор стратегий не было еще ни одной, которая бы не использовала данные панели инструментов AutoGraf 4.0. Но, в виду обилия входных параметров (даже настроек объемов сделок не одна, а три), придется все их определить в качестве внешних переменных самого AutoGraf. Они будут занимать параметры AT_1 - AT_12. Так, AT_1 - период ATR, AT_2 - цена, от которой считаются границы канала, AT_3 - множитель Factor. Следующие три параметра AT_4 - AT_6 соответствуют объемам каждого из ордеров 1, 2 и 3. AT_7 и AT_8 - уровни открытия вторичного и третичного ордеров в процентах сетки Фибоначчи. И последние три параметра AT_9 - AT_11 - цели каждого из ордеров, соответственно 1-го, 2-го и 3-го.
Для запуска советника из-под AutoGraf 4.0 совершите такие шаги:
Воспользуйтесь ссылкой Файлы стратегий для Autograf 4.0 (http://www.forextrade.ru/media/Image/MQLabs/52_ag/AG_NightBat.zip) и полученный архив распакуйте в папку MT4\experts\libraries. Запустите AutoGraf. Для получения похожих с тестами результатов работы (на примере EURUSD) выставьте значения AT_1 = 7, AT_2 = 0, AT_3 = 4, AT_4 = 0.1, AT_5 = 0.1, AT_6 = 0.2, AT_7 = 61.8, AT_8 = 38.2, AT_9 = 161.8, AT_10 = 161.8 и AT_11 - 161.8. Выберите стратегию №3. Для этого передвиньте вверх значок So и среди названий стратегий найдите значок S3, который также потяните вверх. Запустите функцию автоматической торговли, передвинув значок AT в верхнее положение.
Использование полученного советника рекомендуется только в полуавтоматическом режиме под присмотром трейдера и после всестороннего изучения слабых и сильных сторон стратегии.
Советник NightBat (http://www.forextrade.ru/media/Image/MQLabs/52_ag/NightBat.mq4)
Советник ATR_The_bat (http://www.forextrade.ru/media/Image/MQLabs/52_ag/ATR_The_bat.mq4)
Файлы стратегии для AutoGraf 4.0 (http://www.forextrade.ru/media/Image/MQLabs/52_ag/AG_NightBat.zip)
Развернутые результаты тестирования эксперта (http://www.forextrade.ru/media/Image/MQLabs/52_ag/Test.zip)