Двумерные копулы в MQL5 (Часть 2): Реализация архимедовых копул в MQL5
Введение
В предыдущей статье мы представили реализации двумерных эллиптических копул в 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; } }; //+------------------------------------------------------------------+
Параметр θ (тета) определяется по выборкам данных путём оценки ранговой корреляции τ (тау Кендалла) с использованием следующего соотношения:

где 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 определяется функцией-генератором:

В 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.

Двумерная копула 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.

Если подсчитать копулы, которые мы реализовали к этому моменту, то теперь у нас есть достаточно широкий набор моделей, способных улавливать разные типы зависимостей в двумерных наборах данных. В следующем разделе мы обсудим, какие выводы можно делать из моделей зависимости, задаваемых копулами. Это поможет лучше понять, как их применять.
Интерпретация операций с копулами
Установлено, что копула описывает зависимость между переменными. Параметры копулы описывают силу этой зависимости, что достаточно легко понять. Поскольку копулы основаны на теории вероятностей, мы также можем выводить определённые характеристики отдельных переменных на основе этой характеристики зависимости, заданной моделью. Это делается путём вычисления 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 означает сильную локальную зависимость между переменными относительно случая независимости. Низкие или близкие к нулю значения означают маловероятные совместные исходы.
Условную вероятность можно вычислить, найдя частную производную 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.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.

Вывод скрипта ясно показывает, что копула Франка имеет наименьшие информационные критерии, что указывает на неё как на наиболее подходящую модель для выборочного набора данных. Сохранённые модели будут использоваться в индикаторе и советнике, реализующих парную стратегию на основе копул.
Пример стратегии парного трейдинга на основе копул
Стратегия, которую мы реализуем, является модифицированной версией стратегии, описанной в 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); } //+------------------------------------------------------------------+
На скриншоте ниже показано, как выглядит индикатор (подокно).

Синяя линия представляет условные вероятности символа 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 для тестирования. Важно, чтобы читатели понимали: показанные здесь результаты могут отличаться от результатов при воспроизведении этого теста из-за небольших различий в котировках символов.
Ниже приведены результаты.


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

Несмотря на это, результаты обнадёживают и дают стимул для дальнейшего изучения.
Заключение
В статье была представлена реализация распространённых двумерных архимедовых копул на чистом 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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Сеточный советник на клеточном автомате с онлайн-обучением в MQL5
Кодекс рыночных состояний в MQL5 (Часть 1): Побитовое обучение на примере Nvidia
Нейросети в трейдинге: Внимание, память и рыночные паттерны в GDformer
Архитектура системы машинного обучения в MetaTrader 5 (Часть 4): Скрытый изъян пайплайна финансового ML — одновременность меток
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования