English
preview
Теория экстремальных значений в MQL5: Создание индикатора риска обвала с оценкой хвоста распределения — за рамками VaR по методу Монте-Карло

Теория экстремальных значений в MQL5: Создание индикатора риска обвала с оценкой хвоста распределения — за рамками VaR по методу Монте-Карло

MetaTrader 5 — Индикаторы |
121 0
Muhammad Minhas Qamar
Muhammad Minhas Qamar

Введение

Все типичные инструменты управления рисками в MetaTrader 5 основаны на одном скрытом допущении: максимальный убыток, к которому стоит готовиться, примерно соответствует максимальному убытку, с которым вы уже сталкивались. При использовании среднего истинного диапазона (ATR) размер позиций рассчитывается на основе недавней волатильности. Алгоритм повторной выборки по методу Монте-Карло перемешивает фактически совершенные вами сделки. Историческая стоимостная мера риска (Value at Risk, VaR) определяется по процентилю исторических данных о доходности. Все они, по сути, опираются на центральную часть распределения исторических данных в негласной надежде, что в будущем не произойдет ничего более экстремального, чем в прошлом.

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

Для этой задачи существует специальный раздел статистики, однако его отсутствие среди решений на MQL5 особенно заметно. Теория экстремальных значений (EVT) моделирует не все распределение, а только его хвост. Форма хвоста обосновывается предельной теоремой, а не принимается как допущение. Модель с подобранными параметрами позволяет экстраполировать убытки за пределы наблюдавшихся в выборке: оценить убыток при доверительном уровне 99,9%, то есть движение, происходящее примерно раз в три года, и получить значение, которого в вашей истории никогда не было.

В этой статье мы реализуем EVT непосредственно на языке MQL5. Мы реализуем метод превышений над порогом (POT) и с помощью оптимизатора ALGLIB подгоняем обобщенное распределение Парето (GPD) к левому хвосту распределения доходности — без внешних зависимостей и без связующего компонента для Python. На основе этого движка мы создаем индикатор риска обвала, который в реальном времени выводит прямо на графике стоимостную меру риска (VaR) на основе EVT, ожидаемые потери (Expected Shortfall, ES) и показатель тяжести хвоста распределения. Наконец, мы используем этот индикатор в небольшом советнике, который рассчитывает размер позиций по оценке хвоста распределения на основе EVT, и оцениваем результат в Тестере стратегий.

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

Мы рассмотрим следующие темы:

  1. Почему оценка риска при допущении нормального распределения недооценивает хвостовой риск
  2. Теория экстремальных значений — метод превышений над порогом и обобщенное распределение Парето (GPD)
  3. Создание движка — класс CGPDModel
  4. Оценка риска обвала в реальном времени — индикатор
  5. Применение на практике — надстройка для расчета размера позиции на основе EVT
  6. Ограничения и объективный вывод
  7. Заключение


Почему оценка риска при допущении нормального распределения недооценивает хвостовой риск

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

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

Normal distribution overlaid on a fat-tailed return histogram, tail gap shaded

Рис. 1. Нормальная модель (кривая) и реальное распределение доходности с толстыми хвостами (гистограмма); заштрихованный левый хвост отражает убытки, которые модель не учитывает.

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

Стандартные подходы эту проблему не решают. Повторная выборка из истории сделок, как при бутстрэпе по методу Монте-Карло, позволяет лишь перемешивать уже наблюдавшиеся убытки; этот метод не может создать более крупный убыток, которого не было в выборке. Корректировка расстояния до стоп-лосса по недавней волатильности учитывает центральную часть распределения, но ничего не говорит о тяжести его хвоста. Чтобы оценить убыток, которого нет в выборке, нам нужен метод обоснованной экстраполяции хвоста распределения. Именно такой метод предоставляет теория экстремальных значений (EVT).


Теория экстремальных значений — метод превышений над порогом и обобщенное распределение Парето (GPD)

Основная идея метода превышений над порогом (POT) заключается в том, чтобы отказаться от попыток моделировать все распределение доходности и сосредоточиться исключительно на его хвосте. Мы выбираем высокий порог u, отбрасываем все значения ниже него и анализируем только превышения — величины, на которые убытки превышают этот порог. Если убыток x превышает порог, величина превышения равна y = x - u.

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

P(X - u > y | X > u) = (1 + xi * y / sigma)^(-1/xi)

При стремлении xi к нулю это выражение стремится к exp(-y / sigma).

У GPD всего два параметра, и оба имеют понятную трейдеру интерпретацию:

  • Параметр формы xi. Он характеризует тяжесть хвоста. Если xi больше нуля, хвост тяжелый и имеет форму, характерную для распределения Парето: это режим толстых хвостов и обвалов. Чем больше xi, тем тяжелее хвост. Когда xi близок к нулю, хвост убывает экспоненциально. Рост xi — это самое прямое свидетельство нарастания хвостового риска.
  • Параметр масштаба sigma. Это строго положительный параметр разброса, который задает величину типичных превышений порога.

Threshold on a loss series, exceedances peeled off and fitted with a GPD

Рис. 2. Метод превышений над порогом: задайте порог u, соберите значения превышений и подгоните к ним обобщенное распределение Парето.

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

VaR_p = u + (sigma / xi) * [ (n / Nu * (1 - p))^(-xi) - 1 ]

Здесь Nu/n — частота превышений, то есть доля наблюдений, лежащих выше порога.

VaR отвечает на вопрос "насколько велик убыток на границе?", но ничего не говорит о том, что происходит за этой границей. Ожидаемые потери (Expected Shortfall, ES), также называемые условным VaR, восполняют этот пробел: это средний убыток при условии превышения уровня VaR, то есть ожидаемый размер убытка в день, который уже оказался неблагоприятным. Для GPD этот показатель выражается простой формулой в замкнутом виде:

ES_p = VaR_p / (1 - xi) + (sigma - xi * u) / (1 - xi)

Это справедливо при xi < 1.

Условие xi < 1 — не техническая деталь, которой можно пренебречь: когда xi достигает единицы, теоретическое среднее хвостового распределения становится бесконечным, а ES не определен. Это означает, что хвост распределения настолько тяжелый, что понятие "среднего убытка за пределами VaR" перестает иметь смысл. Наша реализация учитывает это ограничение, а не возвращает бессмысленное число.

Итак, весь хвост описывают три величины: VaR (величина убытка на границе), ES (средний убыток за ее пределами) и xi (текущая тяжесть хвоста). Далее в статье мы реализуем эти формулы в рабочем коде MQL5.


Создание движка — класс CGPDModel

Все расчеты EVT реализованы в одном подключаемом файле GPDModel.mqh, пригодном для повторного использования, чтобы индикатор и советник использовали одну и ту же реализацию. У движка одна задача: принять массив убытков, подогнать GPD к хвосту распределения и предоставить значения VaR, ES и оцененных параметров. Но сначала определяется, как движок будет сообщать о надежности своих оценок: оценка EVT, полученная по слишком малому числу наблюдений в хвосте, не просто бесполезна — она создает ложную уверенность.

//--- model status / data-sufficiency flag
enum ENUM_GPD_STATUS
  {
   GPD_OK,                 // fit succeeded and is usable
   GPD_NOT_FITTED,         // Fit() not called yet
   GPD_TOO_FEW_EXCEEDANCES,// not enough tail data for a trustworthy fit
   GPD_FIT_FAILED,         // optimizer did not converge to a valid solution
   GPD_BAD_INPUT           // empty / degenerate input
  };

//--- minimum exceedances below which a GPD fit is not trustworthy.
//--- EVT literature treats ~30-50 tail points as a practical floor.
#define GPD_MIN_EXCEEDANCES 30

Перечисление статусов — это механизм, с помощью которого движок честно сообщает о своем состоянии. Если результат подгонки не получает статус GPD_OK, функции расчета показателей риска возвращают ноль вместо выдуманного значения, а причина сообщается вызывающему коду. Минимальное число превышений GPD_MIN_EXCEEDANCES, равное 30, соответствует стандартной практике EVT: если в хвосте меньше примерно тридцати наблюдений, оценка двух параметров слишком нестабильна, чтобы ей доверять. Именно эта константа позволяет индикатору риска обвала в дальнейшем явно сообщать "недостаточно данных" вместо того, чтобы выводить недостоверную оценку как надежную.

Целевая функция правдоподобия

Два параметра GPD оцениваются методом максимального правдоподобия: мы минимизируем взятый с обратным знаком логарифм функции правдоподобия для превышений. Оптимизаторы ALGLIB обращаются к объекту целевой функции, заданному пользователем, поэтому мы реализуем его, создавая класс-наследник CNDimensional_Func. Этот объект хранит копию значений превышений и для пробного вектора параметров возвращает взятый с обратным знаком логарифм функции правдоподобия.

//+------------------------------------------------------------------+
//| GPD negative log-likelihood.                                     |
//|  For xi ~ 0 we use the exponential limit to stay numerically     |
//|  stable. Invalid parameter regions return a large penalty so the |
//|  optimizer is repelled rather than producing NaNs.               |
//+------------------------------------------------------------------+
void CGPDNegLogLik::Func(double &x[],double &func,CObject &obj)
  {
   double xi    = x[0];
   double sigma = x[1];
   const double BIG = 1.0e15;

   if(sigma<=0.0)
     {
      func=BIG;
      return;
     }

   double nll=0.0;
   for(int i=0;i<m_n;i++)
     {
      double y=m_excess[i];
      double z=y/sigma;
      if(MathAbs(xi)<1.0e-8)
        {
         //--- exponential limit: log f = -log(sigma) - y/sigma
         nll += MathLog(sigma) + z;
        }
      else
        {
         double t=1.0+xi*z;
         if(t<=0.0)
           {
            //--- excess outside the GPD support for this xi: reject
            func=BIG;
            return;
           }
         //--- log f = -log(sigma) - (1 + 1/xi) * log(1 + xi*z)
         nll += MathLog(sigma) + (1.0+1.0/xi)*MathLog(t);
        }
     }
   func=nll;
  }

Устойчивость реализации обеспечивают две защитные меры. Если xi численно неотличим от нуля, применение общей формулы привело бы к делению на него, поэтому мы используем экспоненциальный предельный случай GPD. Если при пробном значении xi наблюдаемое превышение оказывается вне носителя распределения, мы возвращаем большое штрафное значение. Это заставляет оптимизатор избегать данной области и не допускает появления NaN, которое нарушило бы процесс поиска.

Пайплайн подгонки модели

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

//--- sort losses ascending to locate the threshold quantile
   double sorted[];
   ArrayResize(sorted,count);
   for(int i=0;i<count;i++)
      sorted[i]=losses[i];
   ArraySort(sorted);

   m_u=Quantile(sorted,count,m_threshold_q);

//--- collect excesses over u (strictly positive)
   double excess[];
   int k=0;
   ArrayResize(excess,count);
   for(int i=0;i<count;i++)
     {
      double e=losses[i]-m_u;
      if(e>0.0)
         excess[k++]=e;
     }
   ArrayResize(excess,k);
   m_n_exceed=k;

   if(k<GPD_MIN_EXCEEDANCES)
     {
      m_status=GPD_TOO_FEW_EXCEEDANCES;
      return false;
     }

По умолчанию порог равен 95-му процентилю выборки убытков, поэтому мы моделируем превышения порога для пяти процентов наибольших убытков. Именно здесь проявляется ограничение по количеству данных: при пороге на уровне 95-го процентиля число превышений в окне из n баров составляет лишь около 5% от n, поэтому окно должно быть достаточно большим, чтобы обеспечить минимум тридцать превышений. Мы увидим, как это ограничение проявляется на практике, когда запустим индикатор.

Оценка методом максимального правдоподобия

После получения значений превышений RunMLE запускает оптимизатор ALGLIB. Мы используем MinBLEIC — оптимизатор с ограничениями на значения параметров — в варианте с конечными разностями. Это избавляет нас от необходимости выводить градиент функции правдоподобия и вручную реализовывать его расчет в коде: ALGLIB численно дифференцирует целевую функцию.

//--- box constraints: xi in [-0.5, 1.0], sigma in (eps, +inf)
   double bndl[];
   ArrayResize(bndl,2);
   double bndu[];
   ArrayResize(bndu,2);
   bndl[0]=-0.5;
   bndu[0]=1.0;
   bndl[1]=1.0e-12;
   bndu[1]=CInfOrNaN::PositiveInfinity();

   CMinBLEICStateShell  state;
   CMinBLEICReportShell rep;
   CGPDNegLogLik        objective;
   CNDimensional_Rep    repcb;
   CObject              dummy;

   objective.SetData(excess,n);

//--- finite-difference variant: we supply only the function value.
   double diffstep=1.0e-6;
   CAlglib::MinBLEICCreateF(2,x,diffstep,state);
   CAlglib::MinBLEICSetBC(state,bndl,bndu);
//--- inner stop: epsg, epsf, epsx ; outer stop: epsx, epsi
   CAlglib::MinBLEICSetInnerCond(state,0.0,0.0,1.0e-8);
   CAlglib::MinBLEICSetOuterCond(state,1.0e-8,1.0e-8);
   CAlglib::MinBLEICOptimize(state,objective,repcb,false,dummy);

Интервальные ограничения задают допустимые значения параметров GPD. Чтобы параметр масштаба sigma оставался строго положительным, для него задается нижняя граница чуть выше нуля; верхняя граница отсутствует. Параметр формы xi ограничен диапазоном от -0,5 до 1, который охватывает все интересующие нас режимы хвоста и не допускает попадания поиска в вырожденные области. В частности, верхняя граница 1 совпадает с границей, за которой ES перестает существовать.

Задание начального приближения для оптимизатора

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

//--- starting point: method-of-moments seed for robustness
   double mean=0.0;
   for(int i=0;i<n;i++)
      mean+=excess[i];
   mean/=n;
   double var=0.0;
   for(int i=0;i<n;i++)
      var+=(excess[i]-mean)*(excess[i]-mean);
   var/=MathMax(1,n-1);

//--- MoM: xi0 = 0.5*(1 - mean^2/var), sigma0 = 0.5*mean*(mean^2/var + 1)
   double ratio=(var>0.0)?(mean*mean/var):1.0;
   double xi0   = 0.5*(1.0-ratio);
   double sig0  = 0.5*mean*(ratio+1.0);
   if(!MathIsValidNumber(xi0)  || xi0<-0.4 || xi0>0.9)
      xi0=0.1;
   if(!MathIsValidNumber(sig0) || sig0<=0.0)
      sig0=MathMax(mean,1.0e-8);

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

От параметров к показателям риска

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

//+------------------------------------------------------------------+
//| GPD-POT Value at Risk at confidence p (e.g. 0.99).               |
//|  VaR_p = u + (sigma/xi)*[ ( n/Nu * (1-p) )^(-xi) - 1 ]           |
//|  with the exponential limit as xi -> 0.                          |
//|  Nu/n is the exceedance rate = m_n_exceed / m_n_total.           |
//+------------------------------------------------------------------+
double CGPDModel::VaR(double p) const
  {
   if(m_status!=GPD_OK)
      return 0.0;
   if(p<=0.0 || p>=1.0)
      return 0.0;

   double rate=(double)m_n_exceed/(double)m_n_total; // Nu/n
   if(rate<=0.0)
      return 0.0;

   double frac=(1.0-p)/rate;       // = n/Nu * (1-p)
   if(frac<=0.0)
      return m_u;

   if(MathAbs(m_xi)<1.0e-8)
      return m_u - m_sigma*MathLog(frac);            // exponential limit
   return m_u + (m_sigma/m_xi)*(MathPow(frac,-m_xi)-1.0);
  }

Обратите внимание на самую первую строчку метода. Если модель не находится в состоянии GPD_OK, методы расчета VaR и ES возвращают ноль. Правило честности соблюдается непосредственно при обращении к методам, а не просто декларируется: ненадежная подгонка ни при каких условиях не может дать правдоподобное значение показателя риска. Теперь, когда движок готов и самодостаточен, мы можем придать ему "лицо".


Оценка риска обвала в реальном времени — индикатор

Индикатор EVTCrashGauge.mq5 позволяет наглядно наблюдать за результатами работы движка. На каждом новом баре индикатор выбирает последние цены закрытия в скользящем окне, преобразует их в ряд убытков от снижения цены, подгоняет GPD и выводит результат на панели на графике и в виде линии VaR в отдельном окне индикатора. Входных параметров у него намеренно немного.

//--- inputs
input int    Lookback        = 800;    // rolling window (bars) for the fit
input double ThresholdQuant  = 0.95;   // POT threshold quantile
input double Confidence      = 0.99;   // VaR/ES confidence
input int    RefitEvery      = 1;      // refit every N new bars
input bool   ShowPanel       = true;   // draw the on-chart text panel

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

//+------------------------------------------------------------------+
//| Build the downside-loss series from a window of closes ending at |
//| bar index 'end' (older->newer) and fit the GPD.                  |
//|  loss_i = max(0, -logreturn_i) = magnitude of a down move.       |
//+------------------------------------------------------------------+
bool FitWindow(const double &close[],int end,int total)
  {
   int start=end-Lookback;          // need Lookback returns
   if(start<1)
      return false;

   double losses[];
   int k=0;
   ArrayResize(losses,end-start+1);    // loop runs (end-start+1) times
   for(int i=start;i<=end;i++)
     {
      if(i<1 || i>=total)
         continue;
      if(close[i-1]<=0.0 || close[i]<=0.0)
         continue;
      double r=MathLog(close[i]/close[i-1]);   // log return
      double loss=(r<0.0)?-r:0.0;              // downside magnitude
      losses[k++]=loss;
     }
   if(k<Lookback/2)
      return false;           // window too sparse
   ArrayResize(losses,k);

   return g_model.Fit(losses,k);
  }

Как индикатор риска обвала обновляется на разных барах

