Как обезопасить себя и своего эксперта при торговле на Московской бирже

Vasiliy Sokolov | 18 июня, 2015

Оглавление


Введение

Каждый, кто когда-либо торговал на финансовых рынках, сталкивался с рисками потерь денежных средств. Природа этих рисков различна, однако их итоговый результат один и тот же: потерянные деньги, время и нервы. Чтобы избежать их, необходимо следовать простым правилам: контролировать свои риски (Money Management), писать надежные торговые алгоритмы, и, конечно же, использовать прибыльные торговые системы. Все это относится к различным областям трейдинга, однако применяя эти методы только в совокупности, можно надеяться на устойчивый положительный торговый результат.

На сегодняшний день существует довольно обширный материал, освещающий вопросы управления капиталом (Money Management). Также немало книг написано о торговых системах, которые можно использовать в повседневной практике трейдинга. Материала же, описывающего элементарные "правила безопасности" на биржевых рынках, совсем немного.

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

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


Глава 1. Дискретность потоковых цен и методы противодействия ей


1.1. Дискретность потоковых цен. Ценовые разрывы

Ликвидность — одно из основных понятий на биржевом рынке. Ликвидность — это способность рынка покупать у вас и продавать вам товар по ценам, близким к рыночным. Чем ликвидность рынка выше, тем цены более плотно следуют друг за другом. Процесс ценообразования является по своей сути дискретным. Это значит, что цена, за которой мы привыкли наблюдать, состоит из множества сделок, идущих друг за другом с большой скоростью. Этот поток сделок образуют котировки, или тиковые графики, из которых торговые платформы создают более удобные свечные или баровые графики произвольного таймфрейма. С точки зрения трейдера такие графики являются непрерывными. В каждый момент времени у такого бара или свечи есть определенная цена. Давайте рассмотрим такой бар на схеме:


Рис. 1. Ценовой бар и его непрерывная функция цены

В какой бы точке этого ценового бара мы ни находились, для нее будет существовать свой уровень цен, изображенный красной линией. Именно такое представление баров используется в тестере стратегий MetaTrader в режиме "Все тики". Цены внутри него генерируются непрерывно и последовательно. Так, если шаг цены равен единице, и цена прошла с 10 до 15, в процессе хода цены нам также будут доступны цены 11, 12, 13 и 14. В реальности цена, как уже было сказано выше, дискретна и изменяется маленькими скачками. Эти ценовые изменения не всегда могут быть последовательными и равномерными. Иногда цена может "перепрыгнуть" сразу несколько ценовых уровней. Изобразим тот же бар, что и на рисунке выше, однако в этот раз будет использовать более реальную, дискретную схему изменения цены:

Рис. 2. Ценовой бар и его дискретная функция цены

На этой схеме, как таковой непрерывной цены не существует. Эта условная, но в реальности не существующая цена на рисунке 2 изображена пунктирной линией. На практике это означает, что наши рыночные заявки, в особенности стоп-ордера, могут исполниться не там, где они были установлены! Это очень опасное свойство рыночных ордеров. Давайте рассмотрим схему исполнения отложенного Buy Stop ордера для этого бара. Предположим, что мы отправили заявку на биржу, когда цена достигнет уровня 64 203 или выше. Этот уровень мы условно обозначим в виде голубой пунктирной линии, пересекающей уже знакомый нам ценовой бар. Однако этой цены внутри бара может и не быть. В этом случае наш ордер активирует следующая цена, которая будет значительно превышать уровень 64 203:

Рис. 3. Исполнение отложенных ордеров при дискретных ценах

В данном примере фактическое исполнение произойдет лишь по цене 64 220 пунктов, что на 13 пунктов хуже запрашиваемой нами цены. Разница между этими ценами будет нашим проскальзыванием. Если рынки достаточно ликвидны, то дискретные цены идут плотным потоком, плавно переходя из одного уровня в другой. Однако в случаях быстрого изменения цены даже на высоколиквидных рынках возможны ценовые разрывы. Такие ценовые разрывы невозможно заметить, наблюдая обычные ценовые графики, однако возможность их существования следует учитывать.


1.2. Ценовые выбросы и "шпильки"

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

Рассмотрим простую ситуацию: предположим, мы торгуем фьючерсным контрактом на рубль-доллар и выставили Buy Stop ордер на покупку по цене 64 200. Стоп-лосс для этого ордера мы установили на уровне 64 100. Мы ожидаем, что курс пойдет вверх, однако если этого не произойдет, наш стоп-лосс на уровне 64 100 ограничит наш убыток 100 пунктами. Кажется, что наш риск ограничен, однако это не так. Рассмотрим ситуацию, когда происходит ценовой выброс, который активирует срабатывание нашего стоп-ордера совсем по другим ценам:

Рис. 4. Тиковое представление "шпильки" и схема исполнение ордера BuyStop.

На этом графике изображен тиковый поток цен. Видно, что один из тиков находится на значительном расстоянии от остальных тиков. Именно он образует так называемую "шпильку". Этот тик активирует наш Buy Stop ордер и исполнит его по цене 64 440. Уже на следующем тике цена вернется в свой привычный диапазон и активирует наш стоп-ордер по цене 64 100. За долю секунды наш отложенный ордер может исполниться и закрыться по стоп-лоссу, оставив нас с колоссальным убытком. Вместо рассчитанного нами убытка в 100 пунктов, наш ордер принесет 340 пунктов убытка.

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

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


1.3. Контролируем максимальное проскальзывание с помощью лимитных ордеров

Мы выяснили, что рыночные и стоп-ордера несут опасность из-за возможности неограниченного проскальзывания. Для нашей заявки ликвидности может не хватить, также рынок может полностью потерять свою рыночную ликвидность на короткое время, и тогда ценовые выбросы будут неизбежны. Более того, на малоликвидных рынках, например на рынках опционов FORTS, такие ситуации являются постоянными. Однако их можно избежать, если вместо рыночных и стоп-ордеров использовать лимитные ордера.

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

Например, если текущей ценой фьючерсного контракта рубль-доллар будет 64 200, мы можем выставить ордер Buy Limit по цене 64 220. Эта конфигурация будет означать, что мы готовы купить, если цена будет не дороже 64 220. Поскольку текущая цена 64 200 лучше нашей заявленной предельной цены для покупки, исполнение нашей заявки начнется сразу после ее выставления. Благодаря этому свойству мы можем контролировать величину нашего предельного проскальзывания. Ведь если по каким-то причинам ликвидности до уровня 64 220 не хватит, оставшаяся часть нашей заявки просто не исполнится.

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

Хорошей привычкой будет использование лимитных ордеров в качестве входов и выходов с рынка. Используйте лимитные ордера даже тогда, когда ваша стратегия требует входов и выходов по текущим рыночным ценам. Замените ордера Buy и Sell на Buy Limit и Sell Limit соответственно. Например, если вам нужно купить по текущей цене, сформируйте лимитную заявку, в которой предельная цена исполнения будет чуть выше текущей рыночной цены. То же верно и для наших продаж по рынку. В этом случае достаточно установить наш Sell Limit ордер с уровнем цены чуть ниже текущего. Разница между ценой, указанной в лимитном ордере, и текущей ценой будет соответствовать максимальному проскальзыванию, на которое мы готовы подвергнуть исполнение нашей заявки.

Рассмотрим практическую ситуацию применения лимитного ордера в качестве альтернативы входа по рынку. Предположим, нам необходимо купить достаточно крупный объем фьючерса евро-доллар ED-3.15 по цене 1.1356. Наша ситуация покупки будет не совсем обычная. Дело в том, что момент нашей покупки совпадет с моментом сильного размытия ликвидности. Такой момент выбран намеренно, чтобы показать преимущества входа в рынок с помощью лимитных ордеров. Момент входа совпадет с образованием так называемой "шпильки", которая будет видна на минутном графике:

Рис. 5. Вход в рынок в момент ценовой шпильки, ED-3.15

Видно, что вход выбран крайне неудачно. Давайте проанализируем тиковый график в этот момент времени:

Рис. 6. Тиковый график и исполнение лимитного ордера в момент прорыва ликвидности

Исполнение нашего лимитного ордера показано большими белыми кружками (тиками): . Сами тики изображены в виде голубых круглых точек. Если бы мы решили купить по рынку в момент наступления цены 1.1356, нашу заявку исполнили бы несколькими сделками, начиная с цены 1.1356 и заканчивая ценой 1.1398. Это вызвало бы сильное проскальзывание и наша средняя цена входа значительно бы отличалась в худшую сторону от цены 1.1356. Чем больше контрактов нам необходимо было бы купить, тем хуже была бы средняя цена входа в нашу позицию.

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


1.4. Ручная установка лимитного ордера, контролирующего предельное проскальзывание

Теперь, когда мы рассмотрели принцип срабатывания лимитного ордера в теории, настало время немного попрактиковаться и посмотреть на его работу в реальных условиях. Для этого давайте откроем счет, подключенный к Московской бирже, и выставим лимитную заявку по цене чуть хуже текущей. В качестве инструмента выберем ближайший фьючерс на евро-доллар, в нашем случае этим фьючерсом оказался ED-6.15. Вызовем окно нового ордера и установим ордер Buy Limit, чья цена будет немного выше текущей цены Ask:

Рис. 7. Установка лимитного ордера вручную в режиме биржевого исполнения ордеров

Рис. 7. Установка лимитного ордера вручную в режиме биржевого исполнения ордеров

На скриншоте видно, что текущая цена Ask равна 1.1242, мы же установили отложенную цену равную 1.1245. Разница между нашей ценой и ценой лучшего предложения равна 0.0003 пунктам (1.1245 - 1.1242 = 0.0003). Эти три пункта эквивалентны максимальному проскальзыванию, на которое мы можем подвергнуть свою сделку. В режиме биржевого исполнения цен установка лимитного ордера данным способом эквивалента установке обычного ордера Buy или Sell с указанием максимального проскальзывания (Deviation):

Рис. 8. Исполнение рыночного ордера с указанием предельного отклонения

Рис. 8. Исполнение рыночного ордера с указанием предельного отклонения

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


1.5. Установка предельного проскальзывания в режиме биржевого исполнения с помощью эксперта

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

На скриншоте ниже показан внешний вид первой версии получившейся панели:

Рис. 9. Установка предельного проскальзывания в панели DevaitionPanel.

Панель написана в виде класса CDevPanel. Приведем исходный код этого класса:

//+------------------------------------------------------------------+
//|                                                       Panel.mqh  |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#include <Trade\Trade.mqh>
#define OP_BUY 0
#define OP_SELL 1
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CDevPanel
  {
private:
   CTrade            Trade;
   string            m_descr_dev;
   string            m_buy_button_name;
   string            m_sell_button_name;
   string            m_deviation_name;
   string            m_volume_name;
   string            m_bg_fon;
   int               m_deviation;
   void              OnObjClick(string sparam);
   void              OnEndEdit(string sparam);
   double            CalcCurrentPrice(int op_type);

public:
                     CDevPanel();
                    ~CDevPanel();
   void              OnChartEvent(const int id,
                                  const long &lparam,
                                  const double &dparam,
                                  const string &sparam);
  };
//+------------------------------------------------------------------+
//| CDevPanel class                                                  |
//+------------------------------------------------------------------+
CDevPanel::CDevPanel(): m_buy_button_name("buy_button"),
                        m_sell_button_name("sell_button"),
                        m_deviation_name("deviation"),
                        m_volume_name("volume"),
                        m_bg_fon("bg_fon"),
                        m_descr_dev("descr_dev"),
                        m_deviation(3)
  {
//--- background
   ObjectCreate(0,m_bg_fon,OBJ_RECTANGLE_LABEL,0,0,0);
   ObjectSetInteger(0,m_bg_fon,OBJPROP_YSIZE,80);
   ObjectSetInteger(0,m_bg_fon,OBJPROP_XSIZE,190);
   ObjectSetInteger(0,m_bg_fon,OBJPROP_BGCOLOR,clrWhiteSmoke);

//--- buy button
   ObjectCreate(0,m_buy_button_name,OBJ_BUTTON,0,0,0);
   ObjectSetInteger(0,m_buy_button_name,OBJPROP_XDISTANCE,100);
   ObjectSetInteger(0,m_buy_button_name,OBJPROP_YDISTANCE,50);
   ObjectSetInteger(0,m_buy_button_name,OBJPROP_XSIZE,80);
   ObjectSetInteger(0,m_buy_button_name,OBJPROP_BGCOLOR,clrAliceBlue);
   ObjectSetString(0,m_buy_button_name,OBJPROP_TEXT,"BUY");

//--- sell button
   ObjectCreate(0,m_sell_button_name,OBJ_BUTTON,0,0,0);
   ObjectSetInteger(0,m_sell_button_name,OBJPROP_XDISTANCE,10);
   ObjectSetInteger(0,m_sell_button_name,OBJPROP_YDISTANCE,50);
   ObjectSetInteger(0,m_sell_button_name,OBJPROP_XSIZE,80);
   ObjectSetInteger(0,m_sell_button_name,OBJPROP_BGCOLOR,clrPink);
   ObjectSetString(0,m_sell_button_name,OBJPROP_TEXT,"SELL");

//--- deviation
   ObjectCreate(0,m_deviation_name,OBJ_EDIT,0,0,0);
   ObjectSetInteger(0,m_deviation_name,OBJPROP_XDISTANCE,120);
   ObjectSetInteger(0,m_deviation_name,OBJPROP_YDISTANCE,20);
   ObjectSetInteger(0,m_deviation_name,OBJPROP_XSIZE,60);
   ObjectSetInteger(0,m_deviation_name,OBJPROP_BGCOLOR,clrWhite);
   ObjectSetInteger(0,m_deviation_name,OBJPROP_COLOR,clrBlack);
   ObjectSetInteger(0,m_deviation_name,OBJPROP_ALIGN,ALIGN_RIGHT);
   ObjectSetString(0,m_deviation_name,OBJPROP_TEXT,(string)m_deviation);

//--- description
   ObjectCreate(0,m_descr_dev,OBJ_LABEL,0,0,0);
   ObjectSetInteger(0,m_descr_dev,OBJPROP_XDISTANCE,12);
   ObjectSetInteger(0,m_descr_dev,OBJPROP_YDISTANCE,20);
   ObjectSetInteger(0,m_descr_dev,OBJPROP_XSIZE,80);
   ObjectSetInteger(0,m_descr_dev,OBJPROP_BGCOLOR,clrWhite);
   ObjectSetString(0,m_descr_dev,OBJPROP_TEXT,"Deviation (pips):");
   ObjectSetInteger(0,m_descr_dev,OBJPROP_COLOR,clrBlack);
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CDevPanel::~CDevPanel(void)
  {
   ObjectDelete(0,m_buy_button_name);
   ObjectDelete(0,m_sell_button_name);
   ObjectDelete(0,m_bg_fon);
   ObjectDelete(0,m_deviation_name);
   ObjectDelete(0,m_descr_dev);
  }
//+------------------------------------------------------------------+
//| Event function                                                   |
//+------------------------------------------------------------------+
void CDevPanel::OnChartEvent(const int id,
                             const long &lparam,
                             const double &dparam,
                             const string &sparam)
  {
   switch(id)
     {
      case CHARTEVENT_OBJECT_CLICK:
         OnObjClick(sparam);
         break;
      case CHARTEVENT_OBJECT_ENDEDIT:
         OnEndEdit(sparam);
     }
  }
//+------------------------------------------------------------------+
//| End edit detect                                                  |
//+------------------------------------------------------------------+
void CDevPanel::OnEndEdit(string sparam)
  {
   if(sparam != m_deviation_name)return;
   int value = (int)ObjectGetString(0, m_deviation_name, OBJPROP_TEXT);
   if(value <= 0)
      ObjectSetString(0,m_deviation_name,OBJPROP_TEXT,(string)m_deviation);
   else
      m_deviation=value;
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| End obj click                                                    |
//+------------------------------------------------------------------+
void CDevPanel::OnObjClick(string sparam)
  {
   if(sparam==m_buy_button_name)
      Trade.BuyLimit(1,CalcCurrentPrice(OP_BUY));
   if(sparam==m_sell_button_name)
      Trade.SellLimit(1,CalcCurrentPrice(OP_SELL));
   ObjectSetInteger(0,sparam,OBJPROP_STATE,false);
   Sleep(100);
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| Calc level price                                                 |
//+------------------------------------------------------------------+
double CDevPanel::CalcCurrentPrice(int op_type)
  {
   if(op_type==OP_BUY)
     {
      double ask=SymbolInfoDouble(Symbol(),SYMBOL_ASK);
      return ask + (m_deviation * Point());
     }
   else if(op_type==OP_SELL)
     {
      double bid=SymbolInfoDouble(Symbol(),SYMBOL_BID);
      return bid - (m_deviation * Point());
     }
   return 0.0;
  }
//+------------------------------------------------------------------+

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

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

2015.04.15 14:08:39.709 Trades  '58406864': failed buy limit 0.10 EURUSD at 1.05927 [Invalid price]


1.6. Buy Stop Limit и Sell Stop Limit ордера как альтернатива Buy Stop и Sell Stop ордерам

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

В этом случае на помощь нам могут прийти Buy Stop Limit и Sell Stop Limit ордера. Эти ордера являются алгоритмическими ордерами для MetaTrader 5, они не являются биржевыми, а реализуются на сервере MetaTrader. Давайте обратимся к официальной документации чтобы понять, как они работают:

  • Buy Stop Limit — этот вид ордера сочетает в себе первые два типа (Buy Limit и Buy Stop ордера - прим. автора), являясь стоп ордером на установку лимитного ордера на покупку ("Buy Limit"). Как только будущая цена "Ask" достигнет стоп-уровня, указанного в этом ордере (поле "Цена"), будет выставлен "Buy Limit" приказ на уровне, указанном в поле "Цена Stop Limit".
  • Sell Stop Limit — этот вид ордера является стоп ордером на установку лимитного ордера на продажу ("Sell Limit"). Как только будущая цена "Bid" достигнет стоп-уровня, указанного в этом ордере (поле "Цена"), будет выставлен "Sell Limit" приказ на уровне, указанным в поле "Цена Stop Limit".

В документации также представлена схема (рис. 10), изображающая принцип работы ордеров в MetaTrader 5. В этой схеме нам будут интересны два типа ордеров, отмеченные желтой рамкой:

Рис. 10. Типы ордеров в MetaTrader 5

Из определения и схемы Buy Stop Limit и Sell Stop Limit ордеров следует, что это лимитные ордера, которые выставляются в рынок в момент достижения ценой определенного стоп-уровня. Для Buy Stop Limit ордера стоп-уровень выставляется выше текущей цены Ask, а для Sell Stop Limit — ниже текущей цены Bid. Цена же лимитного уровня в режиме биржевого исполнения может быть как выше, так и ниже стоп-цены этих ордеров. Эта особенность дает нам возможность конфигурировать специальные стоп-ордера с контролируемым проскальзыванием. Давайте обратимся к следующей схеме, показывающей, как это работает:

Рис. 11. Схема установки Buy Stop Limit ордера для задания предельного проскальзывания

Их схемы видно, что мы можем выставить Buy Stop Limit ордер с ценой Limit Price, превышающей цену Stop Price. Это значит, что сразу после достижения цены Stop Price будет отправлен Buy Limit ордер, который тут же исполнится, т.к. цена Limit Price будет хуже текущей цены Stop Price. Разница между ценами Stop Price и Limit Price будет предельным проскальзыванием, которое мы решим установить для наших заявок. Так же работают Sell Stop Limit ордера, только для них цена Limit Price должна быть ниже цены Stop Price.

Теперь займемся практикой — попробуем выставить Buy Stop Limit ордер вручную.


1.7. Установка Buy Stop Limit и Sell Stop Limit ордеров вручную вместо стоп-лосс уровней

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

Давайте на примере посмотрим, как это сделать. Предположим, что у нас открыта длинная позиция по Si-6.15. Ее стоп-лосс уровнем является уровень 56 960. Нам необходимо установить предельное проскальзывание в пять пунктов, тогда значение "Stop Limit price" будет равно 56 960 - 5 = 56 955 пунктов:

Рис. 12. Установка SellStopLimit ордера в качестве стоп-уровня для длинной позиции

Рис. 12. Установка SellStopLimit ордера в качестве стоп-уровня для длинной позиции

Как видите, в режиме биржевого исполнения становится возможной подобная конфигурация Sell Stop Limit ордера. Когда текущая цена достигнет уровня 56 960, выставится лимитная заявка на продажу по 56 955. Так как текущая цена 56 960 лучше заявленной цены в лимитной заявке, она тут же исполнится по цене 56 960. Если ликвидности на покупку на этом уровне не хватит, исполнение пойдет по следующим за ним ценам вплоть до уровня 56 955. Однако хуже этой цены наш стоп-лимит ордер не исполнится, гарантируя тем самым нам максимальное проскальзывание в размере пяти пунктов: 56 960 - 56 955 = 5.

Теперь то же самое попробуем сделать для защиты нашей короткой позиции. Чтобы закрыть ее по стоп-лоссу, нужно совершить противоположную короткой позиции операцию — необходимо будет купить, отправив заявку Buy Stop Limit. Предположим, текущий стоп-лосс для нашей короткой позиции будет 56 920, тогда конфигурация нашей Buy Stop Limit заявки с предельным проскальзыванием в пять пунктов будет следующая:

Рис. 13. Установка BuyStopLimit ордера в качестве стоп-уровня для короткой позиции

Рис. 13. Установка BuyStopLimit ордера в качестве стоп-уровня для короткой позиции

Как видите, на этот раз цена "Stop Limit price" на пять пунктов выше цены "Price" и равна 56 925.


1.8. Замена стоп-лосс уровней на Buy Stop Limit и Sell Stop Limit ордера в своем эксперте

Снова вернемся к нашей панели, описанной в пункте 1.5 данной статьи. Попробуем модифицировать ее, чтобы помимо входа в рынок с помощью лимитных ордеров она позволяла устанавливать нам защитные остановки с помощью Buy Stop Limit и Sell Stop Limit ордеров. Для этого в панели создадим еще одно поле "Stop Loss Level". Теперь наша панель будет выглядеть следующим образом:

Рис. 14. Установка стоп-лосс уровня в панели DevaitionPanel.

Наиболее значимых изменений в уже знакомом нам коде два: теперь в классе CDevPanel появился новый метод, ответственный за установку Buy Stop Limit и Sell Stop Limit ордеров. Также модифицировался метод OnObjClick, открывающий новую позицию. Исходный код этих методов следующий:

//+------------------------------------------------------------------+
//| End obj click                                                    |
//+------------------------------------------------------------------+
void CDevPanel::OnObjClick(string sparam)
  {
   if(sparam==m_buy_button_name)
     {
      if(Trade.BuyLimit(1,CalcCurrentPrice(OP_BUY)))
         SendStopLoss(OP_BUY);
     }
   if(sparam==m_sell_button_name)
     {
      if(Trade.SellLimit(1,CalcCurrentPrice(OP_SELL)))
         SendStopLoss(OP_SELL);
     }
   ObjectSetInteger(0,sparam,OBJPROP_STATE,false);
   Sleep(100);
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| Send SL order                                                    |
//+------------------------------------------------------------------+
bool CDevPanel::SendStopLoss(int op_type)
  {
   if(op_type==OP_BUY)
     {
      double bid=SymbolInfoDouble(Symbol(),SYMBOL_BID);
      if(m_sl_level>=0.0 && m_sl_level<bid)
        {
         MqlTradeRequest request={0};
         request.action = TRADE_ACTION_PENDING;
         request.symbol = Symbol();
         request.volume = 1.0;
         request.price=m_sl_level;
         request.stoplimit=m_sl_level -(m_deviation*Point());
         request.type=ORDER_TYPE_SELL_STOP_LIMIT;
         request.type_filling=ORDER_FILLING_RETURN;
         request.type_time=ORDER_TIME_DAY;
         MqlTradeResult result;
         bool res=OrderSend(request,result);
         if(!res)
            Print("Error set S/L. Reason: "+(string)GetLastError());
         return res;
        }
     }
   else if(op_type==OP_SELL)
     {
      double ask=SymbolInfoDouble(Symbol(),SYMBOL_ASK);
      if(m_sl_level>=0.0 && m_sl_level>ask)
        {
         MqlTradeRequest request={0};
         request.action = TRADE_ACTION_PENDING;
         request.symbol = Symbol();
         request.volume = 1.0;
         request.price=m_sl_level;
         request.stoplimit=m_sl_level+(m_deviation*Point());
         request.type=ORDER_TYPE_BUY_STOP_LIMIT;
         request.type_filling=ORDER_FILLING_RETURN;
         request.type_time=ORDER_TIME_DAY;
         MqlTradeResult result;
         bool res=OrderSend(request,result);
         if(!res)
            Print("Error set S/L. Reason: "+(string)GetLastError());
         return res;
        }
      if(CharToStr(StringGetChar(data,strlen-1))=='.')
         StringSetChar(data,strlen-1,'');
     }
   return false;
  }

Помимо этих методов, код класса панели стал включать инициализацию и настройку соответствующего поля для ввода стоп-лосса. Теперь, если нажать кнопку "BUY" или "SELL", а перед этим предварительно заполнить уровень "Stop Loss", то вместе с новым рыночным ордером выставится специальный защитный ордер Buy Stop Limit или Sell Stop Limit в зависимости от направления позиции.


Глава 2. Анализ рыночной ликвидности


2.1 Рассчитываем величину своего проскальзывания до входа в рынок

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

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

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

Рис. 15. Стакан цен по фьючерсному контракту Si-6.15

Из его показаний следует, что если бы в этот момент времени мы решили купить 2 контракта, то мы сделали бы это без проскальзывания, купив их по самой лучшей цене предложения: 51 931. Но если бы наш объем для покупки был бы выше, например, 4 контракта, то наша средневзвешенная цена отличалась бы от лучшего предложения (51 931) и составляла бы: (2*51931+2*51932)/4 = 51931.5. Два контракта мы бы купили по цене 51931, а оставшиеся 2 уже по цене 51932. Наша цена 51931.5 станет средневзвешенной ценой входа в рынок. Разница между этой ценой и лучшей ценой предложения и будет нашим проскальзыванием.

Зная формулу расчета средневзвешенной цены входа, можно рассчитать таблицу ликвидности, описывающую, какое проскальзывание мы будем нести при возрастании объема нашей сделки. Так, мы уже рассчитали, что при объеме сделки 1 или 2 контракта наша сделка совершится по лучшей цене предложения (51 931) без проскальзывания, а при объеме в четыре контракта наша сделка принесет проскальзывание 0.5 пункта (51931.5 - 51931.0). Формула расчета проскальзывания проста: достаточно из средневзвешенной цены входа вычесть лучшую цену предложения или покупки (в зависимости от направления сделки).

Итак, ниже давайте приведем нашу рассчитанную таблицу ликвидности:

ОбъемЦенаОбъем
Сделки
Средневзвешенная
цена входа
Проскальзывание
2 519382551934.53.5
9 51936 2351934.23.2
3 51935 1451933.02.0
7 51933 1151932.51.5
2 51932 451931.50.5
2 51931 251931.00.0

Таблица 1. Расчет средневзвешенной цены входа и величины проскальзывания на его основе

Данную таблицу необходимо читать снизу-вверх, так же как и стакан цен со стороны предложения, описанный выше. Из таблицы следует, что сделка объемом в два контракта будет иметь нулевое проскальзывание. Сделка объемом в четыре контракта, будет иметь проскальзывание 0.5 пунктов. Сделка объемом 25 контрактов проскользит на 3.5 пункта, и ее средневзвешенная цена входа будет равна 51934.5.

Централизованный рынок и наличие стакана позволяют нам сделать следующий вывод:

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

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


2.2. Считаем величину своего потенциального проскальзывания в режиме реального времени

После того, как мы изучили теорию, нам необходимо применить ее на практике. Рассчитывать величину потенциального проскальзывания в ручном режиме невозможно, т.к. изменения стакана происходят слишком быстро, а сам расчет является достаточно громоздкой процедурой. Поэтому нам потребуется автоматизировать наши расчеты. Чтобы облегчить расчеты со стаканом, мы включим в наш проект специальный класс для работы со стаканом CMarketBook. Написание подобного класса — сложная задача, достойная отдельной статьи. Мы не будем отходить в сторону, описывая работу этого класса, а лишь воспользуемся одним из его методов: GetDeviationByVol. Давайте посмотрим как он устроен:

//+------------------------------------------------------------------+
//| Get deviation value by volume. Retun -1.0 if deviation is        |
//| infinity (insufficient liquidity)                                |
//+------------------------------------------------------------------+
double CMarketBook::GetDeviationByVol(long vol,ENUM_MBOOK_SIDE side)
  {
   int best_ask = InfoGetInteger(MBOOK_BEST_ASK_INDEX);
   int last_ask = InfoGetInteger(MBOOK_LAST_ASK_INDEX);
   int best_bid = InfoGetInteger(MBOOK_BEST_BID_INDEX);
   int last_bid = InfoGetInteger(MBOOK_LAST_BID_INDEX);
   double avrg_price=0.0;
   long volume_exe=vol;
   if(side==MBOOK_ASK)
     {
      for(int i=best_ask; i>=last_ask; i--)
        {
         long currVol=MarketBook[i].volume<volume_exe ?
                      MarketBook[i].volume : volume_exe;
         avrg_price += currVol * MarketBook[i].price;
         volume_exe -= MarketBook[i].volume;
         if(volume_exe<=0)break;
        }
     }
   else
     {
      for(int i=best_bid; i<=last_bid; i++)
        {
         long currVol=MarketBook[i].volume<volume_exe ?
                      MarketBook[i].volume : volume_exe;
         avrg_price += currVol * MarketBook[i].price;
         volume_exe -= MarketBook[i].volume;
         if(volume_exe<=0)break;
        }
     }
   if(volume_exe>0)
      return -1.0;
   avrg_price/=(double)vol;
   double deviation=0.0;
   if(side==MBOOK_ASK)
      deviation=avrg_price-MarketBook[best_ask].price;
   else
      deviation=MarketBook[best_bid].price-avrg_price;
   return deviation;
  }

Данный метод в момент вызова обращается к стакану цен. Он перебирает стакан, начиная с лучшей цены, и считает объем, который доступен в стакане. Как только доступный объем в стакане сравняется или превысит требуемый, метод заканчивает перебор и рассчитывает средневзвешенную цену, соответствующую заданному объему. Разница между рассчитанной средневзвешенной ценой и лучшей ценой спроса (bid) или предложения (ask) и будет величиной нашего потенциального проскальзывания.

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

Теперь, когда у нас есть метод расчета потенциального проскальзывания, нам необходимо визуализировать полученные результаты. Очевидно, что величина проскальзывания взаимосвязана с объемом, который необходимо купить или продать на рынке. Чем объем выше, тем и проскальзывание больше. Таким образом, в нашу панель необходимо добавить новую строку и поле для ввода: "Объем сделки" или "Volume":

Рис 16. Панель с полем для ввода требуемого объема

Теперь наша панель способна продавать и покупать произвольный объем. Например, чтобы купить 5 контрактов по рынку, достаточно в поле "Volume" ввести значение "5" и нажать кнопку "BUY". Это не единственное нововведение. Как уже было сказано, с помощью метода GetDeviationVol мы научились контролировать величину своего проскальзывания в момент входа.

Чтобы расчетная величина была понятной, отобразим ее прямо на кнопках "BUY" и "SELL" соответственно. Наше проскальзывание мы будем указывать в пунктах. При каждом изменении стакана эта величина будет рассчитываться заново. С увеличением ликвидности значение проскальзывания будет падать, а при уменьшении, наоборот, увеличиваться. Если мы захотим купить или продать лишь один контракт, проскальзывания не будет вовсе, т.к. требуемый нами объем (1 контракт) никогда не превысит объема лучшего спроса или предложения.

Лучше всего за работой обновленной панели наблюдать в режиме реального времени. На видео ниже показан расчет потенциального проскальзывания в режиме реального времени для фьючерса RTS-6.15:

 

В самом начале в поле "Volume" вводится один контракт. Как и следовало ожидать, кнопки "BUY" и "SELL" показывают значение 0 — это означает, что наш вход в рынок не вызовет проскальзывания. После увеличения объема до 100 контрактов среднее проскальзывание для покупок и продаж возрастает до 10-20 пунктов. С увеличением объема до 500 контрактов среднее проскальзывание становится еще выше: 60-80 пунктов. Наконец, когда мы устанавливаем объем равный 1500 контрактам, ликвидности предложения начинает не хватать, и кнопка BUY показывает это, отображая значение -1.0 (оценка проскальзывания не определена). Ликвидность спроса по-прежнему достаточно, хотя продажа такого количества контрактов вызовет проскальзывание в 100-130 пунктов.

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


2.3. Индикатор спреда SpreadRecord в качестве фильтра для входа в позицию

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

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

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

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

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

//+------------------------------------------------------------------+
//|                                                Spread Record.mq4 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com/ru/users/c-4"
#property version   "1.00"
#property description "Recording spread and show it."
#property indicator_separate_window
#property indicator_buffers 5
#property indicator_plots   5
#property indicator_type1   DRAW_BARS
#property indicator_type2   DRAW_ARROW
#property indicator_color1   clrBlack
#property indicator_color2   clrBlack
double spread_open[];
double spread_high[];
double spread_low[];
double spread_close[];
double spread_avrg[];
int elements;
double avrg_current;
int count;
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,spread_open,INDICATOR_DATA);
   SetIndexBuffer(1,spread_high,INDICATOR_DATA);
   SetIndexBuffer(2,spread_low,INDICATOR_DATA);
   SetIndexBuffer(3,spread_close,INDICATOR_DATA);
   SetIndexBuffer(4,spread_avrg,INDICATOR_DATA);
   IndicatorSetInteger(INDICATOR_DIGITS,1);
   PlotIndexSetInteger(1,PLOT_ARROW,0x9f);
   PlotIndexSetInteger(0,PLOT_LINE_COLOR,clrRed);
   PlotIndexSetInteger(1,PLOT_LINE_COLOR,clrGreen);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   printf("DEINIT");
  }
//+------------------------------------------------------------------+
//| 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)
     {
      printf("INITIALIZE INDICATORS "+TimeToString(TimeCurrent()));
      double init_value=EMPTY_VALUE;
      ArrayInitialize(spread_high,init_value);
      ArrayInitialize(spread_low,init_value);
      ArrayInitialize(spread_open,init_value);
      ArrayInitialize(spread_close,init_value);
      ArrayInitialize(spread_avrg,init_value);
      elements=ArraySize(spread_high);
      InitNewBar(elements-1);
     }
//--- new bar initialization
   for(; elements<ArraySize(spread_high); elements++)
      InitNewBar(elements);
   double d=GetSpread();
   for(int i=rates_total-1; i<rates_total; i++)
     {
      if(d>spread_high[i])
         spread_high[i]=d;
      if(d<spread_low[i])
         spread_low[i]= d;
      spread_close[i] = d;
      avrg_current+=d;
      count++;
      spread_avrg[i]=avrg_current/count;
     }
//--- return value of prev_calculated for next call
   return(rates_total-1);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
double GetSpread()
  {
   double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
   double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID);
   return NormalizeDouble((ask-bid)/Point(), 0);
  }
//+------------------------------------------------------------------+
//| Init new bar                                                     |
//+------------------------------------------------------------------+
void InitNewBar(int index)
  {
   spread_open[index] = GetSpread();
   spread_high[index] = 0.0;
   spread_low[index]=DBL_MAX;
   avrg_current=0.0;
   count=0;
  }

Попробуем запустить этот индикатор на минутном графике Si-6.15. Через некоторое время после запуска он показал нам следующие результаты:

Рис. 17. Индикатор SpreadRecord, запущенный на минутном графике Si-6.15

Для Si-6.15 спред за анализируемый период изменялся с 1 до 21 пункта. В течение каждой минуты был по крайней мере один момент, когда размер спреда соответствовал минимальному значению 1 пункт. Среднее же значение спреда составляло 3 пункта. Напомню, что оно показывается в виде зеленой точки на графике индикатора.


2.4. Ручное и автоматическое ограничение торговли в моменты сильного расширения спреда

Теперь нам необходимо научиться использовать этот индикатор для контролирования своих рисков. Самое простое, что можно сделать — это ограничить свои торговые действия в момент, когда текущие значения индикатора слишком высоки. Для выбранного периода большую часть времени значения индикатора находились в пределах от 1 до 9 пунктов. Эту зону условно можно назвать "зеленой". В ней торговля будет разрешена. Если значение спреда поднимется выше 9 пунктов, мы перейдем в красную зону, в которой торговля будет запрещена. Схематично на графике это будет изображено в следующем виде:

Рис. 18. Зоны разрешенной и запрещенной торговли, определяемые индикатором

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

//+------------------------------------------------------------------+
//|                                          SpreadRecordControl.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
#define OPEN  0
#define HIGH  1
#define LOW   2
#define CLOSE 3
#define AVRG  4

input int MaxSpread=9;

int h_spread_record=INVALID_HANDLE;       // Handle of SpreadRecord indicator
bool print_disable = false;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   h_spread_record=iCustom(Symbol(),Period(),"Spread Record");
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinit function                                           |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   IndicatorRelease(h_spread_record);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   if(IsTradeDisable(MaxSpread))return;
   //
   // TRADE LOGIC...
   //
  }
//+------------------------------------------------------------------+
//| Return true if trade disable, otherwise return false             |
//+------------------------------------------------------------------+
bool IsTradeDisable(int max_spread)
  {
   if(h_spread_record==INVALID_HANDLE)
      return false;
   double close[];
   if(CopyBuffer(h_spread_record, CLOSE, 0, 1, close) < 1)return false;
   if(close[0]>MaxSpread)
     {
      if(!print_disable)
         printf("trade disable");
      print_disable=true;
      return true;
     }
   if(print_disable)
      printf("trade enable");
   print_disable=false;
   return false;
  }

Основную работу по определению того, разрешена торговля или нет, выполняет функция IsTradeDisable. Она возвращает true, если спред слишком высокий и торговля должна быть запрещена. Если спред находится на нормальном уровне, функция возвращает false. Основу функции составляет вызов индикатора SpreadRecord через копирование его текущего значения с помощью функции CopyBuffer. В эксперте присутствует параметр MaxSpread, который равен пороговому значению, при превышении которого эксперт блокирует свои торговые действия. Если спред снова опускается ниже указанной границы, эксперт вновь начинает свою работу. Функция IsTradeDisable сигнализирует о переходе из одного состояния в другое соответствующей надписью: "trade enable" и "trade disable":

2015.05.27 16:57:08.238 SpreadRecordControl (Si-6.15,H1)        trade enable
2015.05.27 16:57:08.218 SpreadRecordControl (Si-6.15,H1)        trade disable
2015.05.27 16:56:49.411 SpreadRecordControl (Si-6.15,H1)        trade enable
2015.05.27 16:56:49.401 SpreadRecordControl (Si-6.15,H1)        trade disable
2015.05.27 16:56:36.478 SpreadRecordControl (Si-6.15,H1)        trade enable
2015.05.27 16:56:36.452 SpreadRecordControl (Si-6.15,H1)        trade disable

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

