English Deutsch 日本語
preview
Двумерные копулы в MQL5 (Часть 2): Реализация архимедовых копул в MQL5

Двумерные копулы в MQL5 (Часть 2): Реализация архимедовых копул в MQL5

MetaTrader 5Примеры |
115 0
Francis Dube
Francis Dube

Введение

В предыдущей статье мы представили реализации двумерных эллиптических копул в MQL5, а именно гауссовой копулы и t-копулы Стьюдента. При применении этих копул есть как минимум два недостатка: их алгебраическое выражение сложно, а их симметричность может быть нежелательной. Эти трудности стали стимулом для поиска копул с удобными алгебраическими выражениями, которые при этом могут моделировать асимметричную зависимость в наборах данных. Одно из наиболее популярных семейств копул, удовлетворяющих обоим требованиям, — класс архимедовых копул. Поэтому в этой статье мы рассмотрим распространённые двумерные архимедовы копулы и их реализацию в MQL5. Мы увидим расширение библиотеки двумерных копул за счёт добавления реализаций копул Франка, Джо, Гумбеля, Клейтона, N13 и N14.


Введение в архимедовы копулы

Двумерная архимедова копула — это особый тип копулы C(u,v), используемый в статистике для моделирования структуры зависимости между двумя случайными величинами с равномерными маргинальными распределениями. Её ключевое свойство — задаётся порождающей функцией, которую можно выразить с помощью одной непрерывной, строго убывающей и выпуклой функции, называемой функцией-генератором ϕ.

Функция-генератор

Генератор ϕ должен удовлетворять условию ϕ(1)=0. Такая структура придаёт архимедовым копулам высокую степень симметрии (C(u,v)=C(v,u)) и позволяет моделировать широкий диапазон структур зависимости простым выбором разных функций-генераторов. Архимедовы копулы в принципе более гибки, чем обычные копулы, но на практике эта гибкость обычно ограничивается. Теоретически существует бесконечное число вариантов функции-генератора. Каждое небольшое изменение функции создаёт новую, уникальную копулу. На практике обычно выбирают параметрическое семейство, задаваемое уникальной функцией-генератором, определяемого одним скалярным параметром. Это существенно упрощает оценивание и моделирование. Поэтому большинство распространённых архимедовых семейств характеризуется одним параметром, встроенным в функцию-генератор, который управляет силой зависимости.

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


Двумерная копула Франка

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

Функция-генератор Франка

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

Класс CFrank, определённый в frank.mqh, наследуется от CBivariateCopula и представляет копулу Frank.

//+------------------------------------------------------------------+
//|                                                        frank.mqh |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#include "base.mqh"
//+------------------------------------------------------------------+
//| Frank Copula.                                                    |
//+------------------------------------------------------------------+
class CFrank:public CBivariateCopula
  {
private:
   class CInt_Function_1_Func : public CIntegrator1_Func
     {
   private:
      double         m_th;
   public:
      //---
                     CInt_Function_1_Func(void) {}
                    ~CInt_Function_1_Func(void) {}
      void              set_theta(double theta)
        {
         m_th = theta;
        }
      virtual void      Int_Func(double x,double xminusa,double bminusx,double &y,CObject &obj)
        {
         y = x/m_th/(exp(x)-1.0);
        }
     };
   class CBrent1:public CBrentQ
     {
   private:
      double            debyel(double theta)
        {
         CInt_Function_1_Func fint;
         fint.set_theta(theta);
         CObject obj;
         CAutoGKStateShell s;
         double integral;
         CAutoGKReportShell rep;
         //---
         CAlglib::AutoGKSmooth(0.0,theta,s);
         //---
         CAlglib::AutoGKIntegrate(s,fint,obj);
         //---
         CAlglib::AutoGKResults(s,integral,rep);

         CAutoGKReport report = rep.GetInnerObj();

         if(report.m_terminationtype<0.0)
            Print(__FUNCTION__, " integration error ",report.m_terminationtype);

         return integral;
        }
   public:
                     CBrent1(void)
        {
        }
                    ~CBrent1(void)
        {
        }
      virtual double objective(double u, double v,double y)
        {
         return (1.0 - 4.0/u+4.0*debyel(u)/u) - y;
        }
     };

   virtual double    theta_hat(const double tau) override
     {
      CBrent1 fun;
      return fun.minimize(0,tau,-100.0,100.0,2.e-12,4.0*2.e-16,100);
     }
   vector            generate_pair(double v1, double v2)
     {
      vector out(2);
      out[0] = v1;
      out[1] = -1.0/m_theta*log(1.0+(v2*(1.0-exp(-m_theta)))/(v2*(exp(-m_theta*v1)-1.0)-exp(-m_theta*v1)));
      return out;
     }
protected:
   virtual double    pdf(double u,double v) override
     {
      double et,eut,evt,pd;
      et = exp(m_theta);
      eut = exp(u*m_theta);
      evt = exp(v*m_theta);
      pd = et*eut*evt*(et - 1.0)*m_theta/pow(et+eut*evt-et*eut-et*evt,2.0);
      return pd;
     }
   virtual double    cdf(double u, double v) override
     {
      return (-1.0 / m_theta * log(1.0 + (exp(-1.0 * m_theta * u) - 1.0) * (exp(-1.0 * m_theta * v) - 1.0)/ (exp(-1 * m_theta) - 1.0)));
     }
   virtual double    condi_cdf(double u, double v) override
     {
      double enut = exp(-u*m_theta);
      double envt = exp(-v*m_theta);
      double ent = exp(-m_theta);
      double denominator = ((ent - 1.0) + (enut - 1.0) * (envt - 1.0));
      if(denominator)
         return (envt * (enut - 1.0)/ (denominator));
      else
         return EMPTY_VALUE;
     }
   virtual vector    pdf(vector &u,vector &v) override
     {
      vector eut,evt,pd;
      double et = exp(m_theta);
      eut = exp(u*m_theta);
      evt = exp(v*m_theta);
      pd = et*eut*evt*(et - 1.0)*m_theta/pow(et+eut*evt-et*eut-et*evt,2.0);
      return pd;
     }
   virtual vector    cdf(vector &u, vector &v) override
     {
      return (-1.0 / m_theta * log(1.0 + (exp(-1.0 * m_theta * u) - 1.0) * (exp(-1.0 * m_theta * v) - 1.0)/ (exp(-1 * m_theta) - 1.0)));
     }
   virtual vector    condi_cdf(vector &u, vector &v) override
     {
      vector enut = exp(-1.0*u*m_theta);
      vector envt = exp(-1.0*v*m_theta);
      double ent = exp(-m_theta);

      return (envt * (enut - 1.0)/ ((ent - 1.0) + (enut - 1.0) * (envt - 1.0)));
     }
public:
                     CFrank(void)
     {
      m_threshold = 1.e-10;
      m_copula_type = FRANK_COPULA;
      m_invalid_theta = 0.0;
     }
                    ~CFrank(void)
     {
     }
   virtual matrix    Sample(ulong num_samples) override
     {
      double unf_v[],unf_c[];

      vector v,c,u;

      if(!MathRandomUniform(0.0,1.0,int(num_samples),unf_v) ||
         !MathRandomUniform(0.0,1.0,int(num_samples),unf_c) ||
         !v.Assign(unf_v) || !c.Assign(unf_c))
        {
         Print(__FUNCTION__, " failed to get uniform random numbers ", GetLastError());
         return matrix::Zeros(0,0);
        }

      matrix out(num_samples,2);

      for(ulong irow = 0; irow<num_samples; irow++)
         out.Row(generate_pair(v[irow],c[irow]),irow);

      return out;

     }

  };
//+------------------------------------------------------------------+

Параметр θ (тета) определяется по выборкам данных путём оценки ранговой корреляции τ (тау Кендалла) с использованием следующего соотношения:

Связь между theta и тау Кендалла


где D1​(θ) — функция Дебая первого порядка. Ниже приведена диаграмма рассеяния синтетического набора данных, сгенерированного из копулы Франка с параметром theta, равным 5.

Выборка из копул Франка


Двумерная копула Клейтона

Далее рассмотрим двумерную копулу Клейтона, которая характеризуется способностью моделировать сильную нижнюю хвостовую зависимость и асимметрию между хвостами совместного распределения. Она определяется функцией-генератором:

Функция-генератор Клейтона

Чем больше значение θ, тем сильнее связь в нижнем хвосте. Копула Клейтона проявляет нижнюю хвостовую зависимость с коэффициентом:

Хвостовая зависимость Клейтона

Она не имеет верхней хвостовой зависимости (λU​=0), что делает её особенно подходящей для моделирования ситуаций, в которых экстремально низкие значения склонны возникать совместно. Она отражает только положительную зависимость (θ>0) и приближается к копуле независимости при стремлении θ к 0. Обратите внимание, что копула Клейтона не определена при θ=0. Её асимметричная природа позволяет эффективно представлять отношения, в которых совместные падения более вероятны, чем совместные росты. Заголовочный файл clayton.mqh содержит определение класса CClayton.

