English 中文 Español Deutsch 日本語 Português
Мини-эмулятор рынка, или Ручной тестер стратегий

Мини-эмулятор рынка, или Ручной тестер стратегий

MetaTrader 5Тестер | 19 октября 2017, 09:00
9 633 10
Dmitriy Zabudskiy
Dmitriy Zabudskiy

Введение

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

Есть несколько сценариев поведения начинающего трейдера.

  • Вариант "Всё и сразу": большая часть новичков хотят заработать много и быстро. Они поддаются на заманчивую рекламу волшебной безотказной стратегии, воспользоваться которой можно за совсем небольшие деньги или даже бесплатно. Выглядит это всё быстро и просто — но и депозит тоже сливается быстро и просто.
  • Вариант "Учиться, учиться и еще раз учиться": есть новички, которые подходят к обучению основательно, без веры в сказки. Они подробно изучают законы рынка и торговые системы. И вот уже начинается торговля на реальном счете — но профита все равно оказывается гораздо меньше, чем ожидалось по учебникам. Как же это происходит и что делать дальше?

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

Данная статья в основном предназначена для новичков, которым не терпится поторговать на демо-счете и протестировать свою стратегию. И здесь тоже есть два варианта:

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

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

В этой статье я хочу рассказать о собственном варианте реализации подобной системы в терминале MetaTrader 5. Я написал индикатор "Мини-эмулятор рынка" с урезанным функционалом по сравнению с полной версией терминала. Он предназначен для теоретической проверки стратегий.

Функционал приложения

У приложения есть как собственная панель управления, так и некоторые кнопки "материнской системы", то есть самого терминала MetaTrader 5.

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

  1. Можно выставить только два разнонаправленных ордера: buy и sell. Также предусмотрена установка стоп-лосса и тейк-профита до выставления ордера и его объема. После выставления ордер можно изменять, перетаскивать стоп-уровни.
  2. Скоростей построения всего семь, их условно можно поделить на три группы. Первая — "ювелирная", подразумевает построение на основе генерации тиков из данных минутного таймфрейма, почти как в Тестере стратегий. Вторая, учитывая минутные данные, строит без генерации (такой режим быстрее, но менее точен). Третий режим самый быстрый: вне зависимости от таймфрейма, строится одна свеча в секунду.
  3. Приводится информация о текущей торговле: прибыль, количество пунктов и объем. Данные приводятся по текущему и прошлому ордерам и общей торговле с начала эмуляции.
  4. Доступны все стандартные графические объекты, которые есть в терминале.
  5. Доступны все стандартные таймфреймы (переключаются кнопками панели терминала).

Рис.1. Органы управления и внешний вид приложения

Рис.1. Органы управления и внешний вид приложения


Система генерации тиков

Принцип генерации тиков был взят из статьи "Алгоритм генерации тиков в тестере стратегий терминала MetaTrader 5". Я творчески переработал его и выдал авторское исполнение.

За генерацию тиков отвечают две функции — главная и вспомогательная.

Главная функция — Tick Generation. В нее передаются два параметра: сама свеча и массив для ответных данных (тиков). Затем, если все четыре цены свечи равны друг другу на входе, то объем тиков приравнивается к одному тику. Это было сделано, чтобы устранить возможность ошибки деления на ноль в случае передачи неправильных данных.

Следом происходит формирование новой свечи. Если в пределах свечи 1 — 3 тика, то процесс генерации продолжается согласно описанию в вышеуказанной статье.

Если тиков больше 3, работа усложняется. Переданная свеча делится на три неравных части (принцип деления см. в коде ниже, отдельно для медвежьей или бычьей свечи). Затем в случае, если в верхней и нижней части не оказалось тиков, производится корректировка. Далее, в зависимости от характера свечи, передаём управление вспомогательной функции.

//+------------------------------------------------------------------+
//| Func Tick Generation                                             |
//+------------------------------------------------------------------+
void func_tick_generation(
MqlRates &rates,      // данные о свече
double &tick[]        // динамический массив тиков
)
{
 if(rates.open==rates.close && rates.high==rates.low && rates.open==rates.high){rates.tick_volume=1;}
 if(rates.tick_volume<4)// тиков меньше четырёх
 {
ArrayResize(tick,int(rates.tick_volume));         // изменяем размер массива на количество тиков
if(rates.tick_volume==1)tick[0]=rates.close;      // один тик
if(rates.tick_volume==2)                          // два тика
{
 tick[0]=rates.open;
 tick[1]=rates.close;
}
if(rates.tick_volume==3)                          // три тика
{
 tick[0]=rates.open;
 tick[2]=rates.close;
 if(rates.open==rates.close)                      // сходили в одну сторону и вернулись на уровень Open
 {
if(rates.high==rates.open)tick[1]=rates.low;
if(rates.low==rates.open)tick[1]=rates.high;
 }
 if(rates.close==rates.low && rates.open!=rates.high)tick[1]=rates.high;           // cходили в одну сторону, откатились и пробили уровень Open
 if(rates.close==rates.high && rates.open!=rates.low)tick[1]=rates.low;
 if(rates.open==rates.high && rates.close!=rates.low)tick[1]=rates.low;            // сходили в одну сторону, откатились, но не дошли до уровня Open
 if(rates.open==rates.low && rates.close!=rates.high)tick[1]=rates.high;
 if((rates.open==rates.low && rates.close==rates.high) || (rates.open==rates.high && rates.close==rates.low))
 {
tick[1]=NormalizeDouble((((rates.high-rates.low)/2)+rates.low),_Digits);       // несколько пунктов в одну сторону
 }
}
 }
 if(rates.tick_volume>3)      // больше трёх тиков
 {

 // рассчитываем размер свечи по пунктам
int candle_up=0;
int candle_down=0;
int candle_centre=0;
if(rates.open>rates.close)
{
 candle_up=int(MathRound((rates.high-rates.open)/_Point));
 candle_down=int(MathRound((rates.close-rates.low)/_Point));
}
if(rates.open<=rates.close)
{
 candle_up=int(MathRound((rates.high-rates.close)/_Point));
 candle_down=int(MathRound((rates.open-rates.low)/_Point));
}
candle_centre=int(MathRound((rates.high-rates.low)/_Point));
int candle_all=candle_up+candle_down+candle_centre;      // общая длина движения
int point_max=int(MathRound(double(candle_all)/2));      // максимально возможное количество тиков
double share_up=double(candle_up)/double(candle_all);
double share_down=double(candle_down)/double(candle_all);
double share_centre=double(candle_centre)/double(candle_all);

// вычисляем количество опорных точек на каждом участке
char point=0;
if(rates.tick_volume<10)point=char(rates.tick_volume);
else point=10;
if(point>point_max)point=char(point_max);
char point_up=char(MathRound(point*share_up));
char point_down=char(MathRound(point*share_down));
char point_centre=char(MathRound(point*share_centre));

// проверяем наличие опорных точек на выбранных диапазонах
if(candle_up>0 && point_up==0)
{point_up=1;point_centre=point_centre-1;}
if(candle_down>0 && point_down==0)
{point_down=1;point_centre=point_centre-1;}

// меняем размер выходного массива
ArrayResize(tick,11);
char p=0;                     // индекс массива тиков (tick[])
tick[p]=rates.open;           // первый тик равен цене открытия
if(rates.open>rates.close)    // нисходящая
{
 func_tick_small(rates.high,1,candle_up,point_up,tick,p);
 func_tick_small(rates.low,-1,candle_centre,point_centre,tick,p);
 func_tick_small(rates.close,1,candle_down,point_down,tick,p);
 ArrayResize(tick,p+1);
}
if(rates.open<=rates.close)   // восходящая или доджи
{
 func_tick_small(rates.low,-1,candle_down,point_down,tick,p);
 func_tick_small(rates.high,1,candle_centre,point_centre,tick,p);
 func_tick_small(rates.close,-1,candle_up,point_up,tick,p);
 ArrayResize(tick,p+1);
}
 }
}

Вспомогательная функция Tick Small, как видно из ее названия, производит незначительную генерацию тиков. Она получает информацию о том, на каком тике мы остановились, в какую сторону идти (вверх или низ), сколько должно быть шагов, какая цена была последней — и передаёт рассчитанные шаги в вышеупомянутый массив тиков. В итоге получаем массив, в котором содержится не более 11 тиков.

//+------------------------------------------------------------------+
//| Func Tick Small                                                  |
//+------------------------------------------------------------------+
void func_tick_small(
 double end,        // конец движения
 char route,        // направление движения
 int candle,        // расстояние движения
 char point,        // количество точек
 double &tick[],    // массив тиков
 char&i           // текущий индекс массива
 )
{
 if(point==1)
 {
i++;
if(i>10)i=10;       // корректировка
tick[i]=end;
 }
 if(point>1)
 {
double wave_v=(point+1)/2;
double step_v=(candle-1)/MathFloor(wave_v)+1;
step_v=MathFloor(step_v);
for(char p_v=i+1,i_v=i; p_v<i_v+point;)
{
 i++;
 if(route==1)tick[i]=tick[i-1]+(step_v*_Point);
 if(route==-1)tick[i]=tick[i-1]-(step_v*_Point);
 p_v++;
 if(p_v<i_v+point)
 {
i++;
if(route==1)tick[i]=tick[i-1]-_Point;
if(route==-1) tick[i]=tick[i-1]+_Point;
 }
 p_v++;
}
if(NormalizeDouble(tick[i],_Digits)!=NormalizeDouble(end,_Digits))
{
 i++;
 if(i>10)i=10;    // корректировка
 tick[i]=end;
}
 }
}

Это, так сказать, сердце всего "ювелирного" построения (почему я назвал его "ювелирным", мы узнаем в заключении). Теперь перейдём к сути взаимодействия системы.

Взаимодействие и обмен данными

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

Рис. 2. Схема взаимодействий в приложении

Рис. 2. Схема взаимодействий в приложении

Управление изменением периодов было заимствовано из оболочки терминала, чтобы уменьшить количество объектов для управления в окне индикатора. Но поскольку при переключении периода приложение перезагружается и все локальные и глобальные переменные переписываются, массив данных каждый раз копируется после переключения. В частности, копируются данные двух периодов — М1 и выбранного. Параметры последующей обработки этих данных выбираются на панели: это скорость и качество построения ("ювелирное" или простое быстрое). После того, как всё готово, начинается построение графика.

С панели управления можно выставлять ордера и удалять их. Для этого программа обращается к классу "COrder". С помощью этого класса происходит и управление ордерами в ходе построения графика.

Как было упомянуто выше, если меняется период графика, индикатор перезапускается. Соответственно, для связи всей конструкции приложения используются глобальные переменные клиентского терминала. От обычных глобальных переменных они отличаются длительным (4 недели) хранением и независимостью от перезагрузки. Единственный недостаток — это тип данных, который может быть только double. Но в целом это гораздо удобнее, чем каждый раз создавать отдельный файл, записывать и читать с него.

Перейдём непосредственно к коду частей взаимодействия.

Реализация в коде

Начало кода

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

Здесь же копируем массивы данных и считываем основную связующую переменную (с именем time_end) — это время, на котором остановилось построение:

//--- устанавливаем значение времени, до которого отрисовывался индикатор
 if(GlobalVariableCheck(time_end))end_time_indicator=datetime(GlobalVariableGet(time_end));

Таким образом, индикатор всегда "знает", где он остановился. Функция OnInit() заканчивается на вызове таймера, который, собственно, и даёт команду на выход нового тика или формирование целой свечи (в зависимости от скорости).

Функция таймера

В начале функции проверяется значение кнопки "play" на панели управления. Если она нажата, то выполняется дальнейший код.

Сначала определяется бар индикатора, на котором остановилось построение (относительно текущего времени). За конечные точки берутся последнее время построения end_time_indicator и текущее время. Данные пересчитываются ежесекундно, так как график постоянно движется, (за исключением субботы и воскресенья), причем несинхронизированно по времени. Таким образом происходит динамическое отслеживание и передвижение графика функцией ChartNavigate().

Затем рассчитываются переменные number_now_rates, bars_now_rates, all_bars_indicator. Далее проверяется время. Если по входящим параметрам индикатора оно не подошло к концу, производится построение с помощью функции func_merger(). Далее проверяются текущие позиции и их прибыльность, с занесением значений в глобальные переменные и выводом в информационный блок индикатора.

Также здесь происходит обращение к классу "COrder", а именно — к частям, отвечающим за автоматическое удаление ордера в результате действий пользователя (position.Delete) или срабатывания стоп-уровней (position.Check).

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
{
//---
 if(button_play)
 {
end_bar_indicator=Bars(_Symbol,_Period,end_time_indicator,TimeCurrent());      // количество баров от самого раннего до текущего
ChartNavigate(0,CHART_END,-end_bar_indicator);                                 // перевод графика (индикатора) на текущее построение
number_now_rates=(Bars(_Symbol,_Period,real_start,end_time_indicator)-1);      // текущий используемый бар для построения
bars_now_rates=(Bars(_Symbol,_Period,real_start,stop)-1);                      // количество баров, используемых из истории по текущему периоду
all_bars_indicator=(Bars(_Symbol,_Period,real_start,TimeCurrent()))-1;         // количество баров с начала построения по текущее время

if(end_time_indicator<stop)                                                    // проверка времени построения
{
 func_merger();
 ObjectSetDouble(0,line_bid,OBJPROP_PRICE,price_bid_now);
 if(ObjectFind(0,line_ask)>=0)
 {ObjectSetDouble(0,line_ask,OBJPROP_PRICE,price_ask_now);}

 //--- текущие подсчёты по ордерам
 int point_now=0;
 double vol_now=0;
 double money_now=0;
 if(ObjectFind(0,order_buy)>=0 && GlobalVariableGet(order_buy)>0)             // ордер на покупку есть
 {
int p_now=int((price_bid_now-GlobalVariableGet(order_buy))*dig_pow);
double v_now=GlobalVariableGet(vol_buy);
double m_now=p_now*v_now*10;
point_now+=p_now;
vol_now+=v_now;
money_now+=m_now;
 }
 if(ObjectFind(0,order_sell)>=0 && GlobalVariableGet(order_sell)>0)           // ордер на продажу есть
 {
int p_now=int((GlobalVariableGet(order_sell)-price_ask_now)*dig_pow);
double v_now=GlobalVariableGet(vol_sell);
double m_now=p_now*v_now*10;
point_now+=p_now;
vol_now+=v_now;
money_now+=m_now;
 }
 GlobalVariableSet(info_point_now,point_now);
 GlobalVariableSet(info_vol_now,vol_now);
 GlobalVariableSet(info_money_now,money_now);
}

COrder position;    //объект класса "COrder"
position.Delete(price_bid_now,price_ask_now,(-1));
position.Check(end_time_indicator,GlobalVariableGet(order_buy),GlobalVariableGet(tp_buy),GlobalVariableGet(sl_buy),
 GlobalVariableGet(order_sell),GlobalVariableGet(tp_sell),GlobalVariableGet(sl_sell));

func_info_print("Money All: ",info_money_all,2);
func_info_print("Money Last: ",info_money_last,2);
func_info_print("Money Now: ",info_money_now,2);
func_info_print("Volume All: ",info_vol_all,2);
func_info_print("Volume Last: ",info_vol_last,2);
func_info_print("Volume Now: ",info_vol_now,2);
func_info_print("Point All: ",info_point_all,0);
func_info_print("Point Last: ",info_point_last,0);
func_info_print("Point Now: ",info_point_now,0);

position.Modify();
 }
//--- управление кнопкой Hide
 char x=char(GlobalVariableGet("hide"));
 if(x==1)
 {
ObjectSetInteger(0,"20",OBJPROP_STATE,false);
ObjectSetInteger(0,"14",OBJPROP_YDISTANCE,24);
ObjectSetInteger(0,"15",OBJPROP_YDISTANCE,24);
ObjectSetInteger(0,"16",OBJPROP_YDISTANCE,24);
ObjectSetInteger(0,"17",OBJPROP_YDISTANCE,24);
ObjectSetInteger(0,"18",OBJPROP_YDISTANCE,24);
ObjectSetInteger(0,"19",OBJPROP_YDISTANCE,24);
 }
 if(x==2)
 {
ObjectSetInteger(0,"20",OBJPROP_STATE,true);
ObjectSetInteger(0,"14",OBJPROP_YDISTANCE,-24);
ObjectSetInteger(0,"15",OBJPROP_YDISTANCE,-24);
ObjectSetInteger(0,"16",OBJPROP_YDISTANCE,-24);
ObjectSetInteger(0,"17",OBJPROP_YDISTANCE,-24);
ObjectSetInteger(0,"18",OBJPROP_YDISTANCE,-24);
ObjectSetInteger(0,"19",OBJPROP_YDISTANCE,-24);
 }
}

Класс COrder

В этом классе находятся функции открытия и закрытия позиции, изменения и проверки текущего состояния ордеров (контроля их уровней тейк-профита и стоп-лосса).

Начнём с выставления ордера Placed. Посредством оператора-переключателя switch выбираем вид ордера (buy или sell), заносим данные в глобальную переменную (order_buy или order_sell). Если ранее обозначены m_take_profit и m_stop_los, заносим их в соответствующие глобальные переменные и отрисовываем на графике линии. Линии устанавливаются функцией Line данного класса.

//+------------------------------------------------------------------+
//| Class COrder                                                     |
//+------------------------------------------------------------------+
class COrder
{
public:
 void Placed(
 char m_type,//тип ордера (1-buy, 2-sell)
 double m_price_bid, //цена bid
 double m_price_ask, //цена ask
 int m_take_profit,//пунктов до take profit
 int m_stop_loss //пунктов до stop loss
 )
 {
switch(m_type)
{
 case 1:
 {
GlobalVariableSet(order_buy,m_price_ask);
Line(GlobalVariableGet(order_buy),order_buy,col_buy,STYLE_SOLID,1,true);
if(m_take_profit>0)
{
 GlobalVariableSet(tp_buy,(m_price_ask+(_Point*m_take_profit)));
 Line(GlobalVariableGet(tp_buy),tp_buy,col_tp,STYLE_DASH,1,true);
}
if(m_stop_loss>0)
{
 GlobalVariableSet(sl_buy,(m_price_ask-(_Point*m_stop_loss)));
 Line(GlobalVariableGet(sl_buy),sl_buy,col_sl,STYLE_DASH,1,true);
}
 }
 break;
 case 2:
 {
GlobalVariableSet(order_sell,m_price_bid);
Line(GlobalVariableGet(order_sell),order_sell,col_sell,STYLE_SOLID,1,true);
if(m_take_profit>0)
{
 GlobalVariableSet(tp_sell,(m_price_bid-(_Point*m_take_profit)));
 Line(GlobalVariableGet(tp_sell),tp_sell,col_tp,STYLE_DASH,1,true);
}
if(m_stop_loss>0)
{
 GlobalVariableSet(sl_sell,(m_price_bid+(_Point*m_stop_loss)));
 Line(GlobalVariableGet(sl_sell),sl_sell,col_sl,STYLE_DASH,1,true);
}
 }
 break;
}
 }

Следом идет функция удаления ордера Delete. Снова оператор-переключатель выбирает из трёх вариантов — автоматическое удаление, buy или sell. Автоматическое удаление в данном случае — ситуация, когда ордер удаляется с помощью удаления с графика его линии.

За это отвечают вспомогательные функциии класса Small_del_buy и Small_del_sell.

 void Delete(
 double m_price_bid,      //цена bid
 double m_price_ask,      //цена ask
 char m_del_manual        //вид удаления (-1 авто, 1 buy, 2 sell)
 )
 {
switch(m_del_manual)
{
 case(-1):
if(ObjectFind(0,order_buy)<0 && GlobalVariableGet(order_buy)>0)
{Small_del_buy(m_price_bid);}
if(ObjectFind(0,order_sell)<0 && GlobalVariableGet(order_sell)>0)
{Small_del_sell(m_price_ask);}
break;
 case 1:
if(ObjectFind(0,order_buy)>=0)
{
 ObjectDelete(0,order_buy);
 Small_del_buy(m_price_bid);
}
break;
 case 2:
if(ObjectFind(0,order_sell)>=0)
{
 ObjectDelete(0,order_sell);
 Small_del_sell(m_price_ask);
}
break;
}
 }

Разберём одну из них — Small_del_sell.

Проверяем наличие линий тейк-профита и стоп-лосса. Если они есть, то удаляем их. Затем обнуляем глобальную переменную order_sell — это понадобится потом, если мы будем проверять по глобальным переменным наличие ордеров.

Также заносим в глобальные переменные информацию о полученной по ордеру прибыли (info_point_last, info_vol_last, info_money_last). За это отвечает функция small_concatenation (представляет собой что-то вроде оператора += в выражении, только с глобальными переменными). Суммируем прибыль (объём) и тоже заносим в глобальные переменные (info_point_all, info_vol_all, info_money_all).

void Small_del_sell(double m_price_ask)
 {
if(ObjectFind(0,tp_sell)>=0)ObjectDelete(0,tp_sell);       // удаляем линию take profit 
 if(ObjectFind(0,sl_sell)>=0)ObjectDelete(0,sl_sell);      // удаляем линию stop loss 
 int point_plus=int(MathRound((GlobalVariableGet(order_sell)-m_price_ask)/_Point));      // подсчитываем прибыль со сделки
GlobalVariableSet(order_sell,0);                           //обнуляем переменную цены выставленного ордера
GlobalVariableSet(info_vol_last,GlobalVariableGet(vol_sell));
GlobalVariableSet(vol_sell,0);
GlobalVariableSet(info_point_last,point_plus);
GlobalVariableSet(info_money_last,(GlobalVariableGet(info_point_last)*GlobalVariableGet(info_vol_last)*10));
Small_concatenation(info_point_all,info_point_last);
Small_concatenation(info_vol_all,info_vol_last);
Small_concatenation(info_money_all,info_money_last);
 }

Модификация ордера — это смена положения его линии при помощи мыши. Для этого есть два способа. Первый — попытка перетаскивания линии открытия ордера. В этом случае строятся новые линии тейк-профита и стоп-лосса, в зависимости от направления перемещения и вида ордера. Данная функция Small_mod также реализуется в классе COrder. Входными параметрами для нее служат название объекта, разрешение на перемещение объекта и вид ордера.

В начале функции Small_mod проверяется наличие объекта. Затем, если перемещение линий тейк-профита/стоп-лосса разрешено, изменение цены заносится в глобальную переменную. Если перемещение запрещено (линии buy и sell), тогда, в зависимости от вида ордера, на новом месте расположения линии появляется линия тейк-профита или стоп-лосса, а линия ордера возвращается на место.

 void Small_mod(string m_name,      // имя объекта и глобальной переменной
bool m_mode,                        // разрешение на изменение положения
char m_type                         // 1 — buy, 2 — sell
)
 {
if(ObjectFind(0,m_name)>=0)
{
 double price_obj_double=ObjectGetDouble(0,m_name,OBJPROP_PRICE);
 int price_obj=int(price_obj_double*dig_pow);
 double price_glo_double=GlobalVariableGet(m_name);
 int price_glo=int(price_glo_double*dig_pow);
 if(price_obj!=price_glo && m_mode==true)
 {
GlobalVariableSet(m_name,(double(price_obj)/double(dig_pow)));
 }
 if(price_obj!=price_glo && m_mode==false)
 {
switch(m_type)
{
 case 1:                         // order buy
if(price_obj>price_glo)          //TP
{
 GlobalVariableSet(tp_buy,(double(price_obj)/double(dig_pow)));
 Line(GlobalVariableGet(tp_buy),tp_buy,col_tp,STYLE_DASH,1,true);
}
if(price_obj<price_glo)          //SL
{
 GlobalVariableSet(sl_buy,(double(price_obj)/double(dig_pow)));
 Line(GlobalVariableGet(sl_buy),sl_buy,col_sl,STYLE_DASH,1,true);
}
break;
 case 2:                        // order sell
if(price_obj>price_glo)         //SL
{
 GlobalVariableSet(sl_sell,(double(price_obj)/double(dig_pow)));
 Line(GlobalVariableGet(sl_sell),sl_sell,col_sl,STYLE_DASH,1,true);
}
if(price_obj<price_glo)         //TP
{
 GlobalVariableSet(tp_sell,(double(price_obj)/double(dig_pow)));
 Line(GlobalVariableGet(tp_sell),tp_sell,col_tp,STYLE_DASH,1,true);
}
break;
}
ObjectSetDouble(0,m_name,OBJPROP_PRICE,(double(price_glo)/double(dig_pow)));
 }
}
 }

В ходе построения графика ордера постоянно проверяются функцией Check класса COrder. В функцию передаются все глобальные переменные, хранящие информацию о ордерах. Есть там и собственная глобальная переменная, содержащая информацию о времени последнего обращения. Это позволяет каждый раз при вызове проверять весь ценовой промежуток (минутного таймфрейма) по времени между последним вызовом функции и текущем временем прорисовки графика.

В случае, если за это время цена дошла до одной из стоп-линий или пробила ее, управление передаётся функции по удалению ордеров (класс COrder, функция Delete).

