Фильтр на основании истории торговли

Andrey Khatimlianskii | 29 ноября, 2006


Введение


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

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

Именно этому и посвящена данная статья.

Подопытная стратегия


Для реализации нашей системы возьмем советник CrossMACD_DeLuxe.mq4 и внесем в него несколько изменений:
  • при открытии/модификации/закрытии каждой позиции будем записывать изменения в массив виртуальных позиций;
  • добавим отслеживание срабатывания СтопЛосс и ТейкПрофит виртуальных позиций;
  • добавим критерий фильтрации - условие, при выполнении которого реальные сделки открываться не будут.
Я постараюсь описать каждый шаг доработки эксперта максимально подробно. Но если вам не интересно, вы можете просто скачать готового эксперта и переходить к главе "Игра не стоит свеч?".

Учет виртуальных позиций


Итак, появился сигнал на открытие позиции. Просчитаны параметры СтопЛосс и ТейкПрофит, всё готово к вызову функции OrderSend(). Именно в этот момент мы и откроем виртуальную сделку - просто сохраним необходимые параметры позиции в соответствующие переменные:
void OpenBuy()
  {
    int _GetLastError = 0;
    double _OpenPriceLevel, _StopLossLevel, _TakeProfitLevel;
    _OpenPriceLevel = NormalizeDouble(Ask, Digits);
 
    if(StopLoss > 0)
        _StopLossLevel = NormalizeDouble(_OpenPriceLevel - 
                                     StopLoss*Point, Digits); 
    else
        _StopLossLevel = 0.0; 
    if(TakeProfit > 0)
        _TakeProfitLevel = NormalizeDouble(_OpenPriceLevel + 
                                   TakeProfit*Point, Digits); 
    else
        _TakeProfitLevel = 0.0; 
 
    //---- открываем виртуальную позицию
    virtOrderSend(OP_BUY, _OpenPriceLevel, _StopLossLevel, 
                  _TakeProfitLevel);
 
    if(OrderSend(Symbol(), OP_BUY, 0.1, _OpenPriceLevel, 3, 
       _StopLossLevel, _TakeProfitLevel, "CrossMACD", 
       _MagicNumber, 0, Green) < 0)
      {
        _GetLastError = GetLastError();
        Alert("Ошибка OrderSend № ", _GetLastError);
        return(-1);
      }
  }
 
//---- Сохраняем параметры открываемой позиции в гл. переменные
void virtualOrderSend(int type, double openprice, double stoploss,
                      double takeprofit)
  {
    virtTicket = 1;
    virtType = type;
    virtOpenPrice = openprice;
    virtStopLoss = stoploss;
    virtTakeProfit = takeprofit;
  }

Как видите, мы используем всего пять переменных:
int       virtTicket     = 0;   
// определяет, есть ли открытая виртуальная позиция
int       virtType       = 0;   // тип позиции
double    virtOpenPrice  = 0.0; // цена открытия позиции
double    virtStopLoss   = 0.0; // СтопЛосс позиции
double    virtTakeProfit = 0.0; // ТейкПрофит позиции

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

Для отслеживания закрытия и модификации позиций придется поработать чуть больше. Скопируем блок контроля открытых позиций, который есть в эксперте и заменим там характеристики реальных ордеров на виртуальные:
int start()
  {
    // skipped...
 
    //+------------------------------------------------------------------+
    //| Блок контроля "виртуальных" позиций                              |
    //+------------------------------------------------------------------+
    if(virtTicket > 0)
      {
        //---- если открыта БАЙ-позиция,
        if(virtType == OP_BUY)
          {
            //---- если МАКД пересек 0-ю линию вниз,
            if(NormalizeDouble(MACD_1 + CloseLuft*Point*0.1, 
               Digits + 1) <= 0.0)
              {
                //---- закрываем позицию
                virtOrderClose(Bid);
              }
            //---- если сигнал не изменился, сопровождаем позицию 
            //     ТрейлингСтопом
            else
              {
                if(TrailingStop > 0)
                  {
                    if(NormalizeDouble(Bid - virtOpenPrice, 
                       Digits ) > 0.0)
                      {
                        if(NormalizeDouble( Bid - TrailingStop*Point - 
                           virtStopLoss, Digits) > 0.0 || virtStopLoss < Point)
                        {
                          virtStopLoss = Bid - TrailingStop*Point;
                        }
                      }
                  }
              }
          }
        //---- если открыта СЕЛЛ-позиция,
        if(virtType == OP_SELL)
          {
            //---- если МАКД пересек 0-ю линию вверх,
            if(NormalizeDouble(MACD_1 - CloseLuft*Point*0.1, 
               Digits + 1 ) >= 0.0)
              {
                //---- закрываем позицию
                virtOrderClose(Ask);
              }
            //---- если сигнал не изменился, сопровождаем позицию 
            //     ТрейлингСтопом
            else
              {
                if ( TrailingStop > 0 )
                  {
                    if(NormalizeDouble( virtOpenPrice - Ask, 
                       Digits ) > 0.0 )
                      {
                        if(NormalizeDouble( virtStopLoss - ( Ask + 
                           TrailingStop*Point ), Digits ) > 0.0 ||
                           virtStopLoss <= Point )
                          {
                            virtStopLoss = Ask + TrailingStop*Point;
                          }
                      }
                  }
              }
          }
      }
    // skipped...
    return(0);
  } 
 
 
//---- Функция закрытия виртуальной позиции
void virtOrderClose(double closeprice)
  {
    //---- Сохраняем параметры закрываемой позиции в массив
    ArrayResize(virtClosedOrders, virtClosedOrdersCount + 1);
 
    virtClosedOrders[virtClosedOrdersCount][0] = virtType;
    virtClosedOrders[virtClosedOrdersCount][1] = virtOpenPrice;
    virtClosedOrders[virtClosedOrdersCount][2] = virtStopLoss;
    virtClosedOrders[virtClosedOrdersCount][3] = virtTakeProfit;
    virtClosedOrders[virtClosedOrdersCount][4] = closeprice;
 
    virtClosedOrdersCount ++;
 
    //---- очищаем переменные
    virtTicket = 0;
    virtType = 0;
    virtOpenPrice = 0.0;
    virtStopLoss = 0.0;
    virtTakeProfit = 0.0;
  }
Как видите, модификация стала простым присваиванием нового значения переменной virtStopLoss. А вот закрытие стало достаточно сложным - все характеристики закрываемого ордера сохраняются в массив. В последствии в нем будет храниться вся виртуальная история сделок. Из нее мы будем брать информацию о закрытых сделках для принятия решения, открывать ли новую позицию.

Теперь нам необходимо обработать закрытие позиций по СтопЛоссу и ТейкПрофиту. Для этого в только что созданный блок управления виртуальными позициями добавим несколько строк:
if(virtType == OP_BUY)
  {
    //---- проверяем, не сработал ли СЛ
    if(virtStopLoss > 0.0 && NormalizeDouble(virtStopLoss - Bid, 
       Digits ) >= 0.0)
      {
        virtOrderClose(virtStopLoss);
      }
    //---- проверяем, не сработал ли ТП
    if(virtTakeProfit > 0.0 && NormalizeDouble( Bid - virtTakeProfit, 
       Digits ) >= 0.0)
      {
        virtOrderClose(virtTakeProfit);
      }
  }

Теперь наша виртуальная история счета готова и мы можем приступать к введению критерия фильтрации.

Что такое "хорошо", а что такое "плохо"?


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

Для проверки работоспособности каждого из них попробуем закодировать все три условия и протестировать на истории:
extern int TradeFiltrVariant = 0;
 
//---- Функция проверки необходимости реальной торговли
bool virtCheckCondition()
  {
    int pos, check_pos = 2;
    double last_profit = 0.0, pre_last_profit = 0.0;
    
    //---- в зависимости от значения TradeFiltrVariant:
    switch(TradeFiltrVariant)
      {
        //---- 1: запрещаем реальную торговлю, если 2 последние сделки убыточные
        case 1:
          {
            //---- если в виртуальной истории есть достаточное количество ордеров,
            if(virtClosedOrdersCount >= check_pos)
              {
                for(pos = 1; pos check_pos; pos ++)
                  {
                    //---- если сделка прибыльная, возвращаем true
                    if((virtClosedOrders[virtClosedOrdersCount-pos][0] == 0 && 
                        virtClosedOrders[virtClosedOrdersCount-pos][4] - 
                        virtClosedOrders[virtClosedOrdersCount-pos][1] >= 0.0) ||
                        (virtClosedOrders[virtClosedOrdersCount-pos][0] == 1 && 
                        virtClosedOrders[virtClosedOrdersCount-pos][1] - 
                        virtClosedOrders[virtClosedOrdersCount-pos][4] >= 0.0))
                      {
                        return(true);
                      }
                    }
              }
            return(false);
          }
        //---- 2: запрещаем реальную торговлю, если последняя позиция закрылась 
        //        по СтопЛоссу
        case 2:
          {
            //---- если в виртуальной истории есть достаточное количество ордеров,
            if(virtClosedOrdersCount > 0)
              {
                //---- если цена закрытия последнего ордера равна стоплоссу,
                if(virtClosedOrders[virtClosedOrdersCount-1][2] - 
                   virtClosedOrders[virtClosedOrdersCount-1][4] < Point &&
                   virtClosedOrders[virtClosedOrdersCount-1][4] - 
                   virtClosedOrders[virtClosedOrdersCount-1][2] < Point)
                  {
                    return(false);
                  }
              }
            return(true);
          }
        //---- 3: запрещаем реальную торговлю, если прибыль последней позиции  
        //----    меньше, чем у предпоследней (или убыток больше)
        case 3:
          {
            if(virtClosedOrdersCount >= 2)
              {
                if(virtClosedOrders[virtClosedOrdersCount-1][0] == 0)
                  {
                    last_profit =  virtClosedOrders[virtClosedOrdersCount-1][4] - 
                                   virtClosedOrders[virtClosedOrdersCount-1][1];
                  }
                else
                  {
                    last_profit =  virtClosedOrders[virtClosedOrdersCount-1][1] - 
                                   virtClosedOrders[virtClosedOrdersCount-1][4];
                  }
                if(virtClosedOrders[virtClosedOrdersCount-2][0] == 0)
                  {
                    pre_last_profit = virtClosedOrders[virtClosedOrdersCount-2][4] - 
                                      virtClosedOrders[virtClosedOrdersCount-2][1];
                  }
                else
                  {
                    pre_last_profit = virtClosedOrders[virtClosedOrdersCount-2][1] - 
                                      virtClosedOrders[virtClosedOrdersCount-2][4];
                  }
 
                if(pre_last_profit - last_profit > 0.0)
                  {
                    return(false);
                  }
              }
            return(true);
          }
        //---- по умолчанию фильтр выключен, т.е. позиция всегда будет открываться в реале
        default: return(true);
      }
    return(true);
  }
 
void OpenBuy()
  {
    int _GetLastError = 0;
    double _OpenPriceLevel, _StopLossLevel, _TakeProfitLevel;
    _OpenPriceLevel = NormalizeDouble(Ask, Digits);
 
    if(StopLoss > 0)
      { 
        _StopLossLevel = NormalizeDouble(_OpenPriceLevel - StopLoss*Point, Digits); 
      }
    else
      { 
        _StopLossLevel = 0.0; 
      }
 
    if(TakeProfit > 0)
      { 
        _TakeProfitLevel = NormalizeDouble(_OpenPriceLevel + TakeProfit*Point, Digits); 
      }
    else
      { 
        _TakeProfitLevel = 0.0; 
      }
 
    //---- открываем виртуальную позицию
    virtOrderSend(OP_BUY, _OpenPriceLevel, _StopLossLevel, _TakeProfitLevel);
 
    //---- если фильтр виртуальных позиций торговлю запрещает, выходим
    if(virtCheckCondition() == false) 
      { 
        return(0); 
      }
 
    if(OrderSend( Symbol(), OP_BUY, 0.1, _OpenPriceLevel, 3, _StopLossLevel, 
          _TakeProfitLevel, "CrossMACD", _MagicNumber, 0, Green) < 0 )
      {
        _GetLastError = GetLastError();
        Alert("Ошибка OrderSend № ", _GetLastError);
        return(-1);
      }
  }
Как видите, у нас появилась внешняя переменная TradeFiltrVariant. Она отвечает за то, какой критерий фильтрации будет выбран:
extern int TradeFiltrVariant = 0;
//---- 0: фильтр выключен, т.е. позиция всегда будет открываться в реале
//---- 1: запрещаем реальную торговлю, если 2 последние сделки убыточные
//---- 2: запрещаем реальную торговлю, если последняя позиция закрылась по СтопЛоссу
//---- 3: запрещаем реальную торговлю, если прибыль последней позиции меньше, 
//----    чем у предпоследней (или убыток больше)
Теперь протестируем эксперта с разными фильтрами и сравним результаты.

Игра не стоит свеч?


Для тестов я выбрал следующие параметры:
Символ: GBPUSD
Период: Н4, 01.01.2005 - 01.01.2006
Режим моделирования: все тики (качество моделирования 90%, котировки HistoryCenter)

Параметры эксперта:
StopLoss: 50
TakeProfit: 0 (отключен)
TrailingStop: 0 (отключен)
FastEMAPeriod: 12
SlowEMAPeriod: 26
OpenLuft: 10
CloseLuft: 0

Зависимость результатов от выбранного фильтра приведена в таблице:

TradeFiltrVariantОбщая прибыль/убыток
Всего сделок Прибильных сделок Убыточных сделок
0 1678.75
419 (22%)
32 (78%)
1105.6520
2 (10%)
18 (90%)
2 -550.20110 (0%)
11 (100%)
3 1225.13287 (25%)
21 (75%)









Как видите, предположение о полезности фильтра сделок пока не подтвердилось. Напротив, все результаты с его использованием хуже исходного. Единственным исключением стал третий вариант - у него больше процент прибыльных сделок (25 против 22), но общая прибыль меньше у всех вариантов.