//+------------------------------------------------------------------+
//|                                                      clayton.mqh |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#include "base.mqh"
//+------------------------------------------------------------------+
//| Clayton copula.                                                  |
//+------------------------------------------------------------------+
class CClayton:public CBivariateCopula
  {
private:
   virtual double    theta_hat(const double tau) override
     {
      return 2.0*tau/(1.0-tau);
     }
   vector            generate_pair(double v1, double v2)
     {
      vector out(2);
      double w = 0.0;

      out[0] = v1;
      out[1] = pow(pow(v1,-m_theta)*(pow(v2,(-m_theta / (1 + m_theta))) - 1.0)+1.0,-1.0/m_theta);
      return out;
     }
protected:
   virtual double    pdf(double u,double v) override
     {
      double u_part,v_part,pd;
      u_part = pow(u,(-1 - m_theta));
      v_part = pow(v,(-1 - m_theta));
      pd = ((1.0 + m_theta) * u_part * v_part*pow(-1.0 + u_part * u + v_part * v, (-2.0 - 1.0 / m_theta)));
      return pd;
     }
   virtual double    cdf(double u, double v) override
     {
      double expo = pow(MathMax(pow(u,-m_theta)+pow(v,-m_theta)-1.0,0.0),-1.0/m_theta);

      return expo;
     }
   virtual double    condi_cdf(double u, double v) override
     {
      double unt = pow(u,-m_theta);
      double vnt = pow(v,-m_theta);
      double tpow = 1.0/m_theta + 1.0;
      double denominator = pow(unt+vnt-1.0,tpow);
      if(denominator)
         return vnt/v/pow(unt+vnt-1.0,tpow);
      else
         return EMPTY_VALUE;
     }
   virtual double    inv_condi_cdf(double y, double V) override
     {
      if(m_theta < 0.0)
         return V;
      else
        {
         double a = pow(y,m_theta/(-1.0 - m_theta));
         double b = pow(V, m_theta);

         if(b==0)
            return 1.0;
         return pow((a + b - 1.0)/b, -1.0/m_theta);
        }
     }
   virtual vector    inv_condi_cdf(vector& y, vector& V) override
     {
      if(m_theta < 0.0)
         return V;
      else
        {
         vector a = pow(y,m_theta/(-1.0 - m_theta));
         vector b = pow(V, m_theta);

         if(b.CompareEqual(vector::Zeros(b.Size()))==0)
            return vector::Ones(V.Size());
         return pow((a + b - 1.0)/b, -1.0/m_theta);
        }
     }
   virtual vector    pdf(vector &u,vector &v) override
     {
      vector u_part,v_part,pd;
      u_part = pow(u,(-1 - m_theta));
      v_part = pow(v,(-1 - m_theta));
      pd = ((1.0 + m_theta) * u_part * v_part*pow(-1.0 + u_part * u + v_part * v, (-2.0 - 1.0 / m_theta)));
      return pd;
     }
   virtual vector    cdf(vector &u, vector &v) override
     {
      vector out(u.Size());
      for(ulong i = 0; i<u.Size(); ++i)
         out[i] = cdf(u[i],v[i]);
      return out;
     }
   virtual vector    condi_cdf(vector &u, vector &v) override
     {
      vector unt = pow(u,-m_theta);
      vector vnt = pow(v,-m_theta);
      double tpow = 1.0/m_theta + 1.0;
      return vnt/v/pow(unt+vnt-1.0,tpow);
     }
public:
                     CClayton(void)
     {
      m_threshold = 1.e-10;
      m_bounds[0] = -1.0;
      m_bounds[1] = DBL_MAX;
      m_invalid_theta = 0.0;
      m_copula_type = CLAYTON_COPULA;
     }
                    ~CClayton(void)
     {
     }
   virtual matrix    Sample(ulong num_samples) override
     {
      double unf_v[],unf_c[];

      vector v,c,u;

      if(!MathRandomUniform(0.0,1.0,int(num_samples),unf_v) ||
         !MathRandomUniform(0.0,1.0,int(num_samples),unf_c) ||
         !v.Assign(unf_v) || !c.Assign(unf_c))
        {
         Print(__FUNCTION__, " failed to get uniform random numbers ", GetLastError());
         return matrix::Zeros(0,0);
        }

      matrix out(num_samples,2);

      for(ulong irow = 0; irow<num_samples; irow++)
         out.Row(generate_pair(v[irow],c[irow]),irow);

      return out;

     }

  };
//+------------------------------------------------------------------+

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

Двумерная выборка Клейтона


Двумерная копула Гумбеля

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

Функция-генератор Гумбеля

Чем больше значение θ, тем сильнее зависимость. Копула Гумбеля проявляет верхнюю хвостовую зависимость с коэффициентом:

Хвостовая зависимость Гумбеля

И не имеет нижней хвостовой зависимости, что делает её особенно полезной для улавливания тенденции экстремально высоких значений одной переменной совпадать с экстремально высокими значениями другой. Она может представлять только положительную зависимость и сходится к копуле независимости при θ, равной единице. Копула Гумбеля реализована как класс CGumbel, определённый в gumbel.mqh.

//+------------------------------------------------------------------+
//|                                                       gumbel.mqh |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#include "base.mqh"
//+------------------------------------------------------------------+
//| Gumbel Copula.                                                   |
//+------------------------------------------------------------------+
class CGumbel:public CBivariateCopula
  {
private:
   class CBrent1:public CBrentQ
     {
   public:
                     CBrent1(void)
        {
        }
                    ~CBrent1(void)
        {
        }
      virtual double objective(double u, double v,double y)
        {
         return (u*(1.0 - log(u)/v)) - y;
        }
     };
   virtual double    theta_hat(const double tau) override
     {
      return 1.0/(1.0-tau);
     }
   vector            generate_pair(double v1, double v2)
     {
      vector out(2);
      double w = 0.0;
      if(v2>m_threshold)
        {
         CBrent1 fun;
         w = fun.minimize(m_theta,v2,m_threshold,1.0,2.e-12,4.0*2.e-16,100);
        }
      else
         w = 1.e10;
      out[0] = exp(pow(v1,1.0/m_theta)*log(w));
      out[1] = exp(pow((1.0-v1),1.0/m_theta)*log(w));
      return out;
     }
protected:
   virtual double    pdf(double u,double v) override
     {
      double u_part,v_part,expo,pd;
      u_part = pow(-1.0*log(u),m_theta);
      v_part = pow(-1.0*log(v),m_theta);
      expo = pow(u_part+v_part,1.0/m_theta);
      pd = 1.0/(u*v) * (exp(-1.0*expo)* u_part/(-1.0*log(u)) * v_part/(-1.0*log(v))*(m_theta+expo-1.0)*pow(u_part+v_part,(1.0/m_theta-2.0)));
      return pd;
     }
   virtual double    cdf(double u, double v) override
     {
      double expo = pow(pow(-1.0*log(u),m_theta) + pow(-1.0*log(v),m_theta),(1. / m_theta));

      return exp(-1.0*expo);
     }
   virtual double    condi_cdf(double u, double v) override
     {
      double expo = pow(pow(-1.0*log(u),m_theta)+pow(-1.0*log(v),m_theta),((1 - m_theta) / m_theta));
      return (cdf(u,v) * expo * pow(-1.0*log(v),m_theta-1.0)/v);
     }
   virtual vector    pdf(vector &u,vector &v) override
     {
      vector u_part,v_part,expo,pd;
      u_part = pow(-1.0*log(u),m_theta);
      v_part = pow(-1.0*log(v),m_theta);
      expo = pow(u_part+v_part,1.0/m_theta);
      pd = 1.0/(u*v) * (exp(-1.0*expo)* u_part/(-1.0*log(u)) * v_part/(-1.0*log(v))*(m_theta+expo-1.0)*pow(u_part+v_part,(1.0/m_theta-2.0)));
      return pd;
     }
   virtual vector    cdf(vector &u, vector &v) override
     {

      vector expo = pow(pow(-1.0*log(u),m_theta) + pow(-1.0*log(v),m_theta),(1. / m_theta));
      return exp(-1.0*expo);
     }
   virtual vector    condi_cdf(vector &u, vector &v) override
     {

      vector expo = pow(pow(-1.0*log(u),m_theta)+pow(-1.0*log(v),m_theta),((1 - m_theta) / m_theta));
      return (cdf(u,v) * expo * pow(-1.0*log(v),m_theta-1.0)/v);
     }
public:
                     CGumbel(void)
     {
      m_threshold = 1.e-10;
      m_bounds[0] = 1.0;
      m_bounds[1] = DBL_MAX;
      m_copula_type = GUMBEL_COPULA;
     }
                    ~CGumbel(void)
     {
     }
   virtual matrix    Sample(ulong num_samples) override
     {
      double unf_v[],unf_c[];

      vector v,c,u;

      if(!MathRandomUniform(0.0,1.0,int(num_samples),unf_v) ||
         !MathRandomUniform(0.0,1.0,int(num_samples),unf_c) ||
         !v.Assign(unf_v) || !c.Assign(unf_c))
        {
         Print(__FUNCTION__, " failed to get uniform random numbers ", GetLastError());
         return matrix::Zeros(0,0);
        }

      matrix out(num_samples,2);

      for(ulong irow = 0; irow<num_samples; irow++)
         out.Row(generate_pair(v[irow],c[irow]),irow);

      return out;

     }

  };
//+------------------------------------------------------------------+

Ниже приведена диаграмма рассеяния, показывающая данные, сгенерированные из копулы Гумбеля с параметром theta, равным 5.

Двумерная выборка Гумбеля


Двумерная копула Джо

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

Функция-генератор Джо

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

Хвостовая зависимость Джо

И не имеет нижней хвостовой зависимости. Код, реализующий копулу Джо, приведён в joe.mqh.

//+------------------------------------------------------------------+
//|                                                          joe.mqh |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#include "base.mqh"
//+------------------------------------------------------------------+
//| Joe Copula.                                                      |
//+------------------------------------------------------------------+
class CJoe:public CBivariateCopula
  {
private:
   class CInt_Function_1_Func : public CIntegrator1_Func
     {
   private:
      double         m_th;
   public:
      //---
                     CInt_Function_1_Func(void) {}
                    ~CInt_Function_1_Func(void) {}
      void              set_theta(double theta)
        {
         m_th = theta;
        }
      virtual void      Int_Func(double x,double xminusa,double bminusx,double &y,CObject &obj)
        {
         y = (1.0 - pow(1.0 - x,m_th)) * pow(1.0 - x,1.0 - m_th) * log(1.0 - pow(1.0 - x,m_th)) / m_th;
        }
     };
   class CBrent1:public CBrentQ
     {
   public:
                     CBrent1(void)
        {
        }
                    ~CBrent1(void)
        {
        }
      virtual double objective(double u, double v,double y)
        {
         return (u-1.0/v*(log(1.0-pow(1.0-u,v))*(1.0-pow(1.0-u,v))/pow(1.0-u,(v-1.0)))) - y;
        }
     };
   class CBrent2:public CBrentQ
     {
   public:
                     CBrent2(void)
        {
        }
                    ~CBrent2(void)
        {
        }
      virtual double objective(double u, double v,double y)
        {
         CInt_Function_1_Func fint;
         fint.set_theta(u);
         CObject obj;
         CAutoGKStateShell s;
         double integral;
         CAutoGKReportShell rep;
         //---
         CAlglib::AutoGKSmooth(0.0,1.0,s);
         //---
         CAlglib::AutoGKIntegrate(s,fint,obj);
         //---
         CAlglib::AutoGKResults(s,integral,rep);

         CAutoGKReport report = rep.GetInnerObj();

         if(report.m_terminationtype<0.0)
            Print(__FUNCTION__, " integration error ",report.m_terminationtype);

         return (1.0+4.0*integral)-y;
        }
     };
   virtual double    theta_hat(const double tau) override
     {
      CBrent2 fun;
      return fun.minimize(0,tau,1.0,100.0,2.e-12,4.0*2.e-16,100);
     }
   vector            generate_pair(double v1, double v2)
     {
      vector out(2);
      double w = 0;
      if(v2>m_threshold)
        {
         CBrent1 fun;
         w = fun.minimize(m_theta,v2,m_threshold,1.0-m_threshold,2.e-12,4.0*2.e-16,100);
        }
      else
         w = m_threshold;
      out[0] = 1.0 - pow(1.0 - pow(1.0 - pow(1.0 - w, m_theta), v1),(1.0 / m_theta));
      out[1] = 1.0 - pow(1.0 - pow(1.0 - pow(1.0 - w, m_theta), (1.0-v1)),(1.0 / m_theta));
      return out;
     }
protected:
   virtual double    pdf(double u,double v) override
     {
      double up,vp,pd;
      up = pow(1.0-u,m_theta);
      vp = pow(1.0-v,m_theta);
      pd = (up/(1.0-u)*vp/(1.0-v)*pow(up+vp-up*vp,1.0/m_theta-2.0)*(m_theta-(up-1.0)*(vp-1.0)));
      return pd;
     }
   virtual double    cdf(double u, double v) override
     {
      double up = pow(1.0-u,m_theta);
      double vp = pow(1.0-v,m_theta);
      return 1.0 - pow(up+vp-up*vp,1.0/m_theta);
     }
   virtual double    condi_cdf(double u, double v) override
     {
      double up = pow(1.0-u,m_theta);
      double vp = pow(1.0-v,m_theta);
      return (-(-1.0+up)*pow(up+vp-up*vp,-1.0+1.0/m_theta)*vp/(1.0-v));
     }
   virtual vector    pdf(vector &u,vector &v) override
     {
      vector up,vp,pd;
      up = pow(1.0-u,m_theta);
      vp = pow(1.0-v,m_theta);
      pd = (up/(1.0-u)*vp/(1.0-v)*pow(up+vp-up*vp,1.0/m_theta-2.0)*(m_theta-(up-1.0)*(vp-1.0)));
      return pd;
     }
   virtual vector    cdf(vector &u, vector &v) override
     {
      vector up = pow(1.0-u,m_theta);
      vector vp = pow(1.0-v,m_theta);
      return 1.0 - pow(up+vp-up*vp,1.0/m_theta);
     }
   virtual vector    condi_cdf(vector &u, vector &v) override
     {

      vector up = pow(1.0-u,m_theta);
      vector vp = pow(1.0-v,m_theta);
      return (-1.0*(-1.0+up)*pow(up+vp-up*vp,-1.0+1.0/m_theta)*vp/(1.0-v));
     }
public:
                     CJoe(void)
     {
      m_threshold = 1.e-10;
      m_copula_type = JOE_COPULA;
      m_bounds[0] = 1.0;
      m_bounds[1] = DBL_MAX;
     }
                    ~CJoe(void)
     {
     }
   virtual matrix    Sample(ulong num_samples) override
     {
      double unf_v[],unf_c[];

      vector v,c,u;

      if(!MathRandomUniform(0.0,1.0,int(num_samples),unf_v) ||
         !MathRandomUniform(0.0,1.0,int(num_samples),unf_c) ||
         !v.Assign(unf_v) || !c.Assign(unf_c))
        {
         Print(__FUNCTION__, " failed to get uniform random numbers ", GetLastError());
         return matrix::Zeros(0,0);
        }

      matrix out(num_samples,2);

      for(ulong irow = 0; irow<num_samples; irow++)
         out.Row(generate_pair(v[irow],c[irow]),irow);

      return out;

     }

  };
//+------------------------------------------------------------------+

Здесь показана зависимость, представленная синтетическим набором данных, сгенерированным из копулы Джо с параметром theta, равным 5.

Двумерная выборка Джо


Двумерная копула N13

Копула N13 — это архимедово однопараметрическое семейство, которое моделирует только положительную зависимость и, в отличие от некоторых других архимедовых семейств, не проявляет асимптотической хвостовой зависимости. По мере роста параметра θ этого семейства общая согласованность усиливается, а на нижней границе параметра (θ=0) оно приближается к независимости. Поскольку у N13 нет ненулевых хвостовых коэффициентов, подходит в случаях, когда нужна более сильная зависимость в центральной или верхне-центральной части распределения, чем у семейств гауссовых или Франка, а не кластеризация одновременных экстремумов. Копула N13 определяется функцией-генератором:

Функция-генератор n13

В MQL5 она реализована как класс CN13, определённый в n13.mqh. 

//+------------------------------------------------------------------+
//|                                                          n13.mqh |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#include "base.mqh"
//+------------------------------------------------------------------+
//| N13 Copula (Nelsen 13)                                           |
//+------------------------------------------------------------------+
class CN13:public CBivariateCopula
  {
private:
   class CInt_Function_1_Func : public CIntegrator1_Func
     {
   private:
      double         m_th;
   public:
      //---
                     CInt_Function_1_Func(void) {}
                    ~CInt_Function_1_Func(void) {}
      void              set_theta(double theta)
        {
         m_th = theta;
        }
      virtual void      Int_Func(double x,double xminusa,double bminusx,double &y,CObject &obj)
        {
         y = -((x - x * pow((1 - log(x)),1 - m_th) - x * log(x)) / m_th);
        }
     };
   class CBrent1:public CBrentQ
     {
   public:
                     CBrent1(void)
        {
        }
                    ~CBrent1(void)
        {
        }
      virtual double objective(double u, double v,double y)
        {
         return (u+1.0/v*(u-u*pow(1.0-1.0*log(u),1.0-v)-u*log(u))) - y;;
        }
     };
   class CBrent2:public CBrentQ
     {
   public:
                     CBrent2(void)
        {
        }
                    ~CBrent2(void)
        {
        }
      virtual double objective(double u, double v,double y)
        {
         CInt_Function_1_Func fint;
         fint.set_theta(u);
         CObject obj;
         CAutoGKStateShell s;
         double integral;
         CAutoGKReportShell rep;
         //---
         CAlglib::AutoGKSmooth(0.0,1.0,s);
         //---
         CAlglib::AutoGKIntegrate(s,fint,obj);
         //---
         CAlglib::AutoGKResults(s,integral,rep);

         CAutoGKReport report = rep.GetInnerObj();

         if(report.m_terminationtype<0.0)
            Print(__FUNCTION__, " integration error ",report.m_terminationtype);

         return (1.0 + 4.0*integral) - y;
        }
     };
   virtual double    theta_hat(const double tau) override
     {
      CBrent2 fun;
      return fun.minimize(0,tau,1.e-7,100.0,2.e-12,4.0*2.e-16,100);
     }
   vector            generate_pair(double v1, double v2)
     {
      vector out(2);
      double w = 0.0;
      if(v2>m_threshold)
        {
         CBrent1 fun;
         w = fun.minimize(m_theta,v2,m_threshold,1.0-m_threshold,2.e-12,4.0*2.e-16,100);
        }
      else
         w = m_threshold;
      out[0] = exp(1. - pow(v1 * (pow((1.0 - log(w)),m_theta) - 1.) + 1.,(1. / m_theta)));
      out[1] = exp(1. - pow((1.0-v1) * (pow((1.0 - log(w)),m_theta) - 1.) + 1.,(1. / m_theta)));
      return out;
     }
protected:
   virtual double    pdf(double u,double v) override
     {
      double Cuv,u_part,v_part,numerator,denominator;
      Cuv = cdf(u,v);
      u_part = pow(1.0 - log(u),m_theta);
      v_part = pow(1.0 - log(v),m_theta);
      numerator = Cuv * u_part *v_part * (-1.0+m_theta + pow(-1.0+u_part +v_part,1.0/m_theta))*pow(-1.0+u_part+v_part,1.0/m_theta);
      denominator = u*v*(1.0-1.0*log(u))*(1.0 - log(v))*pow(-1.0+u_part+v_part,2.0);
      return (numerator+1.e-8)/(denominator+1.e-8);
     }
   virtual double    cdf(double u, double v) override
     {
      double u_part = pow(1.0-1.0*log(u),m_theta);
      double v_part = pow(1.0-1.0*log(v),m_theta);
      double cdf = exp(1.0 - pow(-1.0+u_part+v_part,1.0/m_theta));
      return cdf;
     }
   virtual double    condi_cdf(double u, double v) override
     {
      if(!m_theta)
         return EMPTY_VALUE;

      double u_part = pow(1.0 - log(u),m_theta);
      double v_part = pow(1.0 - log(v),m_theta);
      double cuv = cdf(u,v);

      double numerator = cuv *pow(-1.0+u_part+v_part,1.0/m_theta)*v_part;
      double denominator = v*(-1.0+u_part+v_part)*(1.0-1.0*log(v));

      return (numerator+1.e-8)/(denominator+1.e-8);
     }
   virtual vector    pdf(vector &u,vector &v) override
     {
      vector Cuv,u_part,v_part,numerator,denominator;
      Cuv = cdf(u,v);
      u_part = pow(1.0 - log(u),m_theta);
      v_part = pow(1.0 - log(v),m_theta);
      numerator = Cuv * u_part *v_part * (-1.0+m_theta + pow(-1.0+u_part +v_part,1.0/m_theta))*pow(-1.0+u_part+v_part,1.0/m_theta);
      denominator = u*v*(1.0-1.0*log(u))*(1.0 - log(v))*pow(-1.0+u_part+v_part,2.0);
      return (numerator+1.e-8)/(denominator+1.e-8);
     }
   virtual vector    cdf(vector &u, vector &v) override
     {
      vector u_part = pow(1.0-1.0*log(u),m_theta);
      vector v_part = pow(1.0-1.0*log(v),m_theta);
      vector cdf = exp(1.0 - pow(-1.0+u_part+v_part,1.0/m_theta));
      return cdf;
     }
   virtual vector    condi_cdf(vector &u, vector &v) override
     {

      vector u_part = pow(1.0 - log(u),m_theta);
      vector v_part = pow(1.0 - log(v),m_theta);
      vector cuv = cdf(u,v);

      vector numerator = cuv *pow(-1.0+u_part+v_part,1.0/m_theta)*v_part;
      vector denominator = v*(-1.0+u_part+v_part)*(1.0-1.0*log(v));

      return (numerator+1.e-8)/(denominator+1.e-8);
     }
public:
                     CN13(void)
     {
      m_threshold = 1.e-10;
      m_bounds[0] = 0.0;
      m_bounds[1] = DBL_MAX;
      m_copula_type = N13_COPULA;
     }
                    ~CN13(void)
     {
     }
   virtual matrix    Sample(ulong num_samples) override
     {
      double unf_v[],unf_c[];

      vector v,c,u;

      if(!MathRandomUniform(0.0,1.0,int(num_samples),unf_v) ||
         !MathRandomUniform(0.0,1.0,int(num_samples),unf_c) ||
         !v.Assign(unf_v) || !c.Assign(unf_c))
        {
         Print(__FUNCTION__, " failed to get uniform random numbers ", GetLastError());
         return matrix::Zeros(0,0);
        }

      matrix out(num_samples,2);

      for(ulong irow = 0; irow<num_samples; irow++)
         out.Row(generate_pair(v[irow],c[irow]),irow);

      return out;

     }

  };
//+------------------------------------------------------------------+

Далее приведена визуализация набора данных, сгенерированного из копулы N13.

Двумерная выборка N13


Двумерная копула N14

Копула N14 — ещё одно архимедово однопараметрическое семейство, которое также моделирует только положительную зависимость, но допускает хвостовую зависимость в обоих углах. Она создаёт верхнюю хвостовую зависимость и обычно более слабую нижнюю хвостовую зависимость по мере удаления параметра θ от независимости. Копула N14 может улавливать тенденцию совместных экстремально высоких значений и, в меньшей степени, совместных экстремально низких значений. Зависимость усиливается с ростом параметра, а на нижней границе копула стремится к независимости. Эмпирические исследования финансовых пар часто показывают, что N14 хорошо соответствует реальным данным благодаря этим асимметричным хвостовым свойствам. Копула N14 имеет следующую функцию-генератор:

Функция-генератор n14

Класс CN14, определённый в n14.mqh, реализует копулу N14 в MQL5. 

//+------------------------------------------------------------------+
//|                                                          n14.mqh |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#include "base.mqh"
//+------------------------------------------------------------------+
//| N14 Copula (Nelsen 14)                                           |
//+------------------------------------------------------------------+
class CN14:public CBivariateCopula
  {
private:
   class CBrent:public CBrentQ
     {
   public:
                     CBrent(void)
        {
        }
                    ~CBrent(void)
        {
        }
      virtual double objective(double u, double v,double y) override
        {
         return ((-1.0*u) * (-2.0 + pow(u,1.0/v))) - y;
        }
     };
   virtual double    theta_hat(const double tau) override
     {
      return (1.0 + tau)/(2.0 - 2.0*tau);
     }
   vector            generate_pair(double v1, double v2)
     {
      vector out(2);
      double w = 0.0;
      if(v2>m_threshold)
        {
         CBrent brent;
         w = brent.minimize(m_theta,v2,m_threshold,1.0-m_threshold,2.e-12,4.0*2.e-16,100);
        }
      else
         w = m_threshold;
      out[0] = pow(1.0 + pow(v1 * pow(pow(w,-1.0/m_theta)-1.0,m_theta),1.0/m_theta),-1.0*m_theta);
      out[1] = pow(1.0 + pow((1.0-v1) * pow(pow(w,-1.0/m_theta)-1.0,m_theta),1.0/m_theta),-1.0*m_theta);
      return out;
     }
protected:
   virtual double    pdf(double u,double v) override
     {
      double u_ker,v_ker,u_part,v_part,cdf_ker,numerator,denominator;
      u_ker = -1.0+pow(u,1.0/m_theta);
      v_ker = -1.0+pow(v,1.0/m_theta);
      u_part = pow(-1.0 + pow(u,-1.0/m_theta),m_theta);
      v_part = pow(-1.0 + pow(v,-1.0/m_theta),m_theta);
      cdf_ker = 1.0 + pow(u_part+v_part,1.0/m_theta);
      numerator = (u_part * v_part *(cdf_ker - 1.0)*(-1.0 + m_theta + 2.0*m_theta*(cdf_ker-1.0)));
      denominator = pow(u_part + v_part,2.0) * pow(cdf_ker,(2.0 + m_theta))* u * v * u_ker * v_ker * m_theta;

      return (numerator+1.e-8)/(denominator+1.e-8);
     }
   virtual double    cdf(double u, double v) override
     {
      double u_part = pow(-1.0+pow(u,-1.0/m_theta),m_theta);
      double v_part = pow(-1.0+pow(v,-1.0/m_theta),m_theta);
      double cdf = pow(1.0 + pow(u_part+v_part,1.0/m_theta),-1*m_theta);
      return cdf;
     }
   virtual double    condi_cdf(double u, double v) override
     {
      double v_ker = -1.0 + pow(v, -1.0 / m_theta);
      double u_part = pow(-1.0 + pow(u, -1.0 / m_theta),m_theta);
      double v_part = pow(-1.0 + pow(v, -1.0 / m_theta),m_theta);
      double cdf_ker = 1.0 + pow(u_part + v_part, (1.0/ m_theta));

      double numerator = v_part * (cdf_ker - 1.0);
      double denominator = pow(v, (1.0 + 1.0 / m_theta)) * v_ker * (u_part + v_part) * pow(cdf_ker,1.0 + m_theta);

      return (numerator+1.e-8)/(denominator+1.e-8);
     }
   virtual vector    pdf(vector&u,vector&v) override
     {
      vector u_ker,v_ker,u_part,v_part,cdf_ker,numerator,denominator;
      u_ker = -1.0+pow(u,1.0/m_theta);
      v_ker = -1.0+pow(v,1.0/m_theta);
      u_part = pow(-1.0 + pow(u,-1.0/m_theta),m_theta);
      v_part = pow(-1.0 + pow(v,-1.0/m_theta),m_theta);
      cdf_ker = 1.0 + pow(u_part+v_part,1.0/m_theta);
      numerator = (u_part * v_part *(cdf_ker - 1.0)*(-1.0 + m_theta + 2.0*m_theta*(cdf_ker-1.0)));
      denominator = pow(u_part + v_part,2.0) * pow(cdf_ker,(2.0 + m_theta))* u * v * u_ker * v_ker * m_theta;

      return (numerator+1.e-8)/(denominator+1.e-8);
     }
   virtual vector    cdf(vector&u, vector&v) override
     {
      vector u_part = pow(-1.0+pow(u,-1.0/m_theta),m_theta);
      vector v_part = pow(-1.0+pow(v,-1.0/m_theta),m_theta);
      vector cdf = pow(1.0 + pow(u_part+v_part,1.0/m_theta),-1*m_theta);
      return cdf;
     }
   virtual vector    condi_cdf(vector&u, vector&v) override
     {
      vector v_ker = -1.0 + pow(v, -1.0 / m_theta);
      vector u_part = pow(-1.0 + pow(u, -1.0 / m_theta),m_theta);
      vector v_part = pow(-1.0 + pow(v, -1.0 / m_theta),m_theta);
      vector cdf_ker = 1.0 + pow(u_part + v_part, (1.0/ m_theta));

      vector numerator = v_part * (cdf_ker - 1.0);
      vector denominator = pow(v, (1.0 + 1.0 / m_theta)) * v_ker * (u_part + v_part) * pow(cdf_ker,1.0 + m_theta);

      return (numerator+1.e-8)/(denominator+1.e-8);
     }
public:
                     CN14(void)
     {
      m_threshold = 1.e-10;
      m_bounds[0] = 1.0;
      m_bounds[1] = DBL_MAX;
      m_copula_type = N14_COPULA;
     }
                    ~CN14(void)
     {
     }
   virtual matrix    Sample(ulong num_samples) override
     {
      double unf_v[],unf_c[];

      vector v,c,u;

      if(!MathRandomUniform(0.0,1.0,int(num_samples),unf_v) ||
         !MathRandomUniform(0.0,1.0,int(num_samples),unf_c) ||
         !v.Assign(unf_v) || !c.Assign(unf_c))
        {
         Print(__FUNCTION__, " failed to get uniform random numbers ", GetLastError());
         return matrix::Zeros(0,0);
        }

      matrix out(num_samples,2);

      for(ulong irow = 0; irow<num_samples; irow++)
         out.Row(generate_pair(v[irow],c[irow]),irow);

      return out;

     }
  };
//+------------------------------------------------------------------+

На диаграмме рассеяния ниже показан набор данных, сгенерированный из копулы N14.

Двумерная выборка N14

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


Интерпретация операций с копулами

Установлено, что копула описывает зависимость между переменными. Параметры копулы описывают силу этой зависимости, что достаточно легко понять. Поскольку копулы основаны на теории вероятностей, мы также можем выводить определённые характеристики отдельных переменных на основе этой характеристики зависимости, заданной моделью. Это делается путём вычисления CDF, PDF и условной CDF копулы. Такой анализ проводится для переменных, представляющих квантили или процентили исходной величины. Квантили и процентили — это меры положения в статистике, используемые для разделения отсортированного набора данных на равные части. Они помогают проиллюстрировать распределение и относительное положение конкретной точки данных.

Для заданных квантилей u и v функция распределения копулы C(u,v) представляет совместную вероятность: P(U≤u,V≤v). Она количественно оценивает вероятность того, что одна переменная находится ниже своего u-квантиля, а другая — ниже своего v-квантиля, с учётом их взаимной зависимости. Например, вычисление копулы при u=0.8, v=0.9 показывает вероятность того, что одна переменная окажется в нижних 80% своего распределения, а другая — в нижних 90%. В коде это выполняется методом Copula_CDF() объекта копулы.

Дополнением к CDF является функция плотности вероятности копулы c(u,v), вычисляемая методом Copula_PDF(). Если CDF даёт накопленную массу вероятности до некоторой точки, то PDF показывает, насколько плотно эта масса вероятности сосредоточена в самой точке. Высокое значение PDF означает, что конкретная комбинация исходов (u,v) весьма вероятна. Более конкретно, PDF копулы задаётся следующей формулой.

PDF копулы

Высокое значение PDF означает сильную локальную зависимость между переменными относительно случая независимости. Низкие или близкие к нулю значения означают маловероятные совместные исходы.

Условную вероятность можно вычислить, найдя частную производную CDF копулы по одной переменной; её часто обозначают как C(v∣u) или C(u∣v). Это отражает условное поведение одного квантиля при фиксированном значении другого на определённом уровне. В контексте управления рисками можно спросить: «Если доходность актива A находится на 10-м процентиле (u=0.10), какова вероятность того, что доходность актива B находится на своём 5-м процентиле или ниже (V≤0.05)?». Это реализовано методом Conditional_Probability(). Первый вход метода соответствует переменной, удерживаемой фиксированной, а второй задаёт условный квантиль. Наконец, метод Inv_Conditional_Probability() предоставляет обратную функцию условной CDF. По заданной условной вероятности y и условному квантилю v он находит u такое, что C(v∣u)=y. В следующем разделе мы посмотрим, как применять вывод на основе копул при построении стратегий парного трейдинга.


Стратегии парного трейдинга на основе копул

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

Сигналы копулы

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

При работе с нестационарными временными рядами преобразование к равномерному распределению (через интегральное преобразование вероятности) может не дать данных, независимых и одинаково распределённых (i.i.d.) в равномерном пространстве, что обычно является входным требованием для оценивания копулы. Стационарность помогает обеспечить более чёткое разделение между маргинальным поведением и структурой зависимости, что является главным преимуществом подхода с копулами. Если маргинальные распределения нестационарны, оценённая копула может неявно улавливать нестационарную динамику маргиналов, смешивая эти два компонента. Несмотря на эти ограничения, в академической литературе много примеров стратегий парного трейдинга, основанных на моделях копул.

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


Выбор пар для торговли

При выборе символов для парного трейдинга на основе копул можно применять разные методы, включая методы, используемые в традиционных стратегиях парного трейдинга. К ним относятся коинтеграция и метод расстояния. Коинтеграция обычно считается наиболее строгим методом. Цель состоит в том, чтобы найти два нестационарных ценовых ряда, линейная комбинация которых стационарна. К ценовым рядам применяются тесты вроде двухшагового теста Энгла — Грейнджера или теста Йохансена. Стационарный спред подразумевает стабильное долгосрочное равновесное отношение, которому цены в целом будут следовать. Метод расстояния (сумма квадратов расстояний — SSD) — более простой непараметрический подход.

Сумма квадратов расстояний

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

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

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

Высокая согласованность указывает на сильное совместное движение. Выбираются пары с наибольшими значениями тау Кендалла или ро Спирмена. Высокое значение (близкое к 1) показывает сильную тенденцию цен символов двигаться вместе, что говорит о долгосрочной связи, которую может использовать парная сделка. Скрипт MetaTrader 5 Concordance.ex5 принимает список символов и рассчитывает пару с наибольшей мерой согласованности.

//+------------------------------------------------------------------+
//|                                                  Concordance.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property script_show_inputs
#include<dependence.mqh>
input string   input_symbols = "AUDUSD,NZDUSD,XAUUSD,XAGUSD,EURUSD,XAUEUR";
input datetime input_start_date = D'2025.01.01 00:00';
input ulong    input_count_bars = 260;
input ENUM_TIMEFRAMES input_time_frame = PERIOD_D1;
input ENUM_DEPENDENCE_MEASURE dependence_measure = KENDAL_TAU;
input bool Use_Returns = false;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   string test_symbols[];
   int num_symbols = StringSplit(input_symbols,StringGetCharacter(",",0),test_symbols);
   if(StringFind(input_symbols,",",StringLen(input_symbols)-1) == StringLen(input_symbols)-1)
     {
      --num_symbols;
      ArrayRemove(test_symbols,uint(num_symbols),1);
     }

   if(num_symbols<2)
     {
      Print(" 2 or more symbols expected ");
      return;
     }

   matrix prices = matrix::Zeros(input_count_bars,ulong(num_symbols));
   vector dwnload;
   for(ulong i = 0; i<prices.Cols(); ++i)
     {
      int try = 10;
      ResetLastError();
      while((!dwnload.CopyRates(test_symbols[i],input_time_frame,COPY_RATES_CLOSE,input_start_date,input_count_bars) ||
         !prices.Col(dwnload,i)) && try)
       {
         Sleep(10000);
         --try;
       }
      /*if(StringFind(test_symbols[i],"XAUEUR")>=0)
        {
         prices.Col(dwnload*prices.Col(i-1),i);
        }*/
      if(GetLastError())
       {
        Print("Could not download data");
        return;
       }
     }
     
   if(Use_Returns)
     {
      prices = log(prices);
      prices = np::diff(prices,1,false);
     }

   matrix dep_mat = dependence(prices,dependence_measure);
   Print(dep_mat);
   Print(EnumToString(dependence_measure), "\n","Using ", Use_Returns?"Returns":"Raw prices");
   if(test_symbols.Size()>2)
     {
      matrix mm = dep_mat.TriL(-1);
      int index = int(mm.ArgMax());
      Print("Pair with highest dependence: ", test_symbols[int(index/int(dep_mat.Cols()))], " and ", test_symbols[int(MathMod(index,dep_mat.Cols()))]);
     }
  }
//+------------------------------------------------------------------+

Ниже приведён вывод запуска с использованием следующей вселенной символов: AUDUSD, NZDUSD, XAUUSD, XAGUSD, EURUSD и XAUEUR.

Вывод скрипта Concordance

Помимо статистических измерений, обычно желательно, чтобы зависимость выбранной пары имела экономическое обоснование. Если посмотреть на результаты тестового запуска скрипта Concordance.ex5, у XAUEUR и XAUUSD оказалась наибольшая мера совместного движения. Зависимость этой пары легко объяснить тем, что оба инструмента оба инструмента основаны на одном и том же базовом активе. Следовательно, у нас есть подходящая пара символов для торговли; следующий шаг — выбрать модель копулы, которая лучше всего соответствует характеристикам зависимости этих символов.


Выбор модели копулы

Основные методы выбора подходящей копулы включают критерии выбора модели и тесты согласия. Критерии выбора модели, или информационные критерии, уравновешивают качество подгонки модели с её сложностью (числом параметров). Более низкое значение этих критериев обычно указывает на лучшую модель. Наиболее распространённые из них — информационный критерий Акаике (AIC), байесовский информационный критерий (BIC) и информационный критерий Ханнана — Куинна (HQIC). Этот метод следует трёхшаговой процедуре. Сначала оцениваются параметры нескольких кандидатных моделей копул и рассчитывается оценка логарифмического правдоподобия каждой модели. Следующий шаг — вычислить информационные критерии для каждой кандидатной модели, используя AIC, BIC или HQIC. Последний шаг сравнивает значения информационных критериев всех кандидатных моделей и выбирает модель с наименьшим значением.

В качестве альтернативы подходящую копулу можно выбрать с помощью тестов согласия (Goodness-of-Fit, GoF). Эти тесты статистически определяют, согласуется ли структура зависимости, захваченная конкретной моделью копулы, с наблюдаемыми данными. Это формальные проверки гипотез. Нулевая гипотеза формулируется так: данные сгенерированы предполагаемым семейством копул. Примеры используемых тестовых статистик включают:

  • Статистика Крамера — фон Мизеса: измеряет квадрат разности между эмпирической и параметрической копулой.
  • Статистика Колмогорова — Смирнова: измеряет максимальную абсолютную разность между эмпирической и параметрической копулой.

Для моделирования нулевого распределения и расчёта p-значения часто используется параметрическая бутстрэп-процедура. Высокое p-значение означает, что нулевая гипотеза не может быть отвергнута, то есть копула является правдоподобной моделью для данных. Нативные реализации статистик Колмогорова — Смирнова и Крамера — фон Мизеса для копул в MQL5 недоступны, поэтому нам приходится полагаться на метод критериев выбора модели.

//+------------------------------------------------------------------+
//|                                              CopulaSelection.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property script_show_inputs
#include <Copulas\Bivariate\bivariate_copula.mqh>
#include <ECDF\linear_cdf.mqh>
//--- input parameters
input string   FirstSymbol="XAUUSD";
input string   SecondSymbol="XAUEUR";
input datetime TrainingDataStart=D'2025.01.01';
input ulong    HistorySize=260;
input bool     SaveModel = false;
input string   EcdfModel = "model.ecdf";
input string   CopulaModel = "model.copula";
//---
//string   NormalizingSymbol="EURUSD";
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   vector p_1,p_2,p_3,p_n;
   matrix pdata,pobs;
   if(!p_1.CopyRates(FirstSymbol,PERIOD_CURRENT,COPY_RATES_CLOSE,TrainingDataStart,HistorySize) ||
      !p_2.CopyRates(SecondSymbol,PERIOD_CURRENT,COPY_RATES_CLOSE,TrainingDataStart,HistorySize) ||
      p_1.Size()!=p_2.Size() ||
      !pdata.Resize(p_1.Size(),2) ||
      !pdata.Col(p_1,0) ||
      !pdata.Col(p_2,1))
     {
      Print(" failed to collect and initialize rates matrix ", GetLastError());
      return;
     }
//---
   CLinearCDF qt();
   if(!qt.fit(pdata))
      return;
//---
   pobs = qt.to_quantile(pdata);
//---
   if(SaveModel)
     {
      CFileBin file;
      file.Open(EcdfModel,FILE_WRITE|FILE_BIN|FILE_COMMON);
      if(!qt.save(file.Handle()))
         Print(" Failed to save ", EcdfModel);
      file.Close();
     }
//---
   vector lowest = vector::Zeros(8);
   CBivariateCopula *bcop[8];
//---
   bcop[0] = new CClayton();
   bcop[1] = new CFrank();
   bcop[2] = new CGumbel();
   bcop[3] = new CJoe();
   bcop[4] = new CN13();
   bcop[5] = new CN14();
   bcop[6] = new CGaussian();
   bcop[7] = new CStudent();
//---
   for(uint i = 0; i<bcop.Size(); ++i)
     {
      bool fitted = bcop[i].Fit(pobs.Col(0),pobs.Col(1));
      //---
      if(fitted)
        {
         double ll = bcop[i].Log_likelihood(pobs.Col(0),pobs.Col(1));
         lowest[i] = aic(ll,int(pobs.Rows()));
         Print(EnumToString((ENUM_COPULA_TYPE)bcop[i].Type()));
         Print(" sic ", sic(ll,int(pobs.Rows())));
         Print(" aic ", lowest[i]);
         Print(" hqic ", hqic(ll,int(pobs.Rows())));
        }
     }
//---
   if(SaveModel)
     {
      ulong shift = lowest.ArgMin();
      CFileBin file;
      file.Open(CopulaModel,FILE_WRITE|FILE_BIN|FILE_COMMON);
      if(!bcop[shift].Save(file.Handle()))
         Print("Failed to save ", CopulaModel);
      file.Close();
     }
//---
   for(uint i = 0; i<bcop.Size(); delete bcop[i], ++i);
  }
//+------------------------------------------------------------------+

Скрипт CopulaSelection.ex5 позволяет пользователям указать пару символов, начальную дату и количество баров, которые определяют выборки для построения кандидатных моделей копул. Скрипт рассчитывает логарифмическое правдоподобие для каждой кандидатной модели копулы, а затем вычисляет её информационные критерии. Скрипт позволяет сохранить выбранную модель копулы вместе с соответствующей моделью эмпирической функции распределения. Результаты выводятся на вкладку журнала MetaTrader 5 для ознакомления пользователя. Ниже приведён вывод запуска, в котором скрипт применялся к символам XAUUSD и XAUEUR.

Вывод скрипта CopulaSelection

Вывод скрипта ясно показывает, что копула Франка имеет наименьшие информационные критерии, что указывает на неё как на наиболее подходящую модель для выборочного набора данных. Сохранённые модели будут использоваться в индикаторе и советнике, реализующих парную стратегию на основе копул.


Пример стратегии парного трейдинга на основе копул

Стратегия, которую мы реализуем, является модифицированной версией стратегии, описанной в Journal of Derivatives & Hedge Funds авторами Liew, R.Q. и Wu, Y., с использованием подхода, изложенного Hudson and Thames. Она опирается на модель копулы, обученную на выборке ценовых данных, чтобы уловить зависимость между парами. Идея состоит в использовании условных вероятностей, рассчитанных по квантилям текущих цен, для генерации торговых сигналов. Логика входа такова.

  • Если C(u_1 | u_2) <= 0.05 и C(u_2 | u_1) >= 0.95, то Symbol 1 считается недооценённым, а Symbol 2 — переоценённым; это формирует одновременный сигнал на покупку Symbol 1 и продажу Symbol 2.
  • Если C(u_1 | u_2) >=  0.95 и C(u_2 | u_1) <= 0.05, то Symbol 2 считается недооценённым, а Symbol 1 — переоценённым; это запускает одновременный сигнал на продажу Symbol 1 и покупку Symbol 2.

Переменные u_1 и u_2 представляют квантили для Symbol 1 и Symbol 2 соответственно. Позиции закрываются одним из двух способов.

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

Стратегия реализована исключительно из любопытства. Теоретически должно быть очевидно, что как только цены упадут ниже или поднимутся выше максимальных или минимальных уровней, наблюдавшихся в обучающих данных, модель быстро перестанет работать и утратит практическую ценность. Цель реализации этой стратегии — проверить, способна ли копула сигнализировать о торговых возможностях, которые можно практически использовать. Для этого стратегия будет протестирована на том же периоде, который использовался для обучения модели копулы (тестирование на обучающей выборке). С этой целью был создан индикатор GoldCopulaSignals.ex5. Индикатор загружает эмпирические CDF и модели копул, сохранённые скриптом CopulaSelection.ex5, и отображает условные вероятности, рассчитанные по квантилям символов XAUEUR и XAUUSD.

//+------------------------------------------------------------------+
//|                                            GoldCopulaSignals.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 4
#property indicator_plots   4
#property indicator_maximum 1.1
#property indicator_minimum -0.1
#property indicator_level1  0.05
#property indicator_level2  0.5
#property indicator_level3  0.95
#include<Copulas\Bivariate\bivariate_copula.mqh>
#include<ECDF\linear_cdf.mqh>
//--- plot S1
#property indicator_label1  "S1"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrRed
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- plot S2
#property indicator_label2  "S2"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrBlue
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1
//---
#property indicator_label3  "EURGold"
#property indicator_type3   DRAW_NONE

//---
#property indicator_label4  "USDGold"
#property indicator_type4  DRAW_NONE

//--- input parameters
input ENUM_COPULA_TYPE Copulatype = FRANK_COPULA;
input string   CopulaModelName = "model.copula";
input string   ECDFModelName = "model.ecdf";
//---
string   FirstSymbol="XAUUSD";
string   SecondSymbol="XAUEUR";
//--- indicator buffers
double         S1Buffer[];
double         S2Buffer[];
double         S3Buffer[];
double         S4Buffer[];
matrix         mat_;
CLinearCDF* qt;
CBivariateCopula *bcop;
CFileBin savedcop,savedcdf;
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {

//---
   qt = new CLinearCDF();
//---
   switch(Copulatype)
     {
      case CLAYTON_COPULA:
         bcop = new CClayton();
         break;
      case FRANK_COPULA:
         bcop = new CFrank();
         break;
      case GUMBEL_COPULA:
         bcop = new CGumbel();
         break;
      case JOE_COPULA:
         bcop = new CJoe();
         break;
      case N13_COPULA:
         bcop = new CN13();
         break;
      case N14_COPULA:
         bcop = new CN14();
         break;
      case GAUSSIAN_COPULA:
         bcop = new CGaussian();
         break;
      case STUDENT_COPULA:
         bcop = new CStudent();
         break;
     }
//---
   savedcop.Open(CopulaModelName,FILE_READ|FILE_BIN|FILE_COMMON);
   if(!bcop.Load(savedcop.Handle()))
     {
      Print("Failed to load copula model ", CopulaModelName, " ", GetLastError());
      return INIT_FAILED;
     }
//---
   if((ENUM_COPULA_TYPE)bcop.Type() != Copulatype)
     {
      Print("Invalid copula model. Instantiated copula is ", EnumToString((ENUM_COPULA_TYPE)bcop.Type()),"\n but specified copula in input settings is ", EnumToString(Copulatype));
      return INIT_FAILED;
     }
//---
   savedcop.Close();
//---
   savedcdf.Open(ECDFModelName,FILE_READ|FILE_BIN|FILE_COMMON);
   if(!qt.load(savedcdf.Handle()))
     {
      Print("Failed to load the ECDF model ", ECDFModelName, " ", GetLastError());
      return INIT_FAILED;
     }
//---
   savedcdf.Close();
//---
   mat_.Resize(1,2);
//--- indicator buffers mapping
   SetIndexBuffer(0,S1Buffer,INDICATOR_DATA);
   SetIndexBuffer(1,S2Buffer,INDICATOR_DATA);
   SetIndexBuffer(2,S3Buffer,INDICATOR_DATA);
   SetIndexBuffer(3,S4Buffer,INDICATOR_DATA);
//---
   ArraySetAsSeries(S1Buffer,true);
   ArraySetAsSeries(S2Buffer,true);
   ArraySetAsSeries(S3Buffer,true);
   ArraySetAsSeries(S4Buffer,true);
//---
   PlotIndexSetString(0,PLOT_LABEL,FirstSymbol);
   PlotIndexSetString(1,PLOT_LABEL,SecondSymbol);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| On deinitialization                                              |
//+------------------------------------------------------------------+
void  OnDeinit(const int  reason)
  {
//---
   if(CheckPointer(qt) == POINTER_DYNAMIC)
      delete qt;
//---
   if(CheckPointer(bcop) == POINTER_DYNAMIC)
      delete bcop;
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t rates_total,
                const int32_t 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 int32_t &spread[])
  {
//---
   int32_t limit;
   if(prev_calculated<=0)
      limit= rates_total-2;
   else
      limit=rates_total-prev_calculated;
//---
   for(int32_t i =limit; i >=0 ; --i)
     {
      mat_[0,0] = iClose(FirstSymbol,PERIOD_CURRENT,i);
      mat_[0,1] = iClose(SecondSymbol,PERIOD_CURRENT,i);
      S3Buffer[i] = mat_[0,1];
      S4Buffer[i] = mat_[0,0];
      mat_ = qt.to_quantile(mat_);
      S1Buffer[i] = bcop.Conditional_Probability(mat_[0][0],mat_[0][1]);
      S2Buffer[i] = bcop.Conditional_Probability(mat_[0][1],mat_[0][0]);
     }
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

На скриншоте ниже показано, как выглядит индикатор (подокно).

Индикатор GoldCopulaSignals

Синяя линия представляет условные вероятности символа XAUEUR, а красная линия — вероятности XAUUSD. Сама стратегия реализована в виде советника SimpleCopulaPairsStrategy.ex5.

//+------------------------------------------------------------------+
//|                                    SimpleCopulaPairsStrategy.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Trade\Trade.mqh>
#include <Trade\SymbolInfo.mqh>
#include <Trade\AccountInfo.mqh>
#include <TimeOptions.mqh>
#include<Copulas\Bivariate\base.mqh>
#resource "\\Indicators\\GoldCopulaSignals.ex5"
//---
#define USDGOLD "XAUUSD"
#define EURGOLD "XAUEUR"
//--- input parameters
enum ENUM_EXIT_RULE
  {
   AND_EXIT=0,//"And exit"
   OR_EXIT//Or exit
  };
//--- input parameters
input ENUM_COPULA_TYPE Copulatype = FRANK_COPULA;
input string   CopulaModelName = "model.copula";
input string   ECDFModelName = "model.ecdf";
input double   OpenThresholdProbability_First = 0.05;
input double   OpenThresholdProbability_Second = 0.95;
input double   UpperExitThresholdProbability = 0.5;
input double   LowerExitThresholdProbability = 0.5;
input ENUM_EXIT_RULE ExitRule = OR_EXIT;
input ulong    SlippagePoints = 10;
input double   TradingLots = 0.01;
input ulong    MagicNumber = 18973984;
double   StopLossPoints = 0.0;
double   TakeProfitPoints = 0.0;
int  OpenShift = 1;
int  CloseShift = 1;
//---
ENUM_DAY_TIME_MINUTES TradeSessionOpen = 6;
ENUM_DAY_TIME_MINUTES TradeSessionClose = 1435;
//--- indicator buffers
CSymbolInfo usd,eur;
//--
double usd_sigs[2],eur_sigs[2];
int indicator_handle;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   if(!usd.Name(USDGOLD) || !eur.Name(EURGOLD))
     {
      Print("Symbol info config error, ");
      return INIT_FAILED;
     }
//---
   indicator_handle = INVALID_HANDLE;
   int try
         = 10;
   while(indicator_handle == INVALID_HANDLE && try
            >0)
        {
         indicator_handle = iCustom(NULL,PERIOD_CURRENT,"::Indicators\\GoldCopulaSignals.ex5",Copulatype,CopulaModelName,ECDFModelName);
         --try;
        }
//---
   if(indicator_handle == INVALID_HANDLE)
     {
      Print(" failed to initialize the indicator ", GetLastError());
      return INIT_FAILED;
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   datetime sessionopen = iTime(NULL,PERIOD_D1,0) + int(TradeSessionOpen*60);
   datetime sessionclose = iTime(NULL,PERIOD_D1,0) + int(TradeSessionClose*60);
//---
   if(TimeCurrent()<sessionopen || TimeCurrent()>=sessionclose)
      return;
//---
   if(SumMarketOrders(MagicNumber))
     {
      long exit = GetExitSignal(CloseShift);
      if(exit)
        {
         if(CloseAll(MagicNumber,NULL,WRONG_VALUE,SlippagePoints)!=2)
            Print("Failed to close all opened orders ");
        }
     }
   else
     {
      long entry = GetEntrySignal(OpenShift);
      if(entry>0)
        {
         if(!OpenOrder(usd,ORDER_TYPE_BUY,StopLossPoints,TakeProfitPoints,TradingLots,SlippagePoints,MagicNumber))
            Print(" Failed long entry for ", usd.Name());
         if(!OpenOrder(eur,ORDER_TYPE_SELL,StopLossPoints,TakeProfitPoints,TradingLots,SlippagePoints,MagicNumber))
            Print(" Failed short entry for ", eur.Name());
        }
      else
         if(entry<0)
           {
            if(!OpenOrder(eur,ORDER_TYPE_BUY,StopLossPoints,TakeProfitPoints,TradingLots,SlippagePoints,MagicNumber))
               Print(" Failed long entry for ", eur.Name());
            if(!OpenOrder(usd,ORDER_TYPE_SELL,StopLossPoints,TakeProfitPoints,TradingLots,SlippagePoints,MagicNumber))
               Print(" Failed short entry for ", usd.Name());
           }
     }
  }
//+------------------------------------------------------------------+
//| get the entry signal                                             |
//+------------------------------------------------------------------+
long GetEntrySignal(int shift = 1)
  {
   static datetime last;
   if(shift>0)
     {
      if(iTime(NULL,PERIOD_CURRENT,0)<=last)
         return 0;
      else
         last = iTime(NULL,PERIOD_CURRENT,0);
     }
   else
     {
      datetime last = (datetime)MathMax(eur.Time(),usd.Time());
      if(TimeCurrent()<=last)
         return 0;
      //---
      eur.RefreshRates();
      usd.RefreshRates();
     }
//---
   if(CopyBuffer(indicator_handle,0,shift,2,usd_sigs)<2 ||
      CopyBuffer(indicator_handle,1,shift,2,eur_sigs)<2)
     {
      Print(__FUNCTION__, " error copying from indicator buffers ");
      return 0;
     }
   double current_prob_first,current_prob_second;
//---
   current_prob_first = usd_sigs[1];
   current_prob_second = eur_sigs[1];
//---
   if(current_prob_first<=OpenThresholdProbability_First && current_prob_second>=OpenThresholdProbability_Second)
     {
      //Print(__FUNCTION__, " go long ");
      return 1;
     }
   else
      if(current_prob_first>=OpenThresholdProbability_Second && current_prob_second<=OpenThresholdProbability_First)
        {
         //Print(__FUNCTION__, " go short ");
         return -1;
        }
//---
   return 0;
  }
//+------------------------------------------------------------------+
//| get the exit signal                                              |
//+------------------------------------------------------------------+
long GetExitSignal(int shift = 1)
  {
   datetime last = (datetime)MathMax(eur.Time(),usd.Time());

   if(shift>0)
     {
      if(iTime(NULL,PERIOD_CURRENT,0)<=last)
         return 0;
     }
   else
     {
      if(TimeCurrent()<=last)
         return 0;
     }
//---
   eur.RefreshRates();
   usd.RefreshRates();

//---
   if(CopyBuffer(indicator_handle,0,shift,2,usd_sigs)<2 ||
      CopyBuffer(indicator_handle,1,shift,2,eur_sigs)<2)
     {
      Print(__FUNCTION__, " error copying from indicator buffers ");
      return 0;
     }
//---
   double prev_prob_first,prev_prob_second;
   double current_prob_first,current_prob_second;
//---
   prev_prob_first = usd_sigs[0];
   prev_prob_second = eur_sigs[0];
//---
   current_prob_first = usd_sigs[1];
   current_prob_second = eur_sigs[1];
//---
   bool u1_up,u2_dwn,u2_up,u1_dwn;
   u1_up = (prev_prob_first<= LowerExitThresholdProbability && current_prob_first>=UpperExitThresholdProbability);
   u1_dwn = (prev_prob_first>=UpperExitThresholdProbability && current_prob_first<=LowerExitThresholdProbability);
   u2_up = (prev_prob_second<=LowerExitThresholdProbability && current_prob_second>=UpperExitThresholdProbability);
   u2_dwn = (prev_prob_second>=UpperExitThresholdProbability && current_prob_second<=LowerExitThresholdProbability);

   if(ExitRule == AND_EXIT)
     {
      if(u1_up && u2_dwn)
         return -1;
      else
         if(u2_up && u1_dwn)
            return 1;
     }
   else
     {
      if(u1_up || u2_dwn)
         return -1;
      else
         if(u2_up || u1_dwn)
            return 1;
     }
   return 0;
  }
//+------------------------------------------------------------------+
//|Enumerate all market orders                                       |
//+------------------------------------------------------------------+
uint SumMarketOrders(ulong magic_number=ULONG_MAX,const string sym=NULL,const ENUM_POSITION_TYPE ordertype=WRONG_VALUE)
  {
   CPositionInfo posinfo;
//---
   uint count=0;
//---
   for(int i = PositionsTotal()-1; i>-1;--i)
     {
      if(!posinfo.SelectByIndex(i))
         continue;
      if(magic_number<ULONG_MAX && posinfo.Magic()!=long(magic_number))
         continue;
      if(sym!=NULL && posinfo.Symbol()!=sym)
         continue;
      if(ordertype!=WRONG_VALUE && posinfo.PositionType()!=ordertype)
         continue;
      count++;
     }
//---
   return count;
  }
//+------------------------------------------------------------------+
//|Enumerate all market orders                                       |
//+------------------------------------------------------------------+
ENUM_POSITION_TYPE LastOrderType(ulong magic_number=ULONG_MAX)
  {
   CPositionInfo posinfo;
//---
   ENUM_POSITION_TYPE direction_type = WRONG_VALUE;
   string symb = NULL;
//---
   for(int i = PositionsTotal()-1; i>-1;--i)
     {
      if(!posinfo.SelectByIndex(i))
         continue;
      if(magic_number<ULONG_MAX && posinfo.Magic()!=long(magic_number))
         continue;
      symb = posinfo.Symbol();
      direction_type = posinfo.PositionType();
      //---
      switch(direction_type)
        {
         case POSITION_TYPE_BUY:
           {
            if(symb == EURGOLD)
               return POSITION_TYPE_SELL;
            else
               if(symb == USDGOLD)
                  return POSITION_TYPE_BUY;
           }
         case POSITION_TYPE_SELL:
           {
            if(symb == EURGOLD)
               return POSITION_TYPE_BUY;
            else
               if(symb == USDGOLD)
                  return POSITION_TYPE_SELL;
           }
         default:
            return WRONG_VALUE;
        }
     }
//---
   return WRONG_VALUE;
  }
//+------------------------------------------------------------------+
//| Profit                                                           |
//+------------------------------------------------------------------+
double Profit(ulong magic_number=ULONG_MAX)
  {
   CPositionInfo posinfo;
//---
   double profit = 0.0;
//---
   for(int i = PositionsTotal()-1; i>-1;--i)
     {
      if(!posinfo.SelectByIndex(i))
         continue;
      if(magic_number<ULONG_MAX && posinfo.Magic()!=long(magic_number))
         continue;
      profit += (posinfo.Profit() + posinfo.Commission() + posinfo.Swap());
     }
//---
   return profit;
  }
//+------------------------------------------------------------------+
//|Open trade                                                        |
//+------------------------------------------------------------------+
bool OpenOrder(CSymbolInfo &syminfo, ENUM_ORDER_TYPE type,double stoplosspoints=0.0, double takeprofitpoints = 0.0,double lotsize=0.1,ulong slippage=10,ulong magic=12345678,string tradecomment=NULL)
  {
   CTrade trade;
//---
   CAccountInfo accinfo;
//---
   trade.SetExpertMagicNumber(magic); // magic
   trade.SetMarginMode();
   trade.SetDeviationInPoints(slippage);
//---
   if(!trade.SetTypeFillingBySymbol(syminfo.Name()))
     {
      Print(__FUNCTION__," Unknown Type filling mode for ", syminfo.Name());
      return false;
     }
//---
   bool ans=false;
   syminfo.RefreshRates();
   string sy = syminfo.Name();
//---
   double price = (type == ORDER_TYPE_BUY)?syminfo.Ask():syminfo.Bid();
//---
   price = NormalizeDouble(MathAbs(price),syminfo.Digits());
//---
   bool result = false;
//---
   switch(type)
     {
      case ORDER_TYPE_BUY:
         if(accinfo.FreeMarginCheck(sy,type,lotsize,price)<0.0)
           {
            Print("Insufficient funds to open long order ("+sy+"). Free Margin = ", accinfo.FreeMargin());
            return false;
           }
         result = trade.Buy(lotsize,sy,price,(stoplosspoints)?NormalizeDouble(price - MathAbs(stoplosspoints*syminfo.Point()),syminfo.Digits()):0.0,(takeprofitpoints)?NormalizeDouble(price + MathAbs(takeprofitpoints*syminfo.Point()),syminfo.Digits()):0.0,tradecomment);
         break;
      case ORDER_TYPE_SELL:
         if(accinfo.FreeMarginCheck(sy,type,lotsize,price)<0.0)
           {
            Print("Insufficient funds to open short order ("+sy+"). Free Margin = ", accinfo.FreeMargin());
            return false;
           }
         result = trade.Sell(lotsize,sy,price,(stoplosspoints)?NormalizeDouble(price + MathAbs(stoplosspoints*syminfo.Point()),syminfo.Digits()):0.0,(takeprofitpoints)?NormalizeDouble(price - MathAbs(takeprofitpoints*syminfo.Point()),syminfo.Digits()):0.0,tradecomment);
         break;
     }
//---
   if(!result)
      Print(__FUNCTION__, " ", trade.CheckResultRetcodeDescription());
//---
   return result;
  }
//+------------------------------------------------------------------+
//| Close market order                                               |
//+------------------------------------------------------------------+
bool CloseOrder(ulong ticket,ulong magic,ulong slp=10)
  {
//---
   CTrade trade;
//---
   trade.SetExpertMagicNumber(magic);
//---
   bool  result = trade.PositionClose(ticket,slp);
//---
   if(!result)
      Print(trade.CheckResultRetcodeDescription());
//---
   return result;
  }
//+------------------------------------------------------------------+
//|  Close all orders                                                |
//+------------------------------------------------------------------+
uint CloseAll(ulong magic_number=ULONG_MAX,const string sym=NULL,const ENUM_POSITION_TYPE ordertype=WRONG_VALUE,const ulong mslip=10)
  {
   CPositionInfo posinfo;
//---
   uint countclosed=0;
//---
   for(int i = PositionsTotal()-1; i>-1;--i)
     {
      if(!posinfo.SelectByIndex(i))
         continue;
      if(magic_number<ULONG_MAX && posinfo.Magic()!=long(magic_number))
         continue;
      if(sym!=NULL && posinfo.Symbol()!=sym)
         continue;
      if(ordertype!=WRONG_VALUE && posinfo.PositionType()!=ordertype)
         continue;
      if(CloseOrder(posinfo.Ticket(),mslip))
         countclosed++;
     }
//---
   return countclosed;
  }
//+------------------------------------------------------------------+

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

Ниже приведены результаты.

Отчёт советника

График Equity

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

Индикатор GoldCopulsSignals при разрушении модели

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


Заключение

В статье была представлена реализация распространённых двумерных архимедовых копул на чистом MQL5, а именно копул Франка, Джо, Гумбеля, Клейтона, N13 и N14. Мы обсудили тип зависимости, который способна улавливать каждая из них. Позже в статье мы увидели, как копулы можно применять при формировании стратегий парного трейдинга. Мы даже реализовали простую стратегию и посмотрели на результаты. В следующих статьях мы продолжим изучать копулы. Весь код, упомянутый в статье, доступен ниже.

Файлы или папки   Описание
MQL5/include/Copulas Эта папка содержит все заголовочные файлы, реализующие все модели копул.
MQL5/include/Brent Эта папка содержит заголовочный файл, используемый реализациями копул. 
MQL5/include/ECDF Папка содержит заголовочные файлы, связанные с реализацией эмпирической функции распределения
MQL5/include/dependence.mqh Этот заголовочный файл содержит реализации мер согласованности.
MQL5/include/info_criteria.mqh Этот файл определяет функции для расчёта AIC, HQIC и BIC. 
MQL5/include/np.mqh Этот заголовочный файл определяет различные вспомогательные функции для векторов и матриц. 
MQL5/scripts/Concordance.mq5 Этот скрипт используется для поиска пары символов с наибольшей мерой совместного движения среди набора кандидатов.
MQL5/scripts/CopulaSelection.mq5 Этот скрипт рассчитывает критерии выбора модели для всех реализованных копул.
MQL5/indicators/GoldCopulaSignals.mq5 Это индикатор, используемый для генерации сигналов входа и выхода для советника, описанного в тексте. 
MQL5/experts/SimpleCopulaPairsStrategy.mq5 Это советник, реализующий стратегию парного трейдинга на основе копул. 

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

Прикрепленные файлы |
Сеточный советник на клеточном автомате с онлайн-обучением в MQL5 Сеточный советник на клеточном автомате с онлайн-обучением в MQL5
В статье разобрана архитектура советника на клеточном автомате с 10 000 адаптирующихся параметров и независимым бинарным предиктором на горизонте 10 баров. Показано трёхуровневое онлайн-обучение, эволюция стратегий и валидация через кольцевой буфер и матрицу ошибок. Параметры входа сведены к Magic Number, торговые настройки вычисляются из ATR и пяти геномов. Тест EURUSD H1 дал ориентировочный Hit Rate около 58% против ~51% у фиксированной MLP.
Кодекс рыночных состояний в MQL5 (Часть 1): Побитовое обучение на примере Nvidia Кодекс рыночных состояний в MQL5 (Часть 1): Побитовое обучение на примере Nvidia
Мы начинаем новую серию статей, которая развивает наши предыдущие наработки, изложенные в серии о MQL5 Wizard, и продвигает их дальше по мере усиления нашего подхода к системной торговле и тестированию стратегий. В этой новой серии мы сосредоточимся на советниках, запрограммированных на удержание только одного типа позиций — преимущественно длинных. Сосредоточение на одном направлении торговли может упростить анализ, снизить сложность стратегии и дать важные наблюдения, особенно при работе с активами за пределами Forex. Поэтому в этой серии мы исследуем, эффективен ли такой подход для акций и других невалютных активов, где long-only-системы часто хорошо согласуются с подходом smart money и стратегиями институциональных участников.
Нейросети в трейдинге: Внимание, память и рыночные паттерны в GDformer Нейросети в трейдинге: Внимание, память и рыночные паттерны в GDformer
Статья разбирает архитектуру GDformer применительно к алгоритмическому трейдингу. Показано, как обучаемая память, Dictionary-based Cross-Attention и Similarity Branch помогают сопоставлять текущее состояние рынка с выученными режимами и оценивать степень надёжности интерпретации. Дана реализация прямого прохода механизма внимания в OpenCL с использование разреженных коэффициентов без повторного перенормирования, что повышает устойчивость модели и эффективность на длинных последовательностях.
Архитектура системы машинного обучения в MetaTrader 5 (Часть 4): Скрытый изъян пайплайна финансового ML — одновременность меток Архитектура системы машинного обучения в MetaTrader 5 (Часть 4): Скрытый изъян пайплайна финансового ML — одновременность меток
Узнайте, как исправить критический изъян в финансовом машинном обучении, который приводит к переобученным моделям и плохой работе в реальной торговле, — одновременность меток. При использовании метода тройного барьера (triple-barrier) обучающие метки перекрываются во времени, нарушая базовое предположение IID большинства ML-алгоритмов (алгоритмов машинного обучения). В статье показано практическое решение через взвешивание наблюдений: как измерять временное перекрытие торговых сигналов, рассчитывать взвешивание наблюдений с учётом уникальной информации и применять эти веса в scikit-learn для построения более устойчивых классификаторов. Освоение этих техник поможет сделать торговые модели более устойчивыми, надёжными и прибыльными.