 void Check(
datetime m_time,
double m_price_buy,
double m_price_tp_buy,
double m_price_sl_buy,
double m_price_sell,
double m_price_tp_sell,
double m_price_sl_sell
)
 {
int start_of_z=0;
int end_of_z=0;
datetime time_end_check=datetime(GlobalVariableGet(time_end_order_check));
if(time_end_check<=0){time_end_check=m_time;}
GlobalVariableSet(time_end_order_check,m_time);
start_of_z=Bars(_Symbol,PERIOD_M1,real_start,time_end_check);
end_of_z=Bars(_Symbol,PERIOD_M1,real_start,m_time);
for(int z=start_of_z; z<end_of_z; z++)
{
 COrder del;
 double p_bid_high=period_m1[z].high;
 double p_bid_low=period_m1[z].low;
 double p_ask_high=p_bid_high+(spread*_Point);
 double p_ask_low=p_bid_low+(spread*_Point);
 if(m_price_buy>0)                                              // есть ордер BUY
 {
if(ObjectFind(0,tp_buy)>=0)
{
 if(m_price_tp_buy<=p_bid_high && m_price_tp_buy>=p_bid_low)    // сработал TP
 {del.Delete(m_price_tp_buy,0,1);}                              // закрываем по цене TP
} 
if(ObjectFind(0,sl_buy)>=0)
{
 if(m_price_sl_buy>=p_bid_low && m_price_sl_buy<=p_bid_high)    // сработал SL
 {del.Delete(m_price_sl_buy,0,1);}                              // закрываем по цене SL
}
 }
 if(m_price_sell>0)                                                   // есть ордер SELL
 {
if(ObjectFind(0,tp_sell)>=0)
{
 if(m_price_sl_sell<=p_ask_high && m_price_sl_sell>=p_ask_low)  // сработал SL
 {del.Delete(0,m_price_sl_sell,2);}                             // закрываем по цене SL
}
if(ObjectFind(0,sl_sell)>=0)
{
 if(m_price_tp_sell>=p_ask_low && m_price_tp_sell<=p_ask_high)  // сработал TP
 {del.Delete(0,m_price_tp_sell,2);}                             // закрываем по цене TP
}
 }
}
 }

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

Функция заполнения func_filling()

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

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

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

//+------------------------------------------------------------------+
//| Func Filling                                                     |
//+------------------------------------------------------------------+
void func_filling(MqlRates &input_rates[],                // входящие данные (текущего периода) для заполнения
datetime input_end_time_indicator,      // текущее время индикатора
int input_all_bars_indicator,           // количество всех баров индикатора
datetime &output_time_end_filling,      // время открытия последнего бара
datetime &output_time_next_filling,     // время открытия следующего бара
int input_end_bar_indicator,            // текущий (рисуемый) бар индикатора
double &output_o[],
double &output_h[],
double &output_l[],
double &output_c[],
double &output_col[],
char &work_status)                      // статус работы
{
 if(work_status==1)
 {
int stopped_rates_bar;
for(int x=input_all_bars_indicator,y=0;x>0;x--,y++)
{
 if(input_rates[y].time<input_end_time_indicator)
 {
output_o[x]=input_rates[y].open;
output_h[x]=input_rates[y].high;
output_l[x]=input_rates[y].low;
output_c[x]=input_rates[y].close;
if(output_o[x]>output_c[x])output_col[x]=0;
else output_col[x]=1;
output_time_end_filling=input_rates[y].time;
output_time_next_filling=input_rates[y+1].time;
input_end_bar_indicator=x;
stopped_rates_bar=y;
 }
 else break;
}
output_o[input_end_bar_indicator]=input_rates[stopped_rates_bar].open;
output_h[input_end_bar_indicator]=output_o[input_end_bar_indicator];
output_l[input_end_bar_indicator]=output_o[input_end_bar_indicator];
output_c[input_end_bar_indicator]=output_o[input_end_bar_indicator];
work_status=-1;
 }
}

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

Функция ежесекундного рисования свечи func_candle_per_seconds()

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

Описанная функция отвечает за "седьмую скорость" формирования свечи (см. на панели управления).

//+------------------------------------------------------------------+
//| Func Candle Per Seconds                                          |
//+------------------------------------------------------------------+
void func_candle_per_seconds(MqlRates &input_rates[],
 datetime &input_end_time_indicator,
 int input_bars_now_rates,
 int input_number_now_rates,
 int &input_end_bar_indicator,
 double &output_o[],
 double &output_h[],
 double &output_l[],
 double &output_c[],
 double &output_col[],
 char &work_status)
{
 if(work_status==-1)
 {
if(input_number_now_rates<input_bars_now_rates)
{
 if(input_number_now_rates!=0)
 {
output_o[input_end_bar_indicator]=input_rates[input_number_now_rates-1].open;
output_h[input_end_bar_indicator]=input_rates[input_number_now_rates-1].high;
output_l[input_end_bar_indicator]=input_rates[input_number_now_rates-1].low;
output_c[input_end_bar_indicator]=input_rates[input_number_now_rates-1].close;
if(output_o[input_end_bar_indicator]>output_c[input_end_bar_indicator])output_col[input_end_bar_indicator]=0;
else output_col[input_end_bar_indicator]=1;
 }
 input_end_bar_indicator--;
 output_o[input_end_bar_indicator]=input_rates[input_number_now_rates].open;
 output_h[input_end_bar_indicator]=input_rates[input_number_now_rates].high;
 output_l[input_end_bar_indicator]=input_rates[input_number_now_rates].low;
 output_c[input_end_bar_indicator]=input_rates[input_number_now_rates].close;
 if(output_o[input_end_bar_indicator]>output_c[input_end_bar_indicator])output_col[input_end_bar_indicator]=0;
 else output_col[input_end_bar_indicator]=1;
 input_end_time_indicator=input_rates[input_number_now_rates+1].time;
}
 }
}

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

Функция формирования свечи func_of_form_candle()

Входные параметры те же, что и ранее (OHLC). Что же касается функциональности, то тут всё предельно просто. Происходит циклическое копирование цен из данных по таймфрейму М1 в текущую свечу, от времени, которое поступило из функции заполнения func_filling(). Получается, что изменяя время, мы постепенно формируем свечу.Таким образом построены скорости со второй по шестую (см. панель управления). После того, как время доходит до полного формирования свечи по текущему таймфрейму, изменяется флаг work_status, чтобы при следующем исполнении таймера первоначально снова была запущена функция заполнения func_filling().

//+------------------------------------------------------------------+
//| Func Of Form Candle                                              |
//+------------------------------------------------------------------+
void func_of_form_candle(MqlRates &input_rates[],
 int input_bars,
 datetime &input_time_end_filling,
 datetime &input_end_time_indicator,
 datetime &input_time_next_filling,
 int input_end_bar_indicator,
 double &output_o[],
 double &output_h[],
 double &output_l[],
 double &output_c[],
 double &output_col[],
 char &work_status)
{
 if(work_status==-1)
 {
int start_of_z=0;
int end_of_z=0;
start_of_z=Bars(_Symbol,PERIOD_M1,real_start,input_time_end_filling);
end_of_z=Bars(_Symbol,PERIOD_M1,real_start,input_end_time_indicator);
for(int z=start_of_z; z<end_of_z; z++)
{
 output_c[input_end_bar_indicator]=input_rates[z].close;
 if(output_h[input_end_bar_indicator]<input_rates[z].high)output_h[input_end_bar_indicator]=input_rates[z].high;
 if(output_l[input_end_bar_indicator]>input_rates[z].low)output_l[input_end_bar_indicator]=input_rates[z].low;
 if(output_o[input_end_bar_indicator]>output_c[input_end_bar_indicator])output_col[input_end_bar_indicator]=0;
 else output_col[input_end_bar_indicator]=1;
}
if(input_end_time_indicator>=input_time_next_filling)work_status=1;
 }
}

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

Функция "ювелирного" построения свечи func_of_form_jeweler_candle()

В начале функции всё происходит как в предыдущем варианте. Данные минутного таймфрейма полностью заполняют текущую свечу, за исключением последней минуты. Её данные передаются функции формирования тиков, описанной в начале статьи func_tick_generation(). Полученный массив тиков постепенно, с каждым вызовом функции, передаётся ценой закрытия текущей свечи, с учётом корректировки "теней". Когда "тики" из массива кончаются, процесс повторяется.

//+------------------------------------------------------------------+
//| Func Of Form Jeweler Candle                                      |
//+------------------------------------------------------------------+
void func_of_form_jeweler_candle(MqlRates &input_rates[],                    //информация для генерации тиков
 int input_bars,                             //размер массива информации
 datetime &input_time_end_filling,           //время окончания быстрого заполнения
 datetime &input_end_time_indicator,         //время последнего построения индикатора
 datetime &input_time_next_filling,          //время до конца формирования полного бара текущего таймфрейма
 int input_end_bar_indicator,                //текущий в прорисовке бар индикатора
 double &output_o[],
 double &output_h[],
 double &output_l[],
 double &output_c[],
 double &output_col[],
 char &work_status                           // вид окончания работы (команда для функции быстрого заполнения)
 )
{
 if(work_status==-1)
 {
int start_of_z=0;
int current_of_z=0;
start_of_z=Bars(_Symbol,PERIOD_M1,real_start,input_time_end_filling)-1;
current_of_z=Bars(_Symbol,PERIOD_M1,real_start,input_end_time_indicator)-1;
if(start_of_z<current_of_z-1)
{
 for(int z=start_of_z; z<current_of_z-1; z++)
 {
output_c[input_end_bar_indicator]=input_rates[z].close;
if(output_h[input_end_bar_indicator]<input_rates[z].high)output_h[input_end_bar_indicator]=input_rates[z].high;
if(output_l[input_end_bar_indicator]>input_rates[z].low)output_l[input_end_bar_indicator]=input_rates[z].low;
if(output_o[input_end_bar_indicator]>output_c[input_end_bar_indicator])output_col[input_end_bar_indicator]=0;
else output_col[input_end_bar_indicator]=1;
 }
 input_end_time_indicator=input_rates[current_of_z].time;
}
//получаем тики в массив
static int x=0;                   // подсчёт массива и флаг начала
static double tick_current[];
static int tick_current_size=0;
if(x==0)
{
 func_tick_generation(input_rates[current_of_z-1],tick_current);
 tick_current_size=ArraySize(tick_current);
 if(output_h[input_end_bar_indicator]==0)
 {output_h[input_end_bar_indicator]=tick_current[x];}
 if(output_l[input_end_bar_indicator]==0)
 {output_l[input_end_bar_indicator]=tick_current[x];}
 output_c[input_end_bar_indicator]=tick_current[x];
}
if(x<tick_current_size)
{
 output_c[input_end_bar_indicator]=tick_current[x];
 if(tick_current[x]>output_h[input_end_bar_indicator])
 {output_h[input_end_bar_indicator]=tick_current[x];}
 if(tick_current[x]<output_l[input_end_bar_indicator])
 {output_l[input_end_bar_indicator]=tick_current[x];}
 if(output_o[input_end_bar_indicator]>output_c[input_end_bar_indicator])output_col[input_end_bar_indicator]=0;
 else output_col[input_end_bar_indicator]=1;
 x++;
}
else
{
 input_end_time_indicator=input_rates[current_of_z+1].time;
 x=0;
 tick_current_size=0;
 ArrayFree(tick_current);
}
if(input_end_time_indicator>input_time_next_filling)
{work_status=1;}
 }
}

Все три варианта построения свечей сосредоточены в функции Merger.

Функция объединения построения func_merger()

В зависимости от скорости, выбранной оператором-переключателем switch, определяется та функция, которая будет в работе. Можно выделить три ее разновидности. Любая метка начинается с функции заполнения func_filling(), далее передаётся управление одной из трёх функций формирования свечи: func_of_form_jeweler_candle(), func_of_form_candle() или func_candle_per_seconds(). Со второй скорости по шестую включительно на каждом проходе пересчитывается время. Функция func_calc_time() вычисляет требуемую часть от текущего таймфрейма и добавляет ее к текущему времени. Цена Bid берется с цены закрытия текущей свечи, цена Ask рассчитывается на основании спреда по данным с торгового сервера.

//+------------------------------------------------------------------+
//| Func Merger                                                      |
//+------------------------------------------------------------------+
void func_merger()
{
 switch(button_speed)
 {
case 1:
{
 func_filling(period_array,end_time_indicator,all_bars_indicator,time_open_end_rates,time_open_next_rates,end_bar_indicator,TST_C_O,TST_C_H,TST_C_L,TST_C_C,TST_C_Col,status);
 func_of_form_jeweler_candle(period_m1,bars_m1,time_open_end_rates,end_time_indicator,time_open_next_rates,end_bar_indicator,TST_C_O,TST_C_H,TST_C_L,TST_C_C,TST_C_Col,status);
 price_bid_now=TST_C_C[end_bar_indicator];
 price_ask_now=price_bid_now+(spread*_Point);
}
break;
case 2:
{
 func_filling(period_array,end_time_indicator,all_bars_indicator,time_open_end_rates,time_open_next_rates,end_bar_indicator,TST_C_O,TST_C_H,TST_C_L,TST_C_C,TST_C_Col,status);
 func_of_form_candle(period_m1,bars_m1,time_open_end_rates,end_time_indicator,time_open_next_rates,end_bar_indicator,TST_C_O,TST_C_H,TST_C_L,TST_C_C,TST_C_Col,status);
 price_bid_now=TST_C_C[end_bar_indicator];
 price_ask_now=price_bid_now+(spread*_Point);
 end_time_indicator+=func_calc_time(time_open_end_rates,time_open_next_rates,13);
}
break;
case 3:
{
 ...
 end_time_indicator+=func_calc_time(time_open_end_rates,time_open_next_rates,11);
}
break;
case 4:
{
 ...
 end_time_indicator+=func_calc_time(time_open_end_rates,time_open_next_rates,9);
}
break;
case 5:
{
 ...
 end_time_indicator+=func_calc_time(time_open_end_rates,time_open_next_rates,7);
}
break;
case 6:
{
 ...
 end_time_indicator+=func_calc_time(time_open_end_rates,time_open_next_rates,5);
}
break;
case 7:
{
 func_filling(period_array,end_time_indicator,all_bars_indicator,time_open_end_rates,time_open_next_rates,end_bar_indicator,TST_C_O,TST_C_H,TST_C_L,TST_C_C,TST_C_Col,status);
 func_candle_per_seconds(period_array,end_time_indicator,bars_now_rates,number_now_rates,end_bar_indicator,TST_C_O,TST_C_H,TST_C_L,TST_C_C,TST_C_Col,status);
 price_bid_now=TST_C_C[end_bar_indicator];
 price_ask_now=price_bid_now+(spread*_Point);
}
break;
 }
}

Возможность использования

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

Пример работы индикатора:

Заключение

Теперь открою секрет: почему же все-таки одно из видов построения я назвал "ювелирным"?

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

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

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


Прикрепленные файлы |
STSv1.1.mq5 (127.46 KB)
STSv1.1.ex5 (113.75 KB)
for_STS.zip (13.39 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (10)
Ivan Gurov
Ivan Gurov | 9 дек. 2017 в 11:56

Дмитрий, очень понравилась Ваша идея. Спасибо за статью. :)

Stanislav Korotky
Stanislav Korotky | 9 дек. 2017 в 23:22
Igor Volodin:

Вы можете в тестере MT5 после его запуска накидать любых индикаторов из навигатора и настроить графических построений?

Пока вроде нельзя именно так (я писал, что индикаторы нужно добавить в шаблон). Но кое-что еще делать можно. См. обсуждение здесь:

Форум по трейдингу, автоматическим торговым системам и тестированию торговых стратегий

Ручное открытие позиций в тестере MT5

Vladislav Andruschenko, 2017.10.15 10:53


выложил бесплатный привод для тестера :-) ТЕСТЕРА, БЕСПЛАТНЫЙ. 

