English 中文 Español Deutsch 日本語 Português
preview
Возможности Мастера MQL5, которые вам нужно знать (Часть 04): Линейный дискриминантный анализ

Возможности Мастера MQL5, которые вам нужно знать (Часть 04): Линейный дискриминантный анализ

MetaTrader 5Торговые системы | 10 января 2023, 12:48
1 410 0
Stephen Njuki
Stephen Njuki

Линейный дискриминантный анализ (Linear discriminant analysis, LDA) - очень распространенный метод уменьшения размерности для задач классификации. Как и в случае с картами Кохонена, описанными в предыдущей статье, если у нас есть многомерные данные (с большим количеством атрибутов или переменных), с помощью которых необходимо классифицировать наблюдения, LDA поможет преобразовать данные, чтобы сделать классы как можно более различающимися. Говоря более строго, LDA находит линейную проекцию данных в подпространство более низкого измерения, которое оптимизирует некоторую меру разделения классов. Размерность этого подпространства никогда не превышает числа классов. В этой статье мы рассмотрим, как LDA можно использовать в качестве сигнала, трейлинг-индикатора и инструмента управления капиталом. Вначале рассмотрим теорию, а затем перейдем к практическому применению LDA. 

LDA очень похож на методы PCA, QDA и ANOVA. Все эти аббревиатуры будут расшифрованы ниже. В этой статье я не буду углубляться во все эти методы, а только подчеркну их различия.

1) Метод главных компонент (pincipal components analysis, PCA):

LDA очень похож на PCA. Иногда даже возникает вопрос, имеет ли смысл выполнять PCA с последующей регуляризацией LDA (чтобы избежать подгонки кривой). Это обширная тема, которая, возможно, будет рассмотрена в другой статье.

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

lda

На рисунке выше ясно видно, что PCA даст нам LD2, тогда как LDA даст нам LD1. Это делает главное различие (и, следовательно, преимущество LDA) между PCA и LDA абсолютно очевидным: только потому, что функция имеет высокую дисперсию, не означает, что она будет полезна для прогнозирования классов.

2) Квадратичный дискриминантный анализ (quadratic discriminant analysis, QDA):

QDA является обобщением LDA как классификатора. LDA предполагает, что условные распределения класса являются гауссовыми с той же матрицей ковариаций, если мы хотим, чтобы он выполнял какую-либо классификацию.

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

После всего этого может возникнуть некоторая путаница в отношениях между LDA и QDA, например, что лучше подходит для уменьшения размерности, а что лучше для классификации и т. д. В этом случае вам может помочь пост  CrossValidated (на английском), а также все ссылки, которые в нем приведены.

3) Дисперсионный анализ (analysis of variance, ANOVA):

На первый взгляд, LDA и ANOVA оба пытаются разбить наблюдаемую переменную на несколько независимых/зависимых переменных. Однако инструмент, используемый ANOVA, согласно Википедии, является зеркальной версией того, что использует LDA:

"LDA тесно связан с дисперсионным анализом (ANOVA) и регрессионным анализом, которые также пытаются выразить одну зависимую переменную как линейную комбинацию других признаков или измерений. Однако ANOVA использует  категориальные независимые переменные и  дискриминантную зависимую переменную, тогда как дискриминантный анализ -  непрерывные независимые переменные и  категориальную зависимую переменную (т.е. метку класса)".

 

LDA обычно определяется следующим образом.

Примем, что:

  • n - количество классов
  • μ среднее значение всех наблюдений
  • N i количество наблюдений в i классе
  • μ i среднее значение  i класса
  • Σ i  матрица рассеяния   i класса


SW матрица рассеяния внутри класса, полученная как

SW = ∑ i = 1 n Σ i


 SB матрица рассеяния между классами, полученная как

SB = ∑ i = 1 n N i ( μ i − μ ) ( μ i − μ ) T


Диагонализируем  SW − 1 SB для получения его собственных значений и собственных векторов.

Выберем  k наибольшие собственные значения и связанные с ними собственные векторы. Спроецируем наши наблюдения на подпространство, образованное этими векторами.

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

Как и в предыдущих статьях, мы будем использовать библиотеку MQL-кода при реализации LDA для нашего советника. В частности, мы будем полагаться на класс CLDA в файле dataanalysis.mqh.

Мы будем рассматривать LDA для валютной пары USDJPY за 2022 год на дневном таймфрейме. Выбор входных данных для советника во многом остается за пользователем. В нашем случае для этого LDA входные данные имеют переменную и компонент класса. Нам нужно подготовить эти данные перед запуском тестов. Поскольку мы будем иметь дело с ценами закрытия, по умолчанию LDA будет «продолжен» (в своем исходном состоянии). Мы применим нормализацию и дискретизацию к переменным и классовым компонентам наших данных. Нормализация означает, что все данные находятся между установленным минимумом и максимумом, в то время как дискретизация подразумевает, что данные преобразуются в логическое значение (истина или ложь). Ниже приведены приготовления для пяти наборов данных для нашего сигнала:

  1. Отслеживание данных дискретных переменных изменения цены закрытия для соответствия категориям класса.
  2. Данные нормализованных переменных необработанных изменений цен закрытия в диапазоне от -1.0 до +1.0.
  3. Непрерывные переменные данные в необработанных изменениях цены закрытия. 
  4. Необработанные цены закрытия.

Нормализация покажет изменение цены закрытия в виде пропорции к диапазону последних 2 баров в десятичном выражении (от -1,0 до +1,0), а дискретизация покажет, выросла ли цена (индексом 2), осталась в нейтральном диапазоне (индекс 1) или была отклонена (индекс 0). Мы протестируем все типы данных, чтобы проверить производительность. Эта подготовка выполняется методом Data, показанным ниже. Все четыре типа данных регулируются с помощью входного параметра m_signal_regulizer, чтобы определить нейтральную зону для наших данных и таким образом уменьшить белый шум.

//+------------------------------------------------------------------+
//| Data Set method                                                  |
//| INPUT PARAMETERS                                                 |
//|     Index   -   int, read index within price buffer.             |
//|                                                                  |
//|     Variables                                                    |
//|             -   whether data component is variables or .         |
//|                  classifier.                                     |
//| OUTPUT                                                           |
//|     double  -   Data depending on data set type                  |
//|                                                                  |
//| DATA SET TYPES                                                   |
//| 1. Discretized variables. - 0                                    |
//|                                                                  |
//| 2. Normalized variables. - 1                                     |
//|                                                                  |
//| 3. Continuized variables. - 2                                    |
//|                                                                  |
//| 4. Raw data variables. - 3                                       |
//|                                                                  |
//+------------------------------------------------------------------+
double CSignalDA::Data(int Index,bool Variables=true)
   {
      m_close.Refresh(-1);
         
      m_low.Refresh(-1);
      m_high.Refresh(-1);
            
      if(Variables)
      {
         if(m_signal_type==0)
         {
            return(fabs(Close(StartIndex()+Index)-Close(StartIndex()+Index+1))<m_signal_regulizer*Range(Index)?1.0:((Close(StartIndex()+Index)>Close(StartIndex()+Index+1))?2.0:0.0));
         }
         else if(m_signal_type==1)
         {
            if(fabs(Close(StartIndex()+Index)-Close(StartIndex()+Index+1))<m_signal_regulizer*Range(Index))
            {
               return(0.0);
            }
            return((Close(StartIndex()+Index)-Close(StartIndex()+Index+1))/fmax(m_symbol.Point(),fmax(High(StartIndex()+Index),High(StartIndex()+Index+1))-fmin(Low(StartIndex()+Index),Low(StartIndex()+Index+1))));
         }
         else if(m_signal_type==2)
         {
            if(fabs(Close(StartIndex()+Index)-Close(StartIndex()+Index+1))<m_signal_regulizer*Range(Index))
            {
               return(0.0);
            }
            return(Close(StartIndex()+Index)-Close(StartIndex()+Index+1));
         }
         else if(m_signal_type==3)
         {
            if(fabs(Close(StartIndex()+Index)-Close(StartIndex()+Index+1))<m_signal_regulizer*Range(Index))
            {
               return(Close(StartIndex()+Index+1));
            }
            return(Close(StartIndex()+Index));
         }
      }
      
      return(fabs(Close(StartIndex()+Index)-Close(StartIndex()+Index+1))<m_signal_regulizer*Range(Index)?1.0:((Close(StartIndex()+Index)>Close(StartIndex()+Index+1))?2.0:0.0));
   }


Мы используем размерность четыре, что означает, что каждое значение индикатора будет предоставлять четыре переменные. Итак, для краткости в нашем случае мы будем смотреть на последние четыре значения индикатора для каждого набора данных в обучении. Наша классификация также будет базовой. Она будет учитывать только два класса (минимум) в компоненте класса каждого набора данных. Нам также нужно будет установить количество точек данных в нашей обучающей выборке. Это значение хранится во входном параметре m_signal_points.

На выходе LDA обычно представляет собой матрицу коэффициентов. Эти коэффициенты сортируются по векторам, и скалярное произведение любого из этих векторов и текущей точки данных индикатора должно давать значение, которое затем сравнивается с аналогичными значениями, полученными произведением обучающего набора данных, чтобы классифицировать эти новые/текущие единицы данных. Таким образом, если бы в нашем обучающем наборе было только две точки данных с проекциями LDA 0 и 1, а наше новое значение дает скалярное произведение 0,9, мы бы заключили, что оно находится в той же категории, что и точка данных, проекция LDA которой равна 1, поскольку оно ближе к единице. С другой стороны, если бы произведение дало значение 0,1, мы бы посчитали, что эта новая точка данных должна принадлежать к той же категории, что и точка данных, чья проекция LDA была равна 0. 

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

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

Класс ExpertSignal обычно использует нормализованные целочисленные значения (0-100) для взвешивания длинных и коротких решений. Поскольку проекции LDA обязательно будут иметь тип double, нормализуем их, как показано ниже, чтобы они попадали в диапазон от -1,0 до +1,0 (отрицательный для медвежьего и положительный для бычьего). 

         // best eigen vector is the first 
         for(int v=0;v<__S_VARS;v++){ _unknown_centroid+= (_w[0][v]*_z[0][v]); }
         
         //

         
         if(fabs(_centroids[__S_BULLISH]-_unknown_centroid)<fabs(_centroids[__S_BEARISH]-_unknown_centroid) && fabs(_centroids[__S_BULLISH]-_unknown_centroid)<fabs(_centroids[__S_WHIPSAW]-_unknown_centroid))
         {
            _da=(1.0-(fabs(_centroids[__S_BULLISH]-_unknown_centroid)/(fabs(_centroids[__S_BULLISH]-_unknown_centroid)+fabs(_centroids[__S_WHIPSAW]-_unknown_centroid)+fabs(_centroids[__S_BEARISH]-_unknown_centroid))));
         }
         else if(fabs(_centroids[__S_BEARISH]-_unknown_centroid)<fabs(_centroids[__S_BULLISH]-_unknown_centroid) && fabs(_centroids[__S_BEARISH]-_unknown_centroid)<fabs(_centroids[__S_WHIPSAW]-_unknown_centroid))
         {
            _da=-1.0*(1.0-(fabs(_centroids[__S_BEARISH]-_unknown_centroid)/(fabs(_centroids[__S_BULLISH]-_unknown_centroid)+fabs(_centroids[__S_WHIPSAW]-_unknown_centroid)+fabs(_centroids[__S_BEARISH]-_unknown_centroid))));
         }

Затем это значение легко нормализуется до типичного целого числа (0-100), ожидаемого классом сигнала.

   if(_da>0.0)
     {
      result=int(round(100.0*_da));
     }

 для long-функции Check и,

   if(_da<0.0)
     {
      result=int(round(-100.0*_da));
     }

для short-функции Check.

Итак, тестовый прогон для каждого из типов входных данных дает следующие отчеты тестера стратегий.

Отчет набора данных 1

sr1

cs1

 

Отчет набора данных 2

sr2

cs2

 

Отчет набора данных 3

sr3

cs3

 

Отчет набора данных 4

sr4

cs4

  

Эти отчеты демонстрируют потенциал LDA как инструмента для трейдера. 

Класс ExpertTrailing корректирует или устанавливает стоп-лосс для открытой позиции. Ключевым отображаемым результатом здесь является значение double нового стоп-лосса. Таким образом, в зависимости от открытой позиции мы будем рассматривать цены High и Low как наши первичные наборы данных. Они будут подготовлены следующим образом как для цен High, так и для цен Low с выбором в зависимости от типа открытой позиции: - 

  1. Отслеживание данных дискретных переменных изменений цены (High или Low) для соответствия категориям класса.
  2. Данные нормализованных переменных необработанных (high or low) изменений цены (High или Low) в диапазоне от -1.0 до +1.0. 
  3. Данные непрерывных переменных необработанных изменений цены (High или Low)
  4. Необработанные цены (High или Low).

Как и в случае класса сигнала, LDA будет выведен в виде нормализованного значения double. Поскольку такое значение бесполезно при определении стоп-лосса, оно будет скорректировано, как показано ниже, в зависимости от типа открытой позиции, чтобы определить цену стоп-лосса.

      int _index   =StartIndex();
      double _min_l=Low(_index),_max_l=Low(_index),_min_h=High(_index),_max_h=High(_index);
      
      for(int d=_index;d<m_trailing_points+_index;d++)
      {
         _min_l=fmin(_min_l,Low(d));
         _max_l=fmax(_max_l,Low(d));
         _min_h=fmin(_min_h,High(d));
         _max_h=fmax(_max_h,High(d));
      }
      
      if(Type==POSITION_TYPE_BUY)
      {
         _da*=(_max_l-_min_l);
         _da+=_min_l;
      }
      else if(Type==POSITION_TYPE_SELL)
      {
         _da*=(_max_h-_min_h);
         _da+=_max_h;
      }

Также здесь показано, как мы корректируем и устанавливаем наши новые уровни стоп-лосса. Для длинных позиций:

   m_long_sl=ProcessDA(StartIndex(),POSITION_TYPE_BUY);

   double level =NormalizeDouble(m_symbol.Bid()-m_symbol.StopsLevel()*m_symbol.Point(),m_symbol.Digits());
   double new_sl=NormalizeDouble(m_long_sl,m_symbol.Digits());
   double pos_sl=position.StopLoss();
   double base  =(pos_sl==0.0) ? position.PriceOpen() : pos_sl;
//---
   sl=EMPTY_VALUE;
   tp=EMPTY_VALUE;
   if(new_sl>base && new_sl<level)
      sl=new_sl;

Здесь мы определяем вероятную минимальную точку цены до следующего бара для длинной открытой позиции ('m_long_sl'), а затем устанавливаем ее в качестве нашего нового стоп-лосса, если она больше, чем цена открытия позиции или ее текущий стоп-лосс ниже bid минус стоп-уровень. Тип данных, используемый при расчете, — цены Low.

Cтоп-лосс для коротких позиций устанавливается зеркальным способом.

Итак, тестовый прогон для каждого из типов входных данных при использовании … типа данных для сигнала дает следующие результаты.

Отчет набора данных 1

tr1

ct1

 

Отчет набора данных 2

tr2



ct2

 

Отчет набора данных 3

tr3

ct3

 

Отчет набора данных 4

tr4