Исходный код эксперта и индикатора SpreadRecord вы найдете в приложении к данной статье.


Глава 3. Безопасные режимы торговли и тестирования советников


3.1. "Спящий режим" как альтернатива потиковому контролю

Как было сказано в разделе 1.1 "Дискретность потоковых цен. Ценовые разрывы", биржевые цены можно представить как непрерывный поток последовательно меняющих друг друга цен. В таком представлении, например, изменение цены акции с $10 до $15 означает, что в промежуточные моменты времени были цены 11, 12, 13 и 14 долларов соответственно. Однако в том же разделе статьи мы выяснили, что это не всегда бывает верным.

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

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

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

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

Проиллюстрируем данный подход на реальной ситуации, возникшей 28 мая на фьючерсном контракте рубль-доллар Si-6.15. В 10:03 по московскому времени произошел достаточно сильный ценовой выброс. Предположим, что к этому моменту у нас была открыта длинная позиция по цене 53 040 с защитной остановкой по цене 52 740 (300 пунктов). В этом случае ценовой выброс заставил бы сработать нашу защитную остановку, при этом цена срабатывания была бы гораздо ниже заявленной в ней цены.

Практика показывает, что исполнение стоп-заявок в таких случаях происходит по ценам, близким к наихудшим. Наихудшей ценой в этом случае была цена 52 493. Таким образом, наш реальный убыток от срабатывания стоп-лосса был бы не 300 пунктов или 300 рублей на контракт, а 53 040 - 52493 = 547 рублей на один контракт. На рисунке ниже эта ситуация изображена на графике A. Если бы мы проверяли наш стоп раз в минуту, то подобная ценовая шпилька прошла бы для нашей стратегии незамеченной. Стратегия не стала бы исполнять наш ценовой стоп, и в дальнейшем, мы бы смогли закрыть нашу позицию с прибылью. Такая ситуация изображена на графике B:

Рис. 20. Различное поведение стратегии в случае использования реальной и виртуальной стоп-заявки

Рис. 20. Различное поведение стратегии в случае использования реальной и виртуальной стоп-заявки

Ценовая шпилька, показанная в данном примере, достаточно маленькая. Ценовые выбросы могут достигать значительно больших значений. Известны случаи, когда подобные выбросы достигали ценовых лимитов фьючерсного контракта. Как правило, ценовые лимиты расположены на расстоянии 5% от текущей цены. Это значит, что в случае занятия позиции с плечом 1:1 можно потерять на исполнении своей стоп-заявки 5% депозита своего счета. При маржинальной торговле с плечом 1:10 потери на счете от исполнения заявки составят 50% счета!


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

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

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

Рис. 21. Сигналы на вход в длинную и короткую позицию, для стратегии MovingAverage

Рис. 21. Сигналы на вход в длинную и короткую позицию для стратегии MovingAverage

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

Исходный код эксперта MovingAverage представлен ниже:

//+------------------------------------------------------------------+
//|                                                MovingAverage.mq5 |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#property version   "1.00"
#include <Trade\Trade.mqh>

input int FastMAPeriod = 10;     // Fast MA period
input int SlowMAPeriod = 20;     // Slow MA period
input double Volume = 1.0;       // Volume for Trade
int FastMA = INVALID_HANDLE;     // Handle of fast MA indicator.
int SlowMA = INVALID_HANDLE;     // Handle of slow MA indicator.
datetime TimeLastBar;
CTrade Trade;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   FastMA = iMA(Symbol(), Period(), FastMAPeriod, MODE_SMA, 1, PRICE_CLOSE);
   SlowMA = iMA(Symbol(), Period(), SlowMAPeriod, MODE_SMA, 1, PRICE_CLOSE);
   if(FastMA==POINTER_INVALID || SlowMA==POINTER_INVALID)
     {
      printf("handle of indicator has not been created");
      return(INIT_FAILED);
     }
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   IndicatorRelease(FastMA);
   IndicatorRelease(SlowMA);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   if(!NewBarDetect())return;
   if(CrossOver())
      Trade.Buy(GetVolume());
   else if(CrossUnder())
      Trade.Sell(GetVolume());
  }