Подгонка GPD требует оптимизации, а не простого поиска готового значения, поэтому повторять ее на каждом историческом баре графика с большой глубиной истории было бы нерационально. В функции OnCalculate индикатор использует простое правило периодичности: для самого последнего бара модель всегда подгоняется заново, чтобы текущее показание оставалось актуальным, а на исторических барах повторная подгонка выполняется лишь с интервалом в RefitEvery баров, задаваемым обратным отсчетом в переменной g_bars_since_fit. Полученный результат затем записывается в буфер линии VaR для соответствующего бара.

   for(int bar=start;bar<rates_total;bar++)
     {
      bool do_fit=(bar==rates_total-1) || (g_bars_since_fit<=0);
      if(do_fit)
        {
         if(FitWindow(close,bar,rates_total))
           {
            g_status =g_model.Status();
            g_xi     =g_model.Xi();
            g_exceed =g_model.ExceedanceCount();
            g_var    =g_model.VaR(Confidence);
            g_es     =g_model.ExpectedShortfall(Confidence);
           }
         else
            g_status=g_model.Status();
         g_bars_since_fit=RefitEvery;
        }
      else
         g_bars_since_fit--;

      VaRBuffer[bar]=(g_status==GPD_OK)?g_var:0.0;
     }

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

Панель получает данные из модели с оцененными параметрами и обозначает тяжесть хвоста цветом. Если результат подгонки надежен, панель показывает VaR и ES при выбранном доверительном уровне в процентах, а параметр формы xi выделяет по принципу светофора: зеленым — для легкого хвоста, золотым — для умеренно тяжелого, красным — для режима тяжелых хвостов, в котором обвалы происходят группами. Если результат подгонки ненадежен, панель прямо об этом сообщает.

void UpdatePanel()
  {
   if(g_status!=GPD_OK)
     {
      ObjectSetString(0,g_prefix+"var",OBJPROP_TEXT,"VaR : --");
      ObjectSetString(0,g_prefix+"es",OBJPROP_TEXT,"ES  : --");
      ObjectSetString(0,g_prefix+"xi",OBJPROP_TEXT,"xi  : --");
      ObjectSetInteger(0,g_prefix+"xi",OBJPROP_COLOR,clrGray);
      ObjectSetString(0,g_prefix+"status",OBJPROP_TEXT,"status: "+g_model.StatusText());
      ObjectSetInteger(0,g_prefix+"status",OBJPROP_COLOR,clrSilver);
      return;
     }

Сам "светофор" реализован небольшой вспомогательной функцией; стоит явно указать границы цветовых зон, чтобы значение цветов не оставалось неопределенным:

//+------------------------------------------------------------------+
//| Map xi to a tail-heaviness color (calm -> elevated -> extreme).  |
//+------------------------------------------------------------------+
color XiColor(double xi)
  {
   if(xi<0.10)
      return clrLimeGreen;     // light tail, calm
   if(xi<0.30)
      return clrGold;          // moderately heavy
   return clrOrangeRed;                 // heavy tail, extreme
  }

Границы намеренно заданы округленными, а не точными значениями: при параметре формы ниже 0,10 хвост достаточно близок к экспоненциальному режиму с тонкими хвостами, чтобы считать ситуацию спокойной; зона до 0,30 соответствует обычному режиму умеренно тяжелых хвостов, характерному для большинства ликвидных инструментов; значения выше 0,30 указывают на область, где убытки накапливаются, а обвалы происходят группами. Это ориентиры для интерпретации, а не жесткие статистические пороги. Трейдер может свободно менять их, если хочет сделать критерии цветовой индикации более строгими или мягкими. Важно, что для расчетов и выбора цвета используется одно и то же значение xi, поэтому панель не может показывать зеленый цвет, когда формула рассчитывает VaR для режима толстых хвостов.

Именно здесь на практике проявляется требование к минимальному количеству данных. При глубине окна анализа 500 баров и пороге на уровне 95-го процентиля индикатор получает лишь около 25 превышений — меньше необходимого минимума в тридцать. Поэтому подгонка не выполняется, а панель вместо числа сообщает: недостаточно данных в хвосте (25/30 превышений). Увеличение глубины окна анализа до 800 баров обеспечивает необходимый минимум данных, и индикатор успешно выполняет подгонку. Это не ошибка, которую нужно обходить: модель просто отказывается гадать. Именно такого поведения мы и ожидаем от инструмента, предназначенного для честной оценки редких событий.

EVT Crash Gauge indicator on a chart showing the panel and VaR line

Рис. 3. Индикатор риска обвала на основе EVT на графике: панель показывает VaR, ES и параметр формы хвоста xi с цветовой индикацией, а в отдельном окне индикатора отображается изменение VaR на основе EVT во времени, позволяя наблюдать, как хвост сужается и расширяется.

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


Применение на практике — надстройка для расчета размера позиции на основе EVT

Чтобы показать практическое применение индикатора, а не только вывод информации, мы включаем его в простой советник EVTRiskOverlay.mq5. Главное в этом советнике — не логика входа, намеренно сведенная к простому пересечению скользящих средних, а надстройка управления риском. Перед каждой сделкой надстройка использует оценку хвоста на основе EVT и рассчитывает размер позиции так, чтобы расчетный хвостовой убыток не превышал фиксированной доли эквити. Один главный переключатель позволяет запускать одну и ту же стратегию как с выключенной, так и с включенной надстройкой.

//--- EVT overlay
input bool   UseEVT          = true;   // master toggle (off = baseline)
input int    Lookback        = 800;    // window for the GPD fit (>=~800 for 30+ tail pts at q=0.95)
input double ThresholdQuant  = 0.95;   // POT threshold quantile
input double Confidence      = 0.99;   // VaR confidence
input double EquityBudgetPct = 2.0;    // max tolerated EVT tail loss (% equity)
input bool   BlockOverScale  = false;  // true: block trade; false: halve lot

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

Расчет размера позиции — основа надстройки, но для него нужен служебный компонент, который преобразует единицы измерения. VaR на основе EVT — относительная величина убытка за один бар, безразмерное число, например 0,018, означающее неблагоприятное движение на 1,8%. Лимит риска, заданный как доля эквити, напротив, выражается суммой в валюте счета. Чтобы сравнить эти две величины, нужно знать, какому денежному убытку соответствует такое относительное движение цены для позиции размером один лот по торгуемому инструменту. Именно это и делает LossPerLot:

//+------------------------------------------------------------------+
//| Money value of a 1-lot position if price moves by 'ret' fraction.|
//|  Uses the symbol's tick value/size so it works across FX,        |
//|  indices, metals, etc. Returns loss in account currency for one  |
//|  lot given a fractional adverse move.                            |
//+------------------------------------------------------------------+
double LossPerLot(double ret)
  {
   double price=SymbolInfoDouble(_Symbol,SYMBOL_BID);
   double tickVal=SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_VALUE);
   double tickSz =SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE);
   if(tickSz<=0.0 || price<=0.0)
      return 0.0;
   double priceMove=ret*price;                 // adverse move in price terms
   return (priceMove/tickSz)*tickVal;          // currency loss for 1.0 lot
  }