Что же мы сделали неправильно? Наверное ошибочно выбрали критерий фильтрации. Давайте попробуем поменять все три наши фильтра на прямо противоположные, то есть отключать реальную торговлю если:
  • две последние сделки прибыльные;
  • последняя позиция прибыльная (аналогии срабатыванию СтопЛосс у нас нет, так как ТейкПрофит отключен);
  • или прибыль последней позиции больше, чем у предпоследней.
Чтоб не урезать советника, просто добавим ещё три значения TradeFiltrVariant - 4, 5 и 6:
//---- 4: запрещаем реальную торговлю, если 2 последние сделки прибыльные
//---- 5: запрещаем реальную торговлю, если последняя сделка прибыльная
//---- 6: запрещаем реальную торговлю, если прибыль последней позиции больше, 
//----    чем у предпоследней (или убыток меньше)
 
    //---- 4: запрещаем реальную торговлю, если 2 последние сделки прибыльные
    case 4:
      {
        if(virtClosedOrdersCount >= check_pos)
          {
            for(pos = 1; pos check_pos; pos ++)
              {
                //---- если сделка убыточная, возвращаем true
                if((virtClosedOrders[virtClosedOrdersCount-pos][0] == 0 && 
                   virtClosedOrders[virtClosedOrdersCount-pos][1] - 
                   virtClosedOrders[virtClosedOrdersCount-pos][4] > 0.0) ||
                   (virtClosedOrders[virtClosedOrdersCount-pos][0] == 1 && 
                   virtClosedOrders[virtClosedOrdersCount-pos][4] - 
                   virtClosedOrders[virtClosedOrdersCount-pos][1] > 0.0))
                  {
                    return(true);
                  }
              }
          }
        return(false);
      }
    //---- 5: запрещаем реальную торговлю, если последняя сделка прибыльная
    case 5:
      {
        if(virtClosedOrdersCount >= 1)
          {
            if(virtClosedOrders[virtClosedOrdersCount-1][0] == 0)
              {
                last_profit =  virtClosedOrders[virtClosedOrdersCount-1][4] - 
                               virtClosedOrders[virtClosedOrdersCount-1][1];
              }
            else
              {
                last_profit =  virtClosedOrders[virtClosedOrdersCount-1][1] - 
                               virtClosedOrders[virtClosedOrdersCount-1][4];
              }
 
            if(last_profit > 0.0)
              {
                return(false);
              }
          }
        return(true);
      }
    //---- 6: запрещаем реальную торговлю, если прибыль последней позиции больше, 
    //----    чем у предпоследней (или убыток меньше)
    case 6:
      {
        if(virtClosedOrdersCount >= 2)
          {
            if(virtClosedOrders[virtClosedOrdersCount-1][0] == 0) 
              {
                last_profit =  virtClosedOrders[virtClosedOrdersCount-1][4] - 
                               virtClosedOrders[virtClosedOrdersCount-1][1];
              }
            else
              {
                last_profit =  virtClosedOrders[virtClosedOrdersCount-1][1] - 
                               virtClosedOrders[virtClosedOrdersCount-1][4];
              }
            if(virtClosedOrders[virtClosedOrdersCount-2][0] == 0)
              {
                pre_last_profit = virtClosedOrders[virtClosedOrdersCount-2][4] - 
                                  virtClosedOrders[virtClosedOrdersCount-2][1];
              }
            else
              {
                pre_last_profit = virtClosedOrders[virtClosedOrdersCount-2][1] - 
                                  virtClosedOrders[virtClosedOrdersCount-2][4];
              }
 
            if(last_profit - pre_last_profit > 0.0)
              {
                return(false);
              }
          }
        return(true);
      }
Теперь протестируем три новых варианта и добавим их в нашу таблицу:

AdaptVariantОбщая прибыль/убыток
Всего сделок Прибильных сделок Убыточных сделок
0 1678.75
419 (22%)
32 (78%)
1105.65 202 (10%)
18 (90%)
2 -550.20110 (0%)
11 (100%)
3 1225.13 287 (25%)
21 (75%)
4
1779.24
399 (23%) 30 (77%)
5
2178.95
31
9 (29%)
22 (71%)
6
602.32
24
5 (21%)
19 (79%)














Шестой вариант отфильтровал половину сделок - как прибыльных, так и убыточных. Четвертый - отсеял две убыточные сделки, увеличив итоговую прибыль на 100.49$.

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

Так что надежда есть - даже такую простую и популярную стратегию можно улучшить!

Выводы


Думаю, что для реального улучшения системы таких простых фильтров недостаточно - необходимо провести намного более глубокие исследования и сделать новые выводы.

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

В этой статье я лишь коснулся темы фильтров и надеюсь, что дал стимул развивать эту тему дальше.