//+------------------------------------------------------------------+
//| Return true if fast ma cross slow ma over. Otherwise return      |
//| false.                                                           |
//+------------------------------------------------------------------+
bool CrossOver()
  {
   double fast_ma[];
   double slow_ma[];
   if(CopyBuffer(FastMA, 0, 1, 2, fast_ma) < 1)return false;
   if(CopyBuffer(SlowMA, 0, 1, 2, slow_ma) < 1)return false;
   bool is_over=fast_ma[1]>slow_ma[1] && fast_ma[0]<slow_ma[0];
   return is_over;
  }
//+------------------------------------------------------------------+
//| Return true if fast ma cross slow ma under. Otherwise return      |
//| false.                                                           |
//+------------------------------------------------------------------+
bool CrossUnder()
  {
   double fast_ma[];
   double slow_ma[];
   if(CopyBuffer(FastMA, 0, 1, 2, fast_ma) < 1)return false;
   if(CopyBuffer(SlowMA, 0, 1, 2, slow_ma) < 1)return false;
   bool is_under=fast_ma[0]>slow_ma[0] && fast_ma[1]<slow_ma[1];
   return is_under;
  }
//+------------------------------------------------------------------+
//| Return count volume for trade/                                   |
//+------------------------------------------------------------------+
double GetVolume()
  {
   if(PositionSelect(Symbol()))return Volume*2.0;
   return Volume;
  }