Преобразование выполняется в два этапа. Сначала относительное изменение цены переводится в абсолютное умножением на текущую цену: движение на 1,8% при цене 1,0850 составляет примерно 0,0195 в единицах котировки. Затем изменение цены выражается числом тиков и умножается на стоимость одного тика. Для символа стоимость тика задается свойством SYMBOL_TRADE_TICK_VALUE, а его размер — свойством SYMBOL_TRADE_TICK_SIZE. Поскольку эти два свойства уже учитывают размер контракта и валюту котировки каждого инструмента, одна и та же функция возвращает корректный убыток на один лот для валютной пары, фондового индекса или металла. Надстройке, использующей эту функцию, не требуется отдельно обрабатывать каждый тип инструмента. После этого расчет размера позиции сводится к одному делению.

//+------------------------------------------------------------------+
//| EVT position sizer (the overlay).                                |
//|  Translate EVT VaR into the position's tail loss as a fraction   |
//|  of equity and size the lot so estimated tail loss stays within  |
//|  the equity budget:                                              |
//|     max_lot = (budget * equity) / (LossPerLot(VaR))              |
//|  Final lot = min(base lot, max_lot). Calm tail -> full base lot; |
//|  fat tail -> automatically scaled down. Returns 0.0 = no trade.  |
//+------------------------------------------------------------------+
double OverlayLot()
  {
   double lot=BaseLot;
   if(!UseEVT)
      return lot;

   double var=EVTValueAtRisk();
   if(var<0.0)                  // unreliable fit -> be conservative, skip
      return 0.0;

   double equity=AccountInfoDouble(ACCOUNT_EQUITY);
   double budget=EquityBudgetPct/100.0;     // e.g. 0.02
   double lossPerLot=LossPerLot(var);          // currency tail loss / lot
   if(lossPerLot<=0.0)
      return lot;                              // can't size -> fall back to base

   double maxLot=(budget*equity)/lossPerLot;   // lot meeting the budget exactly

   if(BlockOverScale && maxLot<lot)
      return 0.0;                              // strict mode: veto oversized trades
   return MathMin(lot,maxLot);                 // sizer mode: cap at budget
  }

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

В этой функции стоит отдельно рассмотреть две ветви: именно они определяют подход надстройки к управлению риском. Первая находится в самом начале: если EVTValueAtRisk возвращает отрицательное сигнальное значение, указывающее на ненадежный результат подгонки, OverlayLot возвращает ноль, и сделка не открывается. Это осознанный выбор: считать ситуацию "мы не знаем характеристик хвоста" поводом для осторожности, а не разрешением на торговлю. Ненадежная оценка никогда не трактуется без предупреждения как низкий риск. Вторая связана с переключателем BlockOverScale в конце функции, который позволяет выбрать один из двух режимов надстройки. В используемом по умолчанию режиме расчета размера позиции функция возвращает min(base lot, max lot): размер позиции ограничивается лимитом риска, но сделка все же открывается. В строгом режиме, когда BlockOverScale равен true, функция возвращает ноль, если допустимый по лимиту риска размер позиции меньше базового. Тем самым она запрещает любую сделку, которую, согласно оценке хвоста, слишком рискованно открывать с полным базовым размером позиции. Режим расчета размера позиции уменьшает позицию; строгий режим запрещает сделку. В приведенном ниже тестировании на исторических данных используется режим расчета размера позиции. Поэтому, как мы увидим, надстройка изменяет размер каждой сделки, не исключая ни одной.

Сравнение

Мы дважды запускаем советник на одних и тех же исторических данных за период более года, для одного и того же символа и таймфрейма, изменяя только UseEVT. При отключенной надстройке получаем исходный базовый вариант; при включенной единственное отличие — расчет размера позиции на основе EVT. Результаты представлены ниже.

Показатель
Базовый вариант (EVT выключена)
Надстройка EVT включена
Общая чистая прибыль
-333,60
-223,99
Профит-фактор
0,87
0,90
Ожидаемый выигрыш
-3,44
-2,31
Фактор восстановления
-0,45
-0,40
Коэффициент Шарпа
-0,88
-0,66
Максимальная просадка баланса
715,70
540,90
Максимальная просадка эквити
733,50
556,75
Самая убыточная сделка
-59,60
-52,00
Общее количество сделок
97
97

Самая важная строка — это последняя. В обоих запусках советник совершил ровно 97 сделок. Надстройка не запретила ни одной сделки: все входы базового варианта были сохранены. Единственное, что она изменила, — это размер этих позиций: они уменьшались, когда хвост был тяжелым. Неизменность входов служит контрольным условием, поэтому все различия в таблице обусловлены только размером позиций. Это настолько наглядная демонстрация механизма, насколько позволяет тестирование на исторических данных.

И этот эффект в точности соответствует тому, что должна давать надстройка EVT. Максимальная просадка эквити сократилась примерно на 24% — с 733,50 до 556,75; аналогичное улучшение наблюдалось для просадки баланса, а максимальный убыток по отдельной сделке уменьшился. Оба показателя с учетом риска — коэффициент Шарпа и фактор восстановления — улучшились, а чистый убыток сократился как сопутствующий результат уменьшения позиций в опасные периоды.

Strategy Tester equity curve, baseline EVT off

Рис. 4. Базовая кривая эквити с отключенной надстройкой (UseEVT = false).

Strategy Tester equity curve, EVT overlay on

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

ПРИМЕЧАНИЕ: обе версии стратегии приносят убытки. Логика входа по пересечению скользящих средних используется лишь как заглушка, а не как источник торгового преимущества; EVT не превращает убыточную систему в прибыльную. Наш вывод ограничен, но подтвержден данными, и только его эти данные позволяют сделать: при неизменных входах расчет размера позиции на основе EVT сократил максимальную просадку примерно на четверть. EVT позволяет контролировать величину хвостовых убытков, но не создает доходность.


Ограничения и объективный вывод

EVT заслуживает места среди инструментов управления рисками, но остается специализированным методом. Для его эффективного использования нужно понимать границы применимости.

  • Модели требуется много наблюдений в хвосте. Для подгонки двухпараметрической модели хвоста нужно достаточно превышений. Поэтому движок требует их минимального количества, а окна в 500 баров оказалось недостаточно, тогда как 800 баров хватило. Для инструмента с редкими сделками или очень короткого окна анализа правильный ответ модели — "недостаточно данных". Реализация учитывает это, а не выдает выдуманное число.
  • Модель чувствительна к порогу. Порог на уровне 95-го процентиля — стандартный выбор по умолчанию, но оценки параметров формы и масштаба действительно меняются при изменении порога. Перед практическим применением следует исследовать эту чувствительность, а не полагаться на единственную настройку. Именно для этого квантиль, задающий порог, вынесен во входные параметры.
  • Модель оценивает величину убытка, а не время его наступления. Это стоит повторить: попытка прогнозировать время события — самая распространенная ошибка при использовании EVT. Индикатор показывает, насколько тяжел хвост и насколько крупным может быть редкий убыток; он не предсказывает следующий обвал и не указывает направление движения рынка. Рассматривать рост xi как сигнал к открытию короткой позиции — значит неверно понимать назначение инструмента.
  • Хвост может перестать быть корректно определенным. Когда оцененное значение xi приближается к единице, среднее хвостового распределения стремится к бесконечности, и ES теряет смысл. В этом режиме движок возвращает ноль вместо выдуманного значения. Это само по себе информативно: хвост слишком тяжел, чтобы существовал конечный средний убыток за пределами VaR.

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


Заключение

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

  • Движок с возможностью повторного использования. Класс CGPDModel с помощью ALGLIB подгоняет обобщенное распределение Парето к левому хвосту распределения доходности методом максимального правдоподобия, без внешних зависимостей. Он предоставляет значения VaR, ES и параметра формы хвоста xi, но не выводит оценки, надежность которых не может подтвердить.
  • Индикатор риска обвала в реальном времени. Индикатор отображает результаты работы движка на панели на графике и в виде линии VaR, обозначает тяжесть хвоста цветом и явно сообщает "недостаточно данных", когда наблюдений в хвосте слишком мало для надежной оценки.
  • Рабочая надстройка. Советник использует оценку хвоста на основе EVT для расчета размера позиции. В тестировании на исторических данных за период более года он сократил максимальную просадку примерно на 24%, сохранив тот же набор сделок.

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

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


Получение исходного кода через MQL5 Algo Forge

Все исходные файлы прикреплены ниже к этой статье, но полный репозиторий также доступен на MQL5 Algo Forge — платформе сообщества на базе Git для публикации торговых проектов и совместной работы над ними.

Имя файла
Описание
MQL5\Include\EVT\GPDModel.mqh
Движок оценки хвостового риска на основе теории экстремальных значений: подгонка GPD методом превышений над порогом с помощью ALGLIB и расчет VaR и ES
MQL5\Indicators\EVT\EVTCrashGauge.mq5
Индикатор риска обвала: панель на графике со значениями VaR, ES и параметра формы хвоста, а также линия VaR на основе EVT
MQL5\Experts\EVT\EVTRiskOverlay.mq5
Простой советник, демонстрирующий расчет размера позиции на основе EVT, с переключателем для сравнения вариантов с выключенной и включенной надстройкой

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/23278

Прикрепленные файлы |
MQL5.zip (10.3 KB)
Нейросети в трейдинге: Диффузионная генерация торговых планов (Сценарный расчёт) Нейросети в трейдинге: Диффузионная генерация торговых планов (Сценарный расчёт)
Продолжаем торговую адаптацию DPCC и объединяем ранее разработанные OpenCL-кернелы в два класса MQL5: CNeuronScenarioRolloutOCL для сценарного расчёта торговых планов и CNeuronPlanProjectionOCL для их коррекции с учётом ограничений. Разбираем организацию данных, взаимодействие классов, поиск поправок и повторную проверку финансовых последствий. Показываем, как использовать эти механизмы в собственных моделях и различать допустимый план, исчерпание бюджета поиска и ошибку вычислений.
За пределами максимальной просадки: Создание анализатора ДНК просадки на MQL5 За пределами максимальной просадки: Создание анализатора ДНК просадки на MQL5
Максимальная просадка — это одно число, за которым скрывается то, что действительно важно: как часто снижается кривая эквити, как долго она остается ниже предыдущего пика и как быстро восстанавливается. В этой статье мы создадим нативный инструмент на MQL5, который восстанавливает кривую просадки, разбивает ее на отдельные эпизоды (глубина, продолжительность, время восстановления), рассчитывает индекс Ulcer, индекс боли и фактор восстановления, а затем объединяет эти показатели в единую оценку устойчивости с практическими рекомендациями. Никаких внешних библиотек, никакого Python, никакого ИИ.
Трейдинг с экономическим календарем MQL5 (Часть 11): Модульная новостная панель на Canvas Трейдинг с экономическим календарем MQL5 (Часть 11): Модульная новостная панель на Canvas
Мы перестраиваем информационную панель Экономического календаря MQL5: вместо монолитной панели, основанной на объектах, она становится модульной системой на Canvas, разделённой на четыре файла. В обновлении добавлены две темы оформления — светлая и тёмная, сворачиваемые группы дней, макет с изменяемым размером и прокруткой с пиксельной точностью, маркеры пересмотренных значений, а также обратный отсчёт в реальном времени со всплывающими уведомлениями. Кэш событий-кандидатов и таймер с ускоренной веткой, который перерисовывает только изменившиеся ячейки, повышают отзывчивость и упрощают расширение кодовой базы.
Нейронная сеть на практике: Чтение PNG-изображений из MNIST Нейронная сеть на практике: Чтение PNG-изображений из MNIST
Здесь мы рассмотрим, как реализовать простой алгоритм чтения изображений в формате PNG. Умение читать содержимое этих изображений будет важно для дальнейшей разработки топологии перцептронной сети. Это связано с тем, что мы начнём использовать базу данных MNIST для обучения сети, которую будем реализовывать.