English Deutsch 日本語
preview
Количественный анализ на MQL5: реализуем перспективный алгоритм

Количественный анализ на MQL5: реализуем перспективный алгоритм

MetaTrader 5Торговые системы | 8 декабря 2023, 11:10
1 157 0
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Что такое количественный анализ на финансовом рынке

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

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

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


Как количественный анализ используется хэдж-фондами

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

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

Как еще крупнейшие участники рынка используют количественный анализ? 

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

Управление рисками — еще одна супер-важная область применения количественного анализа. Хедж-фонды используют модели для оценки и управления рисками в своих портфелях. Они оптимизируют структуру активов с учетом риска, чтобы минимизировать потенциальные убытки. Сюда можно привести много примеров - как оптимизации портфеля по портфельной теории Марковица (тогда мы пляшем от риска, чтобы отклонение портфеля не превысило потенциальную прибыль), так и управление риском по системе VaR, данная уникальная модель позволяет рассчитать просадку, которую мы не превысим с шансом в 99%.

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

В результате фонд применил усреднение, крайне агрессивно набрал сотое плечо, нагрузившись активами в долг, и попросту лопнул, хотя нобелевские лауреаты в штате компании говорили о невозможности такого исхода событий. Тогда одна модель количественного анализа VaR едва не погубила весь рынок США, а председателю ФРС Алану Гринспену пришлось срочно вызывать к себе директоров крупнейших банков США, чтобы они выкупили маржинальные позиции фонда, в противном случае продажа такого огромного навеса активов "в рынок" вызвала бы моментальное обнуление фондового рынка США и панику похуже Великой Депрессии.

Поэтому при применении количественного анализа и усреднения каких-либо показателей важно помнить о хвостах нормального распределения вероятностей. Колоколообразная кривая вероятностей в случае с финансовыми рынками имеет "хвосты", которые отражают события в виде разного рода черных лебедей. С одной стороны, они статистически крайне маловероятны, с другой стороны, их масштаб и сила, сила этих событий, уничтожает портфели инвесторов, портфели хэдж-фондов, ликвидирует маржинальные позиции, рушит рынки и меняет их в каждом новом цикле. Мы это видели в 1998, мы это видели в 2008, мы это увидели в 2020, в 2022, и еще увидим многократно.

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


Основа алгоритма: считаем волны движений

Основа нашей идеи впервые высказана трейдером Артемом Звездиным: он в своей практике считает величину волн движения цены, чтобы понять, насколько переоценен или же недооценен актив по отношению к самому себе. К примеру, мы считаем бычьи и медвежьи волны за последние 500-5000 баров, чтобы понять, сколько цена проходила в каждом своем маленьком цикле. Каждый цикл движения цены - это чьи-то позиции, чьи-то деньги, решения о покупке и продаже. Каждый новый цикл - это новое рождение и смерть рынка, и мы будем использовать идею анализа движений цен без откатов, от вершинки до низинки. Это отдельный набор участников, который действует примерно одинаково, поэтому ставим гипотезу, что и длина циклов будет всегда плюс-минус одинаковой. Считать среднее движение цены мы будем через индикатор ZigZag, который входит в штатную поставку любого терминала MetaTrader 5.

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

//+------------------------------------------------------------------+
//|                                          QuantAnalysisSample.mq5 |
//|                                                   Copyright 2023 |
//|                                                Evgeniy Koshtenko |
//+------------------------------------------------------------------+
#property copyright   "Copyright 2023, Evgeniy Koshtenko"
#property link        "https://www.mql5.com"
#property version     "0.90"
#property strict

#include <Trade\Trade.mqh>
#include <Graphics\Graphic.mqh>
#include <Math\Stat\Normal.mqh>
#include <Math\Stat\Math.mqh>
CTrade trade;
//--- Входы
input double Lots       = 0.1;      // лот
input double Risk       = 0.1;     // риск
input double Profit     = 0;     // профит
input int StopLoss      = 0;        // стоп ATR
input int TakeProfit    = 0;        // тейк ATR
input string Symbol1    = "EURUSD";
input int    Magic      = 777;    // магик
//--- iВходные параметры
input uint   InpDepth       =  120;   // ZigZag Depth
input uint   InpDeviation   =  50;    // ZigZag Deviation
input uint   InpBackstep    =  30;    // ZigZag Backstep
input uchar  InpPivotPoint  =  1;    // ZigZag pivot point
datetime t=0;
double last=0;
double countMovements;
double currentMovement;
// Глобальная переменная для хранения дескриптора индикатора
int zigzagHandle;

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   trade.SetExpertMagicNumber(Magic);
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert initialization function custom                            |
//+------------------------------------------------------------------+
int OnIniti(string symb)
  {// Загрузка индикатора ZigZag
   zigzagHandle = iCustom(symb, _Period, "ZigZag", InpDepth, InpDeviation, InpBackstep, InpPivotPoint);
   if (zigzagHandle == INVALID_HANDLE)
     {
      Print("Ошибка при загрузке индикатора ZigZag: ", GetLastError());
      return(INIT_FAILED);
     }
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   Comment("");
  }

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