Ладно. 

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


Usera
Usera | 4 февр. 2018 в 11:40
Подскажите пожалуйста, если я правильно понял, то возможности выставлять отложенные ордера отсутствует?
Dmitriy Zabudskiy
Dmitriy Zabudskiy | 4 февр. 2018 в 14:42
Usera:
Подскажите пожалуйста, если я правильно понял, то возможности выставлять отложенные ордера отсутствует?

Всё верно, отсутствует.

Sergei Poliukhov
Sergei Poliukhov | 7 нояб. 2021 в 22:06

Подскажите, получаю ошибку.

2021.11.08 00:04:32.398 STSv1.1 (EURUSD,M1) array out of range in 'STSv1.1.mq5' (734,54)

build 2875.


Ругается на эту строчку кода. Чоо можно исправить?

output_o[input_end_bar_indicator]=input_rates[stopped_rates_bar].open;

Кроссплатформенный торговый советник: Пользовательские стопы, Безубыток и Трейлинг Кроссплатформенный торговый советник: Пользовательские стопы, Безубыток и Трейлинг
В статье обсуждается установка пользовательских стоп-уровней в кроссплатформенном советнике. Также описан тесно связанный с ними метод, который помогает задать изменение стоп-уровней с течением времени.
Тестирование паттернов, возникающих при торговле корзинами валютных пар. Часть II Тестирование паттернов, возникающих при торговле корзинами валютных пар. Часть II
Продолжаем тестирование паттернов и проверку методик, описанных в статьях о торговле корзинами валютных пар. Рассмотрим на практике, можно ли использовать паттерны пересечения графиком объединенного WPR скользящей средней, и если можно, то как именно.
R-квадрат как оценка качества кривой баланса стратегии R-квадрат как оценка качества кривой баланса стратегии
Статья описывает построение пользовательского критерия оптимизации R-квадрат. По этому критерию можно оценить качество кривой баланса стратегии и выбрать наиболее равномерно растущие и стабильные стратегии. Материал описывает принципы его построения и статистические методы, используемые для оценки свойств и качества этой метрики.
Кроссплатформенный торговый советник: Стоп-уровни Кроссплатформенный торговый советник: Стоп-уровни
В этой статье рассматривается реализация стоп-уровней в торговом советнике, совместимая с платформами MetaTrader 4 и MetaTrader 5.