Рецепты MQL5 - Торговые сигналы скользящих каналов
Введение
В
своей статье «Рецепты MQL5 - Программируем скользящие каналы» я представил
способ построения равноудалённых каналов, которые часто называют скользящими. При
этом для решения задачи я обращался к инструменту «Равноудалённый канал» и
возможностям ООП.
В
данной статье речь пойдёт о сигналах, которые можно идентифицировать при
использовании этих каналов. На основании этих сигналов попробуем создать торговую стратегию.
На MQL5 уже опубликовано несколько статей, посвящённых генерации торговых сигналов посредством обращения к готовым модулям Стандартной библиотеки. Надеюсь, что данная статья дополнит материал и расширит круг пользователей стандартных классов.
Тем, кто начинает своё знакомство с этой стратегией, я предлагаю изучать материал от простого к сложному. Сначала создадим базовую стратегию, а потом по возможности будем её усложнять и дополнять.
1. Индикатор равноудалённых каналов
В предыдущей статье о скользящих каналах советник сам строил каналы, создавая графические объекты. Такой подход, с одной стороны, облегчал для программиста задачу, но, с другой стороны, делает некоторые вещи невозможными. Например, если советник будет работать в режиме оптимизации, то он не обнаружит графические объекты на графике, ведь графика не будет вовсе. Согласно ограничениям при тестировании:
Графические объекты при тестировании
Во время тестирования/оптимизации не осуществляется построение графических объектов. Таким образом, при обращении к свойствам созданного объекта во время тестирования/оптимизации эксперт получит нулевые значения.
Данное ограничение не распространяется на тестирование в визуальном режиме. |
Поэтому я пошёл по другому пути и создал индикатор, отражающий как фракталы, так и актуальный канал.
Этот индикатор называется EquidistantChannels. Он состоит, по сути, из двух блоков. В первом рассчитываются буферы фракталов, а во втором — буферы канала.
Приведу код обработчика события Calculate.
//+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- если на предыдущем вызове не было баров if(prev_calculated==0) { //--- обнулить буферы ArrayInitialize(gUpFractalsBuffer,0.); ArrayInitialize(gDnFractalsBuffer,0.); ArrayInitialize(gUpperBuffer,0.); ArrayInitialize(gLowerBuffer,0.); ArrayInitialize(gNewChannelBuffer,0.); } //--- Расчёт для фракталов [start] int startBar,lastBar; //--- if(rates_total<gMinRequiredBars) { Print("Недостаточно данных для расчёта"); return 0; } //--- if(prev_calculated<gMinRequiredBars) startBar=gLeftSide; else startBar=rates_total-gMinRequiredBars; //--- lastBar=rates_total-gRightSide; for(int bar_idx=startBar; bar_idx<lastBar && !IsStopped(); bar_idx++) { //--- if(isUpFractal(bar_idx,gMaxSide,high)) gUpFractalsBuffer[bar_idx]=high[bar_idx]; else gUpFractalsBuffer[bar_idx]=0.0; //--- if(isDnFractal(bar_idx,gMaxSide,low)) gDnFractalsBuffer[bar_idx]=low[bar_idx]; else gDnFractalsBuffer[bar_idx]=0.0; } //--- Расчёт для фракталов [end] //--- Расчёт для границ канала [start] if(prev_calculated>0) { //--- если набор не инициализирован if(!gFracSet.IsInit()) if(!gFracSet.Init( InpPrevFracNum, InpBarsBeside, InpBarsBetween, InpRelevantPoint, InpLineWidth, InpToLog )) { Print("Ошибка инициализации фрактального набора!"); return 0; } //--- обсчёт gFracSet.Calculate(gUpFractalsBuffer,gDnFractalsBuffer,time, gUpperBuffer,gLowerBuffer, gNewChannelBuffer ); } //--- Расчёт для границ канала [end] //--- return value of prev_calculated for next call return rates_total; }
Блок, где происходит расчет значений фрактальных буферов, выделен жёлтым, а блок, где рассчитываются буферы канала, — зелёным. Легко заметить, что второй блок будет активирован не на первом, а только на последующем вызове обработчика. Такая реализация для второго блока позволяет получить уже заполненные фрактальные буферы.
Теперь несколько слов о наборе фрактальных точек — объекте CFractalSet. Ввиду того, что я изменил способ отображения канала, также пришлось модифицировать класс CFractalSet. Ключевым методом стал CFractalSet::Calculate, обсчитывающий буферы канала индикатора. Код приведён в файле CFractalPoint.mqh.
Теперь у нас есть основа — поставщик сигналов от равноудалённого канала. Работа индикатора представлена на видео.
2. Базовая стратегия
Итак, предлагаю начать с чего-то несложного, что с помощью ООП можно будет усовершенствовать и доработать. Пусть имеется какая-то базовая стратегия.
Эта стратегия будет
рассматривать достаточно простые правила торговли. Вход в рынок производится от
границ канала. При касании ценой нижней границы открываем покупку, а верхней –
продажу. На Рис.1 цена коснулась нижней границы, поэтому робот купил некоторый
объём. Торговые уровни (стоп-лосс и тейк-профит) имеют фиксированный размер и были
выставлены автоматически. Если позиция открыта, повторные сигналы на вход игнорируем.
Рис.1 Сигнал на вход
Хотел бы ещё отметить, что Стандартная библиотека уже довольно-таки сильно разрослась. В ней
уже есть много готовых классов, которыми можно воспользоваться.
Попробуем для начала «присоседиться» к сигнальному классу CExpertSignal. Согласно
документации, он является базовым классом для создания генераторов торговых сигналов.
На мой взгляд, данный класс был назван очень точно. Это не CTradeSignal и не CSignal, а именно класс сигналов, предназначенных для использования в коде советника — CExpertSignal.
Я не буду останавливаться на его содержании. В
статье «Мастер MQL5: Как написать свой модуль торговых сигналов» подробно
описаны методы сигнального класса.
2.1 Сигнальный класс CSignalEquidChannel
Итак, производный сигнальный класс получился таким:
//+------------------------------------------------------------------+ //| Class CSignalEquidChannel | //| Purpose: Класс торговых сигналов на основе равноудалённого | //| канала. | //| Потомок класса CExpertSignal. | //+------------------------------------------------------------------+ class CSignalEquidChannel : public CExpertSignal { protected: CiCustom m_equi_chs; // объект-индикатор "EquidistantChannels" //--- настраиваемые параметры int m_prev_frac_num; // предыдущих фракталов bool m_to_plot_fracs; // отображать фракталы? int m_bars_beside; // баров слева/справа от фрактала int m_bars_between; // промежуточных баров ENUM_RELEVANT_EXTREMUM m_relevant_pnt; // актуальная точка int m_line_width; // толщина линии bool m_to_log; // вести журнал? double m_pnt_in; // внутренний допуск,пп double m_pnt_out; // внешний допуск,пп bool m_on_start; // флаг сигнала при старте //--- расчётные double m_base_low_price; // базовая минимальная цена double m_base_high_price; // базовая максимальная цена double m_upper_zone[2]; // верхняя зона: [0]-внутренний допуск, [1]-внешний double m_lower_zone[2]; // нижняя зона datetime m_last_ch_time; // время появления последнего канала //--- "веса" рыночных моделей (0-100) int m_pattern_0; // "касание нижней границы канала - buy, верхней - sell" //--- === Methods === --- public: //--- конструктор/деструктор void CSignalEquidChannel(void); void ~CSignalEquidChannel(void){}; //--- методы установки настраиваемых параметров void PrevFracNum(int _prev_frac_num) {m_prev_frac_num=_prev_frac_num;} void ToPlotFracs(bool _to_plot) {m_to_plot_fracs=_to_plot;} void BarsBeside(int _bars_beside) {m_bars_beside=_bars_beside;} void BarsBetween(int _bars_between) {m_bars_between=_bars_between;} void RelevantPoint(ENUM_RELEVANT_EXTREMUM _pnt) {m_relevant_pnt=_pnt;} void LineWidth(int _line_wid) {m_line_width=_line_wid;} void ToLog(bool _to_log) {m_to_log=_to_log;} void PointsOutside(double _out_pnt) {m_pnt_out=_out_pnt;} void PointsInside(double _in_pnt) {m_pnt_in=_in_pnt;} void SignalOnStart(bool _on_start) {m_on_start=_on_start;} //--- методы настраивания "весов" рыночных моделей void Pattern_0(int _val) {m_pattern_0=_val;} //--- метод проверки настроек virtual bool ValidationSettings(void); //--- метод создания индикатора и таймсерий virtual bool InitIndicators(CIndicators *indicators); //--- методы проверки, если модели рынка сформированы virtual int LongCondition(void); virtual int ShortCondition(void); virtual double Direction(void); //--- protected: //--- метод инициализации индикатора bool InitCustomIndicator(CIndicators *indicators); //- получение значения верхней границы канала double Upper(int ind) {return(m_equi_chs.GetData(2,ind));} //- получение значения нижней границы канала double Lower(int ind) {return(m_equi_chs.GetData(3,ind));} //- получение флага появления канала double NewChannel(int ind) {return(m_equi_chs.GetData(4,ind));} }; //+------------------------------------------------------------------+
Отмечу несколько нюансов.
В данном классе основной сигнальщик — это сетап на равноудалённом канале. И в текущем варианте он является единственным. Других пока не будет совсем. В состав класса, в свою очередь, входит класс для работы с техническим индикатором пользовательского типа — CiCustom.
В качестве сигнальной модели используется базовая: "касание нижней границы канала — buy, верхней — sell". Ввиду того, что касание с точностью до пункта, скажем так, не самое вероятное событие, то используется некоторый буфер, границы которого можно регулировать. Параметр внешнего допуска m_pnt_out определяет размер допустимого выхода цены за пределы канала, а параметр внутреннего m_pnt_in — размер того, насколько цена не дойдёт до границы. Логика совершенно простая. Считаем, что цена коснулась границы канала, если она немного не дошла до неё или немного вышла за её пределы. На Рис.2 схематично представлен буфер, попав в который снизу, цена вместе с границей отработает модель.
Рис.2 Отработка базовой сигнальной модели
Параметр-массив m_upper_zone[2] очерчивает границы верхнего буфера, а m_lower_zone[2] — нижнего.
В примере уровень на $1,11552 выступает в качестве верхней границы канала (красная прямая). Уровень на $1,11452 отвечает за нижний предел буфера, а $1,11702 — за верхний. Тогда размер внешнего допуска — 150 пп, а внутреннего — 100 пп. Цена отображена синей кривой.
Параметр m_on_start позволяет проигнорировать сигналы первого канала при запуске робота на график, если такой канал уже отрисован. Если флаг сброшен, то робот будет работать только на следующем канале, а на текущем не будет обрабатывать торговые сигналы.
Параметры m_base_low_price и m_base_high_price сохраняют значения минимальной и максимальной цен актуального бара. Таким считается нулевой бар, если торговый режим будет потиковым, либо прошлый бар, если торговать можно будет лишь при появлении нового бара.
Теперь несколько слов о методах. Здесь отметил бы, что разработчик предоставляет достаточно широкую свободу действий — ведь примерно половину методов составляют виртуальные. А это означает, что мы можем по своему разумению реализовать поведение классов-потомков.
Начну с метода Direction(), который количественно оценивает потенциальное торговое направление:
//+------------------------------------------------------------------+ //| Определение "взвешенного" направления | //+------------------------------------------------------------------+ double CSignalEquidChannel::Direction(void) { double result=0.; //--- появление нового канала datetime last_bar_time=this.Time(0); bool is_new_channel=(this.NewChannel(0)>0.); //--- если игнорировать сигналы первого канала if(!m_on_start) //--- если первый канал обычно отражается при инициализации if(m_prev_frac_num==3) { static datetime last_ch_time=0; //--- если появился новый канал if(is_new_channel) { last_ch_time=last_bar_time; //--- если первый запуск if(m_last_ch_time==0) //--- запомнить время бара, где появился первый канал m_last_ch_time=last_ch_time; } //--- если времена совпадают if(m_last_ch_time==last_ch_time) return 0.; else //--- сбросить флаг m_on_start=true; } //--- индекс актуального бара int actual_bar_idx=this.StartIndex(); //--- установить границы double upper_vals[2],lower_vals[2]; // [0]-бар предшествующий актуальному, [1]-актуальный бар ArrayInitialize(upper_vals,0.); ArrayInitialize(lower_vals,0.); for(int idx=ArraySize(upper_vals)-1,jdx=0;idx>=0;idx--,jdx++) { upper_vals[jdx]=this.Upper(actual_bar_idx+idx); lower_vals[jdx]=this.Lower(actual_bar_idx+idx); if((upper_vals[jdx]==0.) || (lower_vals[jdx]==0.)) return 0.; } //--- получить цены double curr_high_pr,curr_low_pr; curr_high_pr=this.High(actual_bar_idx); curr_low_pr=this.Low(actual_bar_idx); //--- если цены получены if(curr_high_pr!=EMPTY_VALUE) if(curr_low_pr!=EMPTY_VALUE) { //--- запомнить цены m_base_low_price=curr_low_pr; m_base_high_price=curr_high_pr; //--- Определить цены для буферных зон //--- верхняя зона: [0]-внутренний допуск, [1]-внешний this.m_upper_zone[0]=upper_vals[1]-m_pnt_in; this.m_upper_zone[1]=upper_vals[1]+m_pnt_out; //--- нижняя зона: [0]-внутренний допуск, [1]-внешний this.m_lower_zone[0]=lower_vals[1]+m_pnt_in; this.m_lower_zone[1]=lower_vals[1]-m_pnt_out; //--- нормализация for(int jdx=0;jdx<ArraySize(m_lower_zone);jdx++) { this.m_lower_zone[jdx]=m_symbol.NormalizePrice(m_lower_zone[jdx]); this.m_upper_zone[jdx]=m_symbol.NormalizePrice(m_upper_zone[jdx]); } //--- проверить, не сходятся ли зоны if(this.m_upper_zone[0]<=this.m_lower_zone[0]) return 0.; //--- результат result=m_weight*(this.LongCondition()-this.ShortCondition()); } //--- return result; } //+------------------------------------------------------------------+
Первый блок в теле метода — это проверка того, требуется ли игнорировать первый канал на графике, если такой вообще присутствует.
Во втором блоке мы получаем текущие цены и определяются буферные зоны. Есть и проверка на схождение этих зон. Если канал будет слишком узким или буферные зоны — слишком широкими, то есть вероятность, что чисто математически цена может попасть в обе зоны. Поэтому такую ситуацию тоже нужно обработать.
Целевая строка выделена синим. Здесь получаем количественную оценку торгового направления, если оно есть.
Рассмотрим теперь метод LongCondition().
//+------------------------------------------------------------------+ //| Проверка условия на покупку | //+------------------------------------------------------------------+ int CSignalEquidChannel::LongCondition(void) { int result=0; //--- если задана минимальная цена if(m_base_low_price>0.) //--- если минимальная цена на уровне нижней границы if((m_base_low_price<=m_lower_zone[0]) && (m_base_low_price>=m_lower_zone[1])) { if(IS_PATTERN_USAGE(0)) result=m_pattern_0; } //--- return result; } //+------------------------------------------------------------------+
Для покупки проверяем, попала ли цена в нижнюю буферную зону. Если попала, то проверяем разрешение на задействование рыночной модели. Более подробно о конструкции вида "IS_PATTERN_USAGE(k)" можно прочесть в статье «Генератор торговых сигналов пользовательского индикатора».
Метод ShortCondition() работает по аналогии с вышеописанным. Только здесь в фокусе верхняя буферная зона.
//+------------------------------------------------------------------+ //| Проверка условия на продажу | //+------------------------------------------------------------------+ int CSignalEquidistantChannel::ShortCondition(void) { int result=0; //--- если задана максимальная цена if(m_base_high_price>0.) //--- если максимальная цена на уровне верхней границы if((m_base_high_price>=m_upper_zone[0]) && (m_base_high_price<=m_upper_zone[1])) { if(IS_PATTERN_USAGE(0)) result=m_pattern_0; } //--- return result; } //+------------------------------------------------------------------+
В классе инициализируется собственный индикатор с помощью метода InitCustomIndicator():
//+------------------------------------------------------------------+ //| Инициализация собственных индикаторов | //+------------------------------------------------------------------+ bool CSignalEquidChannel::InitCustomIndicator(CIndicators *indicators) { //--- добавление объекта в коллекцию if(!indicators.Add(GetPointer(m_equi_chs))) { PrintFormat(__FUNCTION__+": error adding object"); return false; } //--- задание параметров индикатора MqlParam parameters[8]; parameters[0].type=TYPE_STRING; parameters[0].string_value="EquidistantChannels.ex5"; parameters[1].type=TYPE_INT; parameters[1].integer_value=m_prev_frac_num; // 1) предыдущих фракталов parameters[2].type=TYPE_BOOL; parameters[2].integer_value=m_to_plot_fracs; // 2) отображать фракталы? parameters[3].type=TYPE_INT; parameters[3].integer_value=m_bars_beside; // 3) баров слева/справа от фрактала parameters[4].type=TYPE_INT; parameters[4].integer_value=m_bars_between; // 4) промежуточных баров parameters[5].type=TYPE_INT; parameters[5].integer_value=m_relevant_pnt; // 5) актуальная точка parameters[6].type=TYPE_INT; parameters[6].integer_value=m_line_width; // 6) толщина линии parameters[7].type=TYPE_BOOL; parameters[7].integer_value=m_to_log; // 7) вести журнал? //--- инициализация объекта if(!m_equi_chs.Create(m_symbol.Name(),_Period,IND_CUSTOM,8,parameters)) { PrintFormat(__FUNCTION__+": error initializing object"); return false; } //--- количество буферов if(!m_equi_chs.NumBuffers(5)) return false; //--- ok return true; } //+------------------------------------------------------------------+
В массиве параметров первым нужно указать строковое название индикатора.
В классе имеется и свой виртуальный метод ValidationSettings(). Он вызывает аналогичный метод предка и проверяет, корректно ли были заданы параметры канального индикатора. Ещё есть сервисные методы, получающие значения соответствующих буферов пользовательского индикатора.
Вот пока и всё, что касается производного сигнального класса.
2.2 Класс торговой стратегии CEquidChannelExpert
Для реализации заложенной базовой идеи придётся написать класс, производный от стандартного класса CExpert. На текущем шаге его код будет максимально компактным, поскольку, по сути, нужно изменить только поведение главного обработчика — метода Processing(). Он является виртуальным, что даёт нам возможность писать любые стратегии.
//+------------------------------------------------------------------+ //| Класс CEquidChannelExpert. | //| Цель: Класс для советника, торгующего по равноудалённому каналу. | //| Потомок класса CExpert. | //+------------------------------------------------------------------+ class CEquidChannelExpert : public CExpert { //--- === Data members === --- private: //--- === Methods === --- public: //--- constructor/destructor void CEquidChannelExpert(void){}; void ~CEquidChannelExpert(void){}; protected: virtual bool Processing(void); }; //+------------------------------------------------------------------+
Вот сам метод:
//+------------------------------------------------------------------+ //| Главный модуль | //+------------------------------------------------------------------+ bool CEquidChannelExpert::Processing(void) { //--- расчёт направления m_signal.SetDirection(); //--- check if open positions if(!this.SelectPosition()) { //--- модуль открытия позиции if(this.CheckOpen()) return true; } //--- если нет торговых операций return false; }
Всё предельно просто. Сначала сигнальный объект оценивает возможное торговое направление, а затем проверяется наличие открытой позиции. Если её нет, то будем искать возможность открыться. Если позиция есть, тогда выходим.
Код базовой стратегии реализован в файле BaseChannelsTrader.mq5.
Пример работы базовой стратегии представлен на видео.
Рис.3 Результаты базовой стратегии за 2013-2015 гг.
Был сделан прогон в Тестере стратегий на часовом таймфрейме по символу EURUSD. На графике баланса заметно, что базовая стратегия на некоторых участках работала по "принципу пилы": за убыточной серией следовала прибыльная. Значения параметров пользователя, которые были задействованы при тестировании, находятся в файле base_signal.set. В нём же указаны и параметры канала, значения которых будут неизменными во всех версиях стратегии.
Здесь и далее используется режим тестирования "Каждый тик на основе реальных тиков".
По большому счёту, есть 2 способа улучшить торговые показатели стратегии. Первый — это оптимизация, заключающаяся в том, чтобы подобрать такое сочетание значений параметров, которые максимизирует прибыль и т.п. Второй способ касается поиска факторов, влияющих на результативность советника. Если первый способ не связан с изменением логики торговой стратегии, то второй без этого не обойдётся.
В следующем разделе будем править базовую стратегию и искать факторы результативности.
3. Факторы результативности
Несколько слов о диспозиции. На мой взгляд, удобно расположить все файлы стратегии, которые делают её уникальной в одной папке проектов. Так, реализация базовой стратегии находится в подпапке Base (Рис.4) и т.д.
Рис.4 Пример иерархии папок проектов стратегии каналов
Далее будем считать каждый фактор новым этапом для внесения изменений в исходные файлы, образующие код советника.
3.1 Использование трала
Для начала предлагаю добавить в нашу стратегию возможность трала. Пусть это будет объект класса CTrailingFixedPips, который позволяет сопровождать открытые позиции на фиксированном "расстоянии" (в пунктах). При этом тралиться будет как цена стоп-лосса, так и цена тейк-профита. Чтобы профит не тралился, нужно указать нулевое значение для соответствующего параметра (InpProfitLevelPips).
Внесем в код следующие изменения.
В исходный файл советника ChannelsTrader1.mq5 добавим группу пользовательских параметров:
//--- sinput string Info_trailing="+===-- Трал --====+"; // +===-- Трал --====+ input int InpStopLevelPips=30; // Уровень для StopLoss, пп input int InpProfitLevelPips=50; // Уровень для TakeProfit, пп
В блок инициализации запишем, что создаём объект типа CTrailingFixedPips, включаем его в состав стратегии и задаём параметры трала.
//--- объект трала CTrailingFixedPips *trailing=new CTrailingFixedPips; if(trailing==NULL) { //--- ошибка printf(__FUNCTION__+": error creating trailing"); myChannelExpert.Deinit(); return(INIT_FAILED); } //--- добавление объекта трала if(!myChannelExpert.InitTrailing(trailing)) { //--- ошибка PrintFormat(__FUNCTION__+": error initializing trailing"); myChannelExpert.Deinit(); return INIT_FAILED; } //--- параметры трала trailing.StopLevel(InpStopLevelPips); trailing.ProfitLevel(InpProfitLevelPips);
Поскольку мы будем использовать трал, то нужно модифицировать и главный метод CEquidChannelExpert::Processing() в файле EquidistantChannelExpert1.mqh.
//+------------------------------------------------------------------+ //| Главный модуль | //+------------------------------------------------------------------+ bool CEquidChannelExpert::Processing(void) { //--- расчёт направления m_signal.SetDirection(); //--- если позиции нет if(!this.SelectPosition()) { //--- модуль открытия позиции if(this.CheckOpen()) return true; } //--- если есть позиция else { //--- проверка возможности модификации позиции if(this.CheckTrailingStop()) return true; } //--- если нет торговых операций return false; }
Вот и всё. Трал добавлен. Файлы обновлённой стратегии расположены в отдельной подпапке ChannelsTrader1.
Попробуем проверить, сказывается ли на результативности наше нововведение.
Итак, на том же участке истории, при тех же параметрах, что и для базовой стратегии, в Тестере было сделано несколько прогонов в режиме оптимизации. Подгонялись параметры стоп-лосса и тейк-профита:
Переменная | Старт | Шаг | Стоп |
---|---|---|---|
Уровень для StopLoss, пп | 0 | 10 | 100 |
Уровень для TakeProfit, пп | 0 | 10 | 150 |
Результаты оптимизации находятся в файле ReportOptimizer-signal1.xml. Лучший проход представлен на Рис.5, где Уровень для StopLoss = 0, а для TakeProfit = 150.
Рис.5 Результаты стратегии с использованием трала за 2013-2015 гг.
Легко заметить, что последний рисунок напоминает Рис.3. Таким образом можно сказать, что в данном диапазоне значений использование трала не способствовало улучшению результата.
3.2 Тип канала
Есть предположение, что тип канала влияет на торговые результаты. Общая идея такая: продавать лучше в рамках нисходящего канала, а покупать — восходящего. Если канал ровный (не наклонный), то можно торговать от обеих границ.
Перечисление ENUM_CHANNEL_TYPE определяет тип канала:
//+------------------------------------------------------------------+ //| Тип канала | //+------------------------------------------------------------------+ enum ENUM_CHANNEL_TYPE { CHANNEL_TYPE_ASCENDING=0, // восходящий CHANNEL_TYPE_DESCENDING=1, // нисходящий CHANNEL_TYPE_FLAT=2, // ровный }; //+------------------------------------------------------------------+
В исходный файл советника ChannelsTrader2.mq5 в блок инициализации запишем, что задаём параметр допуска для поиска типа канала.
//--- параметры фильтра filter0.PointsInside(_Point*InpPipsInside); filter0.PointsOutside(_Point*InpPipsOutside); filter0.TypeTolerance(_Point*InpTypePips); filter0.PrevFracNum(InpPrevFracNum); ...
Этот параметр контролирует скорость изменения цены в пунктах. Допустим, что он равен 7 пп. Значит, если на каждом баре канал "растёт" на 6 пп, то он не дотягивает до того, чтобы считаться восходящим. Тогда просто будем считать его ровным (не наклонным).
В исходный файл сигнала SignalEquidChannel2.mqh в метод Direction() добавим поиск типа канала.
//--- если новый канал if(is_new_channel) { m_ch_type=CHANNEL_TYPE_FLAT; // ровный (не наклонный) канал //--- если указан допуск для типа if(m_ch_type_tol!=EMPTY_VALUE) { //--- Тип канала //--- скорость изменения double pr_speed_pnt=m_symbol.NormalizePrice(upper_vals[1]-upper_vals[0]); //--- если достаточная скорость if(MathAbs(pr_speed_pnt)>m_ch_type_tol) { if(pr_speed_pnt>0.) m_ch_type=CHANNEL_TYPE_ASCENDING; // восходящий канал else m_ch_type=CHANNEL_TYPE_DESCENDING; // нисходящий канал } } }
Изначально канал считается ровным — не растёт и не снижается. Если не задавалось значение параметра допуска для поиска типа канала, то до вычисления скорости изменения дело не дойдёт.
Условие на покупку будет включать проверку того, что канал не является нисходящим.
//+------------------------------------------------------------------+ //| Проверка условия на покупку | //+------------------------------------------------------------------+ int CSignalEquidChannel::LongCondition(void) { int result=0; //--- если задана минимальная цена if(m_base_low_price>0.) //--- если канал не нисходящий if(m_ch_type!=CHANNEL_TYPE_DESCENDING) //--- если минимальная цена на уровне нижней границы if((m_base_low_price<=m_lower_zone[0]) && (m_base_low_price>=m_lower_zone[1])) { if(IS_PATTERN_USAGE(0)) result=m_pattern_0; } //--- return result; } //+------------------------------------------------------------------+
Аналогичная проверка производится в условии на продажу, что канал не является восходящим.
Главный метод CEquidChannelExpert::Processing() в файле EquidistantChannelExpert2.mqh будет таким же, что и в базовой версии, т.к. трал исключаем.
Проверим результативность данного фактора. Параметр оптимизируем только один.
Переменная | Старт | Шаг | Стоп |
---|---|---|---|
Допуск для типа, пп | 0 | 5 | 150 |
Результаты оптимизации находятся в файле ReportOptimizer-signal2.xml. Лучший проход представлен на Рис.6.
Рис.6 Результаты стратегии с использованием типа канала за 2013-2015 гг.
Несложно заметить, что результаты стратегии немногим лучше, чем результаты базовой стратегии. Выходит, что при заданных базовых значениях параметров такой фактор как тип канала влияет на конечный результат.
3.3 Ширина канала
Мне кажется, что ширина канала может влиять на тип самой стратегии. Если канал получился узким, то при пробитии его границы (верхней или нижней) можно играть в сторону пробития, а не против. Тогда получим пробойную стратегию. Если канал получился широким, то можно торговать от его границ. Это и есть отбойная стратегия. Именно такой и является текущая стратегия — торговля идёт от границ канала.
Здесь, очевидно, нужен критерий для определения того, узок канал или широк. Чтобы не впадать в крайности, предлагаю добавить еще что-то среднее, чтобы считать исследуемый канал и не узким, и не широким. В итоге понадобятся 2 критерия:
- достаточная ширина узкого канала;
- достаточная ширина широкого канала.
Если канал окажется и не первым, и не вторым, то можно воздержаться от входа в рынок.
Рис.7 Ширина канала, схема
Сразу отмечу, что есть геометрическая проблема при определении ширины канала. Ведь на графике оси измеряются в разных величинах. Так, легко вычислить длину отрезков AB и CD. Но есть проблема с вычислением отрезка CE (Рис.7).
Я избрал, возможно, спорный и не самый точный способ нормализации, но зато он простой. Формула такая:
длина отрезка CE ≃ длина отрезка CD / (1.0 + скорость канала)
Ширину канала фиксируем с помощью перечисления ENUM_CHANNEL_WIDTH_TYPE:
//+------------------------------------------------------------------+ //| Ширина канала | //+------------------------------------------------------------------+ enum ENUM_CHANNEL_WIDTH_TYPE { CHANNEL_WIDTH_NARROW=0, // узкий CHANNEL_WIDTH_MID=1, // средний CHANNEL_WIDTH_BROAD=2, // широкий };
В исходный файл советника ChannelsTrader3.mq5 в группу пользовательских параметров "Каналы" добавим критерии ширины канала:
//--- sinput string Info_channels="+===-- Каналы --====+"; // +===-- Каналы --====+ input int InpPipsInside=100; // Внутренний допуск, пп input int InpPipsOutside=150; // Внешний допуск, пп input int InpNarrowPips=250; // Узкий канал, пп input int InpBroadPips=1200; // Широкий канал, пп ...
Если критерий узкого канала будет иметь значение, превышающее аналогичное значение широкого, то будет ошибка инициализации:
//--- параметры фильтра filter0.PointsInside(_Point*InpPipsInside); filter0.PointsOutside(_Point*InpPipsOutside); if(InpNarrowPips>=InpBroadPips) { PrintFormat(__FUNCTION__+": error specifying narrow and broad values"); return INIT_FAILED; } filter0.NarrowTolerance(_Point*InpNarrowPips); filter0.BroadTolerance(_Point*InpBroadPips);
В коде момент определения степени ширины канала представлен в теле метода Direction().
//--- Ширина канала m_ch_width=CHANNEL_WIDTH_MID; // средний double ch_width_pnt=((upper_vals[1]-lower_vals[1])/(1.0+pr_speed_pnt)); //--- если указан критерий узкого if(m_ch_narrow_tol!=EMPTY_VALUE) if(ch_width_pnt<=m_ch_narrow_tol) m_ch_width=CHANNEL_WIDTH_NARROW; // узкий //--- если указан критерий широкого if(m_ch_narrow_tol!=EMPTY_VALUE) if(ch_width_pnt>=m_ch_broad_tol) m_ch_width=CHANNEL_WIDTH_BROAD; // широкий
Изначально считаем по ширине канал средним. Потом проверяем, не является ли он узким или широким.
Также нам нужно изменить еще и методы определения торгового направления. Так, условие на покупку будет выглядеть теперь следующим образом:
//+------------------------------------------------------------------+ //| Проверка условия на покупку | //+------------------------------------------------------------------+ int CSignalEquidChannel::LongCondition(void) { int result=0; //--- если канал узкий - играем на пробой верхней границы if(m_ch_width==CHANNEL_WIDTH_NARROW) { //--- если задана максимальная цена if(m_base_high_price>0.) //--- если максимальная цена на уровне верхней границы if(m_base_high_price>=m_upper_zone[1]) { if(IS_PATTERN_USAGE(0)) result=m_pattern_0; } } //--- или если канал широкий - играем на отбой от нижней границы else if(m_ch_width==CHANNEL_WIDTH_BROAD) { //--- если задана минимальная цена if(m_base_low_price>0.) //--- если минимальная цена на уровне нижней границы if((m_base_low_price<=m_lower_zone[0]) && (m_base_low_price>=m_lower_zone[1])) { if(IS_PATTERN_USAGE(0)) result=m_pattern_0; } } //--- return result; } //+------------------------------------------------------------------+
Метод состоит из двух блоков. В первом проверяется возможность сыграть на пробой в рамках узкого канала. Отмечу, что в текущем варианте пробоем считается достижением ценой верха верхней буферной зоны. Во втором блоке мы уже ищем, не попала ли цена в нижнюю буферную зону, чтобы в игру вступила отбойная стратегия.
Метод проверки возможности продавать — ShortCondition() — создан по аналогии.
Главный метод CEquidChannelExpert::Processing() в файле EquidistantChannelExpert3.mqh не изменится.
Для оптимизации есть 2 параметра.
Переменная | Старт | Шаг | Стоп |
---|---|---|---|
Узкий канал, пп | 100 | 20 | 250 |
Широкий канал, пп | 350 | 50 | 1250 |
Результаты оптимизации находятся в файле ReportOptimizer-signal3.xml. Лучший проход представлен на Рис.8.
Рис.8 Результаты стратегии с учётом ширины канала за 2013-2015 гг.
Пожалуй, из всех описанных здесь факторов данный оказался наиболее влиятельным. Кривая баланса уже имеет более выраженное направление.
3.4 Приграничные уровни стоп-лосса и тейк-профита
Если есть изначально торговые цели, выраженные в значениях уровней стоп-лосса и тейк-профита, то стоит иметь возможность эти цели подстраивать под условия текущей стратегии. Проще говоря, раз есть канал, который в динамике прокладывает себе путь на графике под каким-то углом, то и мы должны будем перемещать свои уровни стоп-лосса и тейк-профита в привязке к границам канала.
Я добавил ещё пару моделей для удобства. Теперь они выглядят так:
//--- "веса" рыночных моделей (0-100) int m_pattern_0; // Модель "отбой от границы канала" int m_pattern_1; // Модель "пробой границы канала" int m_pattern_2; // Модель "новый канал"
В предыдущих версиях была только одна, и она отвечала за касание ценой любой границы канала. Теперь будем различать отбойную и пробойную модель. Появилась и третья модель — модель нового канала. Она нужна для случаев, когда есть новый канал, и есть позиция, открытая на прошлом канале. Если модель сработала, то будем эту позицию закрывать.
Условие на покупку выглядит следующим образом:
//+------------------------------------------------------------------+ //| Проверка условия на покупку | //+------------------------------------------------------------------+ int CSignalEquidChannel::LongCondition(void) { int result=0; bool is_position=PositionSelect(m_symbol.Name()); //--- если канал узкий - играем на пробой верхней границы if(m_ch_width_type==CHANNEL_WIDTH_NARROW) { //--- если задана максимальная цена if(m_base_high_price>0.) //--- если максимальная цена на уровне верхней границы if(m_base_high_price>=m_upper_zone[1]) { if(IS_PATTERN_USAGE(1)) { result=m_pattern_1; //--- если позиции нет if(!is_position) //--- в Журнал if(m_to_log) { Print("\nСработала Модель \"пробой границы канала\" на покупку."); PrintFormat("High-цена: %0."+IntegerToString(m_symbol.Digits())+"f",m_base_high_price); PrintFormat("Триггер-цена: %0."+IntegerToString(m_symbol.Digits())+"f",m_upper_zone[1]); } } } } //--- или если канал широкий или средний - играем на отбой от нижней границы else { //--- если задана минимальная цена if(m_base_low_price>0.) //--- если минимальная цена на уровне нижней границы if((m_base_low_price<=m_lower_zone[0]) && (m_base_low_price>=m_lower_zone[1])) { if(IS_PATTERN_USAGE(0)) { result=m_pattern_0; //--- если позиции нет if(!is_position) //--- в Журнал if(m_to_log) { Print("\nСработала Модель \"отбой от границы канала\" на покупку."); PrintFormat("Low-цена: %0."+IntegerToString(m_symbol.Digits())+"f",m_base_low_price); PrintFormat("Зона вверх: %0."+IntegerToString(m_symbol.Digits())+"f",m_upper_zone[0]); PrintFormat("Зона низ: %0."+IntegerToString(m_symbol.Digits())+"f",m_upper_zone[1]); } } } } //--- return result; } //+------------------------------------------------------------------+
Также появилась проверка условия на закрытие:
//+------------------------------------------------------------------+ //| Проверка условия закрытия покупки | //+------------------------------------------------------------------+ bool CSignalEquidChannel::CheckCloseLong(double &price) const { bool to_close_long=true; int result=0; if(IS_PATTERN_USAGE(2)) result=m_pattern_2; if(result>=m_threshold_close) { if(m_is_new_channel) //--- если закрывать покупку if(to_close_long) { price=NormalizeDouble(m_symbol.Bid(),m_symbol.Digits()); //--- в Журнал if(m_to_log) { Print("\nСработала Модель \"новый канал\" для закрытия покупки."); PrintFormat("Цена закрытия: %0."+IntegerToString(m_symbol.Digits())+"f",price); } } } //--- return to_close_long; } //+------------------------------------------------------------------+Для короткой позиции условие на закрытие будет идентичным.
Теперь несколько слов о трале. Для него был написан свой класс CTrailingEquidChannel, родителем для которого стал стандартный класс CExpertTrailing.
//+------------------------------------------------------------------+ //| Class CTrailingEquidChannel. | //| Purpose: Class of trailing stops based on Equidistant Channel. | //| Derives from class CExpertTrailing. | //+------------------------------------------------------------------+ class CTrailingEquidChannel : public CExpertTrailing { protected: double m_sl_distance; // расстояние для стопа double m_tp_distance; // расстояние для профита double m_upper_val; // верхняя граница double m_lower_val; // нижняя граница ENUM_CHANNEL_WIDTH_TYPE m_ch_wid_type; // тип канала по ширине //--- public: void CTrailingEquidChannel(void); void ~CTrailingEquidChannel(void){}; //--- methods of initialization of protected data void SetTradeLevels(double _sl_distance,double _tp_distance); //--- virtual bool CheckTrailingStopLong(CPositionInfo *position,double &sl,double &tp); virtual bool CheckTrailingStopShort(CPositionInfo *position,double &sl,double &tp); //--- bool RefreshData(const CSignalEquidChannel *_ptr_ch_signal); }; //+------------------------------------------------------------------+
Красным цветом выделен метод, который получает информацию от канального сигнала.
Методы проверки возможности трала короткой и длинной позиций предка были переопределены посредством полиморфизма - базового принципа ООП .
Для того, чтобы класс трала мог получать временные и ценовые ориентиры актуального канала, потребовалось создать связку с сигнальным классом CSignalEquidChannel. Её реализовал константный указатель в составе класса CEquidChannelExpert. Такой подход позволяет получать всю необходимую информацию от сигнала, не подвергая угрозе изменения значений параметров самого сигнала.
//+------------------------------------------------------------------+ //| Класс CEquidChannelExpert. | //| Цель: Класс для советника, торгующего по равноудалённому каналу. | //| Потомок класса CExpert. | //+------------------------------------------------------------------+ class CEquidChannelExpert : public CExpert { //--- === Data members === --- private: const CSignalEquidChannel *m_ptr_ch_signal; //--- === Methods === --- public: //--- constructor/destructor void CEquidChannelExpert(void); void ~CEquidChannelExpert(void); //--- указатель на объект сигнала каналов void EquidChannelSignal(const CSignalEquidChannel *_ptr_ch_signal){m_ptr_ch_signal=_ptr_ch_signal;}; const CSignalEquidChannel *EquidChannelSignal(void) const {return m_ptr_ch_signal;}; protected: virtual bool Processing(void); //--- trade close positions check virtual bool CheckClose(void); virtual bool CheckCloseLong(void); virtual bool CheckCloseShort(void); //--- trailing stop check virtual bool CheckTrailingStop(void); virtual bool CheckTrailingStopLong(void); virtual bool CheckTrailingStopShort(void); }; //+------------------------------------------------------------------+
В классе советника были также переопределены методы, отвечающие за закрытие и трал.
Главный метод CEquidChannelExpert::Processing() в файле EquidistantChannelExpert4.mqh выглядит так:
//+------------------------------------------------------------------+ //| Главный модуль | //+------------------------------------------------------------------+ bool CEquidChannelExpert::Processing(void) { //--- расчёт направления m_signal.SetDirection(); //--- если позиции нет if(!this.SelectPosition()) { //--- модуль открытия позиции if(this.CheckOpen()) return true; } //--- если есть позиция else { if(!this.CheckClose()) { //--- проверка возможности модификации позиции if(this.CheckTrailingStop()) return true; //--- return false; } else { return true; } } //--- если нет торговых операций return false; } //+------------------------------------------------------------------+Будем оптимизировать такие параметры:
Переменная | Старт | Шаг | Стоп |
---|---|---|---|
Стоп-лосс, поинт | 25 | 5 | 75 |
Тэйк-профит, поинт | 50 | 5 | 200 |
Результаты оптимизации находятся в файле ReportOptimizer-signal4.xml. Лучший проход представлен на Рис.9.
Рис.9 Результаты стратегии с учётом приграничных уровней за 2013-2015 гг.
Очевидно, что данный фактор — приграничные ценовые уровни — не способствовали улучшению результативности.
Заключение
В этой статье я постарался представить процесс разработки и реализации класса-сигнальщика на основе скользящих каналов. За каждой из версий сигнала следовала торговая стратегия с результатами тестирования.
Подчеркну, что везде использовались фиксированные значения настроек равноудалённого канала. Поэтому выводы о том, насколько тот или иной фактор был результативен, справедливы только для указанных значений.
Остаются и другие возможности улучшения результативных показателей. В рамках данной статьи была представлена часть работы по поиску таковых.
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
...на видео строятся каналы в виде двух отрезков. А почему Вы не делаете следующее
А в чём цимес?
Видеть канал на протяжении всей истории.
Чтобы прошлые каналы не пропадали при появлении нового?
Чтобы видеть на истории, где бы по краям канала располагались отложенники, в случае его торговли.