//+------------------------------------------------------------------+
//|  Position Profit                                                 |
//+------------------------------------------------------------------+
double AllProfit(int type=-1)
  {
   double p=0;

    for(int i=PositionsTotal()-1; i>=0; i--)
     {
      if(PositionSelectByTicket(PositionGetTicket(i)))
        {
         if(PositionGetInteger(POSITION_MAGIC)==Magic)
           {
            if(PositionGetInteger(POSITION_TYPE)==type || type==-1)
               p+=PositionGetDouble(POSITION_PROFIT);
           }
        }
     }

   return(p);
  }
//+------------------------------------------------------------------+
//|   CloseAll                                                       |
//+------------------------------------------------------------------+
void CloseAll(int type=-1)
  {
   for(int i=PositionsTotal()-1; i>=0; i--)
     {
      if(PositionSelectByTicket(PositionGetTicket(i)))
        {
         if(PositionGetInteger(POSITION_MAGIC)==Magic)
           {
            if(PositionGetInteger(POSITION_TYPE)==type || type==-1)
               trade.PositionClose(PositionGetTicket(i));
           }
        }
     }
  }

Затем идут функция расчета лота и функция расчета количества открытых позиций:

//+------------------------------------------------------------------+
//|     CountTrades                                                  |
//+------------------------------------------------------------------+
int CountTrades(string symb)
  {
   int count=0;

   for(int i=PositionsTotal()-1; i>=0; i--)
     {
      if(PositionSelectByTicket(PositionGetTicket(i)))
        {
         if(PositionGetString(POSITION_SYMBOL)==symb)
           {
            count++;
           }
        }
     }
   return(count);
  }
//+------------------------------------------------------------------+
//|     Lot                                                          |
//+------------------------------------------------------------------+  
double Lot()
  {
   double lot=Lots;

   if(Risk>0)
      lot=AccountInfoDouble(ACCOUNT_BALANCE)*Risk/100000;

   return(NormalizeDouble(lot,2));
  }

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
double FindLastBuyPrice(string symb)
  {
   double pr=0;

   for(int i=PositionsTotal()-1; i>=0; i--)
     {
      if(PositionSelectByTicket(PositionGetTicket(i)) && PositionGetInteger(POSITION_TYPE)==0)
        {
         if(PositionGetString(POSITION_SYMBOL)==symb)
           {
            pr=PositionGetDouble(POSITION_PRICE_OPEN);
            break;
           }
        }
     }
   return(pr);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
double FindLastSellPrice(string symb)
  {
   double pr=0;

   for(int i=PositionsTotal()-1; i>=0; i--)
     {
      if(PositionSelectByTicket(PositionGetTicket(i)) && PositionGetInteger(POSITION_TYPE)==1)
        {
         if(PositionGetString(POSITION_SYMBOL)==symb)
           {
            pr=PositionGetDouble(POSITION_PRICE_OPEN);
            break;
           }
        }
     }
   return(pr);
  }
//+------------------------------------------------------------------+
//|  PositionType                                                    |
//+------------------------------------------------------------------+
int PositionType(string symb)
  {
   int type=8;

   for(int i=PositionsTotal()-1; i>=0; i--)
     {
      if(PositionSelectByTicket(PositionGetTicket(i)))
        {
         if(PositionGetString(POSITION_SYMBOL)==symb)
           {
            type=(int)PositionGetInteger(POSITION_TYPE);
            break;
           }
        }
     }
   return(type);
  }

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

//+------------------------------------------------------------------+
//|     CalculateAverageMovement                                     |
//+------------------------------------------------------------------+ 
void CalculateAverageMovement(string symb, double &averageMovement, double &currentMovement) {
    const int lookback = 500; // Количество баров для анализа
    double sumMovements = 0.0;
    int countMovements = 0;
    double lastExtremePrice = 0.0;
    double zigzagArray[500]; // Массив для хранения значений ZigZag
    OnIniti(symb);
    // Копируем значения ZigZag в массив
    if (CopyBuffer(zigzagHandle, 0, 0, lookback, zigzagArray) <= 0) {
        Print("Ошибка при копировании данных индикатора");
        averageMovement = -1;
        currentMovement = -1;
        return;
    }

    // Копируем значения ZigZag в массив
    if (CopyBuffer(zigzagHandle, 0, 0, lookback, zigzagArray) <= 0) {
        Print("Ошибка при копировании данных индикатора");
        averageMovement = -1;
        currentMovement = -1;
        return;
    }

    for (int i = 0; i < lookback; i++) {
        if (zigzagArray[i] != 0 && zigzagArray[i] != lastExtremePrice) {
            if (lastExtremePrice != 0) {
                // Определяем направление движения
                double movement = zigzagArray[i] - lastExtremePrice;
                sumMovements += movement;
                countMovements++;
            }
            lastExtremePrice = zigzagArray[i];
        }
    }

    // Вычисляем текущее движение
    double lastMovement = iClose(symb, _Period, 0) - lastExtremePrice;
    currentMovement = lastMovement;

    // Вычисляем среднее движение
    averageMovement = countMovements > 0 ? sumMovements / countMovements : 0.0;

    // Выводим результаты
    Print("Среднее движение: ", averageMovement);
    Print("Текущее движение: ", currentMovement);

    // Освобождаем ресурсы
    IndicatorRelease(zigzagHandle);
}

Ну и еще одна из ключевых наших функций - непосредственно функция мультивалютной торговли по сигналам превышения текущим движением цены своего среднего значения. Тейки и стопы у нас в ATR, шаг сетки (усреднения) - у нас также в величинах ATR. Сделки открываются на новых барах - это для нас важно, чтобы эксперт "не рисовал как Репин" тесты в тестере. Данная функция затем вызывается в OnTick и работает либо на одном, либо на нескольких символах. Успешно запустить работу на нескольких символах у меня пока не получается, как уже сказал, я буду применять только один символ, на котором запускается робот - его нужно также прописывать в настройках эксперта. 

//+------------------------------------------------------------------+
//| Expert Trade unction                                             |
//+------------------------------------------------------------------+
void Trade(string symb)
  {
   double averageMovement = 0;
   double currentMovement = 0;
   double pr=0,sl=0,tp=0,hi=0,lo=0;
// Вызов функции для расчета
   CalculateAverageMovement(symb, averageMovement, currentMovement);

// Использование результатов
   double Ask = SymbolInfoDouble(symb, SYMBOL_ASK);
   double Bid = SymbolInfoDouble(symb, SYMBOL_BID);
   int dg=(int)SymbolInfoInteger(symb,SYMBOL_DIGITS);
   double pp=SymbolInfoDouble(symb,SYMBOL_POINT);
  
   double atr = iATR(symb, PERIOD_CURRENT, 3);
     
// Здесь определите вашу логику для покупки и продажи
   bool sell  = currentMovement > -averageMovement; // условие для покупки
   bool buy = -currentMovement > averageMovement; // условие для продажи
  
   if(AllProfit()>Profit && Profit>0)
      CloseAll();

   if(t!=iTime(symb,PERIOD_CURRENT,0))
     {
      if(buy && CountTrades(symb)<1)
        {
         if(StopLoss>0)
            sl=NormalizeDouble(Bid-(atr*StopLoss)*Point(),_Digits);
         if(TakeProfit>0)
            tp=NormalizeDouble(Bid+(atr*TakeProfit)*Point(),_Digits);
         pr=NormalizeDouble(Bid,dg);
         trade.Buy(Lot(),symb,pr,sl,tp,"");
         last=pr;
        }
      if(sell && CountTrades(symb)<1)
        {
         if(StopLoss>0)
            sl=NormalizeDouble(Ask+(atr*StopLoss)*Point(),_Digits);
         if(TakeProfit>0)
            tp=NormalizeDouble(Ask-(atr*TakeProfit)*Point(),_Digits);
         pr=NormalizeDouble(Ask,dg);
         trade.Sell(Lot(),symb,Ask,sl,tp,"");
         last=pr;
        }
      if(CountTrades(symb)>0)
        {
         if(PositionType(symb)==0 && (FindLastBuyPrice(symb)-Ask)/pp>=atr*30)
           {
            if(StopLoss>0)
               sl=NormalizeDouble(Bid-(atr*StopLoss)*Point(),_Digits);
            if(TakeProfit>0)
               tp=NormalizeDouble(Bid+(atr*TakeProfit)*Point(),_Digits);
            trade.Buy(Lot(),symb,Ask,sl,tp);
           }
         if(PositionType(symb)==1 && (Bid-FindLastSellPrice(symb))/pp>=atr*30)
           {
            if(StopLoss>0)
               sl=NormalizeDouble(Ask+(atr*StopLoss)*Point(),_Digits);
            if(TakeProfit>0)
               tp=NormalizeDouble(Ask-(atr*TakeProfit)*Point(),_Digits);
            trade.Sell(Lot(),symb,Bid,sl,tp);
           }
        }
      t=iTime(symb,0,0);
     }
  }


Тестируем модель в MQL5

Итак, пришло время для самого интересного - тестирования нашей модели на реальном рынке. Сразу оговорюсь, что расчет через цикл довольно грузит процессор, поэтому целесообразнее нам запускать советник только на ценах открытия. Запустим одиночный тест на EUR/USD по ценам открытия таймфрейма H1, с 1 января 2020 по сегодняшнюю дату, 6 декабря 2023 года:


Одиночный тест прибыльный, но высоковата просадка. Брать на себя дополнительный риск при торговле мало кому интересно. Но тут мы вспоминаем о том, что у нас есть еще закрытие по прибыли. И мы можем провести тест на Netting счете

Чтобы запустить тест с закрытием по прибыли, ставим закрытие по прибыли выше 0. Пробуем тестировать. Возможно, мы получим стабильный тест. Запускаем советник на том же активе по ценам открытия. Тип счета у нас Hedging. И вот что мы видим:


Советник получился крайне рискованным, и всему виной усреднение. Попробуем прогнать тот же тест на Netting счете.


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

if (CloseSig)
   {
      if (buy)
         CloseAll(1);
      if (sell)
         CloseAll(0);
   }

И такую настройку:

input bool CloseSig     = 1;        // закрытие по сигналу

Проводим тестирование, и все вновь печально:


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


Заключение

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

Прикрепленные файлы |
Нейросети — это просто (Часть 67): Использование прошлого опыта для решения новых задач Нейросети — это просто (Часть 67): Использование прошлого опыта для решения новых задач
В данной статье мы продолжим разговор о методах сбора данных в обучающую выборку. Очевидно, что в процессе обучения необходимо постоянное взаимодействие с окружающей средой. Но ситуации бывают разные.
Цветные буферы в мультисимвольных мультипериодных индикаторах Цветные буферы в мультисимвольных мультипериодных индикаторах
В статье пересмотрим структуру индикаторного буфера в мультисимвольных мультипериодных индикаторах и организуем вывод на график цветных буферов этих индикаторов.
Популяционные алгоритмы оптимизации: Алгоритм имитации изотропного отжига (Simulated Isotropic Annealing, SIA). Часть II Популяционные алгоритмы оптимизации: Алгоритм имитации изотропного отжига (Simulated Isotropic Annealing, SIA). Часть II
Первая часть статьи была посвящена известному и популярному алгоритму - имитации отжига, были рассмотрены его достоинства и подробно описаны недостатки. Вторая часть статьи посвящена кардинальному преобразованию алгоритма, его перерождению в новый алгоритм оптимизации "имитации изотропного отжига, SIA".
Популяционные алгоритмы оптимизации: Алгоритм имитации отжига (Simulated Annealing, SA). Часть I Популяционные алгоритмы оптимизации: Алгоритм имитации отжига (Simulated Annealing, SA). Часть I
Алгоритм имитации отжига (Simulated Annealing) является метаэвристикой, вдохновленной процессом отжига металлов. В нашей статье проведем тщательный анализ алгоритма и покажем, как многие распространенные представления и мифы, вокруг этого наиболее популярного и широко известного метода оптимизации, могут быть ошибочными и неполными. Анонс второй части статьи: "Встречайте собственный авторский алгоритм имитации изотропного отжига (Simulated Isotropic Annealing, SIA)!"