ct4

 

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

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

  1. Отслеживание данных дискретных переменных изменения значения диапазона для соответствия категориям класса.
  2. Данные нормализованных переменных  необработанных изменений  значения диапазона от -1.0 до +1.0. 
  3. Непрерывные переменные данные в необработанных изменениях диапазона значений
  4. Необработанные значения диапазона.

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

      int _index   =StartIndex();
      double _min_l=Low(_index),_max_h=High(_index);
      
      for(int d=_index;d<m_money_points+_index;d++)
      {
         _min_l=fmin(_min_l,Low(d));
         _max_h=fmax(_max_h,High(d));
      }
   
      _da*=(_max_h-_min_l);
      _da+=(_max_h-_min_l);

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

   double _da=ProcessDA(StartIndex());
   
   if(m_symbol==NULL)
      return(0.0);
   
   sl=m_symbol.Bid()-_da;
   
//--- select lot size
   double _da_1_lot_loss=(_da/m_symbol.TickSize())*m_symbol.TickValue();
   double lot=((m_percent/100.0)*m_account.FreeMargin())/_da_1_lot_loss;
   
//--- calculate margin requirements for 1 lot
   if(m_account.FreeMarginCheck(m_symbol.Name(),ORDER_TYPE_BUY,lot,m_symbol.Ask())<0.0)
     {
      printf(__FUNCSIG__" insufficient margin for sl lot! ");
      lot=m_account.MaxLotCheck(m_symbol.Name(),ORDER_TYPE_BUY,m_symbol.Ask(),m_percent);
     }
   
//--- return trading volume
   return(Optimize(lot));

Здесь примечательно то, что мы определяем прогнозируемое изменение цены диапазона и вычитаем этот прогноз из нашей цены bid (необходимо было также вычесть уровень стопов). Это даст нам стоп-лосс с поправкой на риск. И если мы используем входной параметр percent в качестве параметра максимального риска потерь, мы можем вычислить размер лота, который ограничит наш процент просадки значением percent, если мы столкнемся с просадкой ниже прогнозной цены bid.

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

Отчет набора данных 1

mr1

cm1

 

Отчет набора данных 2

mr2

cm2


Отчет набора данных 3

mr3

cm3


Отчет набора данных 4

mr4


cm4


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

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


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

Прикрепленные файлы |
TrailingDA.mqh (16.66 KB)
SignalDA.mqh (13.48 KB)
MoneyDA.mqh (15.19 KB)
Популяционные алгоритмы оптимизации: Алгоритм летучих мышей (Bat algorithm - BA) Популяционные алгоритмы оптимизации: Алгоритм летучих мышей (Bat algorithm - BA)
Сегодня изучим алгоритм летучих мышей (Bat algorithm - BA), который отличается удивительной сходимостью на гладких функциях.
Машинное обучение и Data Science (Часть 9): Алгоритм k-ближайших соседей (KNN) Машинное обучение и Data Science (Часть 9): Алгоритм k-ближайших соседей (KNN)
Это ленивый алгоритм, который не учится на обучающей выборке, а хранит все доступные наблюдения и классифицирует данные сразу же, как только получает новую выборку. Несмотря на простоту, этот метод используется во множестве реальных приложений.
Бегущая строка котировок: базовая версия Бегущая строка котировок: базовая версия
Здесь я покажу, как создать в терминале бегущую строку, которая обычно используется для отображения котировок на бирже. Создавать такую строку мы будем только при помощи MQL5, не используя никакое другое внешнее программирование.
Горная карта, или График "Айсберг" Горная карта, или График "Айсберг"
Как вам идея добавить новый тип графика в платформу MetaTrader 5? Многие говорят, что в ней не хватает несколько вещей, которые есть в других платформах. Но на самом деле MetaTrader 5 — очень практичная платформа, которая позволяет делать то, что невозможно сделать во многих других платформах, или по крайней мере, в них это сделать не так легко.