//+------------------------------------------------------------------+
//| Return true if new bar detect, otherwise return false.           |
//+------------------------------------------------------------------+
bool NewBarDetect()
  {
   datetime times[];
   if(CopyTime(Symbol(),Period(),0,1,times)<1)
      return false;
   if(times[0] == TimeLastBar)return false;
   TimeLastBar = times[0];
   return true;
  }
//+------------------------------------------------------------------+

 Основной особенностью данного советника является условия проверки наступления нового бара:

void OnTick()
{
   if(!NewBarDetect())return;
   ...
}

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

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


3.2. Тестирование советников в режиме сформировавшихся баров как альтернатива потиковому тестированию

В заключение опишем один из наиболее интересных режимов тестирования советников и индикаторов в тестере стратегий MetaTrader 5. Этот режим называется "Только цены открытия". Чтобы воспользоваться им, необходимо запустить тестер стратегий, выбрав в меню терминала команду "Вид" --> "Тестер стратегий" и в появившемся окне тестера в опции "Режим торговли" выбрать одноименный режим.

Рис. 22. Выбор режима тестирования "Только цены открытия"

Рис. 22. Выбор режима тестирования "Только цены открытия"

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

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

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

Потиковый режим тестирования представляет наибольшую угрозу для стратегий, основанных на пробитии какого-либо уровня. Более того, исказить фактический результат в равной степени могут и отложенные ордера. Рассмотрим стратегию, которая выставляет отложенный ордер Buy Stop и ждет сильного движения наверх. 25 мая 2015 года в 19:00, сразу после вечернего клиринга, фьючерсный контракт SBRF-6.15 в течение одной минуты совершил сильное направленное движение наверх с 7 473 до 7 530 рублей. Если бы у нас был отложенный ордер, установленный на уровень 7 485, он бы сработал в тестере стратегий по своей цене и в итоге принес бы нам прибыль, если бы мы закрыли позицию через несколько баров:

Рис. 23. Схема срабатывания отложенного ордера

Однако в реальности картина может быть совершенно другой. Мы не знаем, какие цены были внутри минутного бара. Фактическое исполнение ордера может произойти по гораздо худшей для нас цене. Давайте посмотрим, как происходит исполнение ордера в режиме тестирования на каждом тике, в видеоролике ниже:

 

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

Рис. 24. Тиковый график минутной свечи

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

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

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

Более безопасным режимом тестирования является режим тестирования на сформировавшихся барах. Для достижения точности стратегии в этом режиме не должны использовать отложенные ордера. Если бы нашей стратегии требовалось бы зайти в рынок по цене 7 495, она должна была бы проверять цену открытия каждого нового бара, и в случае, если бы цена превысила нужный уровень, совершить покупку по текущей цене в момент открытия. В режиме сформировавшихся баров мы могли бы узнать, что цена находится выше нужной нам цены лишь в момент открытия нового бара в 19:01, потому что цена открытия бара 19:00 была еще ниже 7 495 рублей. Наша сделка в режиме сформировавшихся баров на графике выглядела бы следующим образом:

 

Рис. 25. Фактическая сделка в режиме сформировавшихся баров

И хотя конкретно эта сделка была закрыта с отрицательным результатом, она обладает неоспоримым преимуществом:

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

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

Режим тестирования "Все тики" нельзя применять для стратегий, работающих на малоликвидных рынках. Также такие стратегии для своей торговли не должны использовать Stop-ордера.

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

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


Заключение

В заключение сформулируем основные тезисы, которые можно вывести из изложенного материала: