English 中文 Español Deutsch 日本語 Português
preview
Машинное обучение и Data Science (Часть 9): Алгоритм k-ближайших соседей (KNN)

Машинное обучение и Data Science (Часть 9): Алгоритм k-ближайших соседей (KNN)

MetaTrader 5Трейдинг | 23 декабря 2022, 13:42
4 230 0
Omega J Msigwa
Omega J Msigwa

Птицы одного оперения собираются вместе (английская пословица) — идея, лежащая в основе алгоритма KNN.

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


Алгоритм kNN

Источник изображения: skicit-learn.org

Несколько замечаний:

  1. Часто используется как классификатор, но можно использовать и для регрессии.
  2. K-NN — непараметрический алгоритм, что означает, что он не делает никаких предположений относительно исходных данных.
  3. Его часто называют алгоритмом ленивого обучения, потому что он не учится на тренировочной выборке. Метод хранит все данные и использует их.
  4. Алгоритм KNN предполагает сходство между новыми данными и доступным набором данных. На основе этого он помещает новые данные в категорию с наиболее похожими данными.

Ка работает алгоритм KNN?

Прежде чем углубимся в написание кода, давайте разберемся, как работает алгоритм KNN:
  • Шаг 1: выбор числа k соседей
  • Шаг 2: находим евклидово расстояние от точки до всех членов набора данных
  • Шаг 3: берем K ближайших соседей по евклидову расстоянию
  • Шаг 4: среди этих ближайших соседей считаем количество точек данных в каждой категории
  • Шаг 5: относим новые данные к той категории, для которой количество соседей максимально

Шаг 1: выбор количества k соседей

Это простой шаг. Все, что нам нужно сделать, это выбрать число k, которое будем использовать в классе CKNNnearestNeighbours. Теперь возникает вопрос, как факторизовать k.

Факторизация K

K — это количество ближайших соседей, которых будем использовать для голосования относительно того, куда отнести данное значение/точку. Выбор маленького значения k приведет к большому количеству шума в классифицированных данных, что может привести к большему числу смещений, а очень большее значение k делает алгоритм значительно медленнее. 
Значение k должно быть нечетным числом, чтобы процесс принятия решения не застревал: поскольку для процесса голосования требуется количество соседей равное k, если оно установлено как 4, то 2 члена могут проголосовать за то, что данная точка принадлежит к категории А, а оставшиеся 2 голоса отнесут к категории И. И как в таком случае определить, кто выиграл?


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

Внутри библиотеки кластеризации создадим функцию для получения доступных классов из матрицы наборов данных и сохранения их в глобальном векторе классов m_classesVector

vector CKNNNearestNeighbors::ClassVector()
 {
   vector t_vectors = Matrix.Col(m_cols-1); //целевые переменные находятся в последнем столбце матрицы
   vector temp_t = t_vectors, v = {t_vectors[0]};
   
   for (ulong i=0, count =1; i<m_rows; i++) //подсчет разных соседей
    {
      for (ulong j=0; j<m_rows; j++)
         {
            if (t_vectors[i] == temp_t[j] && temp_t[j] != -1000)
               {                   
                  bool count_ready = false;
                  
                  for(ulong n=0;n<v.Size();n++)
                      if (t_vectors[i] == v[n])
			 count_ready = true;
                    
                    if (!count_ready)
                      {
                        count++;
                        v.Resize(count);
                        
                        v[count-1] = t_vectors[i]; 
                        
                        temp_t[j] = -1000; //изменим, чтобы больше нельзя было считать
                      }
                    else 
			break;
                  //Print("t vectors vector ",t_vectors);
               }
             else
		 continue;
         }
    }
   
   return(v);
 } 

CKNNNearestNeighbors::CKNNNearestNeighbors(matrix<double> &Matrix_)
  {
    Matrix.Copy(Matrix_);

    k = (int)round(MathSqrt(Matrix.Rows())); 
    k = k%2 ==0 ? k+1 : k; //проверим, является ли k нечетным
    
    m_rows = Matrix.Rows();
    m_cols = Matrix.Cols();
    
   m_classesVector = ClassVector();
   Print("classes vector | Neighbors ",m_classesVector);
  }

Выводимая информация:

2022.10.31 05:40:33.825 TestScript      classes vector | Neighbors [1,0]

Если вы обратили внимание, в конструкторе есть строка, которая гарантирует, что значение k является нечетным числом. Оно генерируется по умолчанию как квадратный корень из общего количества строк в наборе данных/количестве точек данных. Это тот случай, когда мы не заморачиваемся со значением K, то есть не настраиваем алгоритм. Есть и другой конструктор, который позволяет настраивать значение k, но после этого все-равно проверяется, является ли k нечетным. Значение в данном случае равно 3, для 9 строк: √9 = 3 (нечетное число).

CKNNNearestNeighbors:: CKNNNearestNeighbors(matrix<double> &Matrix_, uint k_)
 {
   k = k_;
   
   if (k %2 ==0)
      printf("K %d is an even number, It will be added by One so it becomes an odd Number %d",k,k=k+1);
      
   Matrix.Copy(Matrix_);
   
   m_rows = Matrix.Rows();
   m_cols = Matrix.Cols();
   
   m_classesVector = ClassVector();
   Print("classes vector | Neighbors ",m_classesVector);
 }

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

Вот как данные выглядят в MetaEditor:

    matrix Matrix = 
      {//weight(kg) | height(cm) | class
         {51, 167,   1}, //underweight
         {62, 182,   0}, //Normal
         {69, 176,   0}, //Normal
         {64, 173,   0}, //Normal
         {65, 172,   0}, //Normal
         {56, 174,   1}, //Underweight
         {58, 169,   0}, //Normal
         {57, 173,   0}, //Normal
         {55, 170,   0}  //Normal
      };    


Шаг 2: находим евклидово расстояние от точки до всех членов набора данных

Предполагая, что мы не знаем, как рассчитать индекс массы тела, нужно узнать, к какой категории относится человек с весом 57 кг и ростом 170 см — к категории недостаточного веса или нормального веса.

    vector v = {57, 170};
    
    nearest_neighbors = new CKNNNearestNeighbors(Matrix); //вызовем конструктор и передадим матрицу
    nearest_neighbors.KNNAlgorithm(v);  //передаем новые точки в алгоритм

Первое, что делает функция KNNAlgorithm, — это находит евклидово расстояние между заданной точкой и всеми точками в наборе данных.

   vector vector_2;
   vector euc_dist;
   euc_dist.Resize(m_rows);
   
   matrix temp_matrix = Matrix;
   temp_matrix.Resize(Matrix.Rows(),Matrix.Cols()-1); //удаляем последний столбец независимых переменных
   
   for (ulong i=0; i<m_rows; i++)
      {
         vector_2 = temp_matrix.Row(i);
         euc_dist[i] = Euclidean_distance(vector_,vector_2);
      }      

Сама функция для вычисления евклидова расстояния выглядит так:

double CKNNNearestNeighbors:: Euclidean_distance(const vector &v1,const vector &v2)
 {
   double dist = 0;
   
   if (v1.Size() != v2.Size()) 
	Print(__FUNCTION__," v1 and v2 not matching in size");
   else
    {
       double c = 0;
         for (ulong i=0; i<v1.Size(); i++)
               c += MathPow(v1[i] - v2[i], 2); 

        dist = MathSqrt(c);
    } 
    
    return(dist);
 }

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

Print("Euclidean distance vector\n",euc_dist);

Output  -----------> 

CS      0       19:29:09.057    TestScript   Euclidean distance vector
CS      0       19:29:09.057    TestScript   [6.7082,13,13.41641,7.61577,8.24621,4.12311,1.41421,3,2]

Теперь давайте заполним последний столбец матрицы евклидовым расстоянием:

   if (isdebug)
      {  
         matrix dbgMatrix = Matrix; //временная матрица отладки
         dbgMatrix.Resize(dbgMatrix.Rows(),dbgMatrix.Cols()+1);
         dbgMatrix.Col(euc_dist,dbgMatrix.Cols()-1);
         
         Print("Matrix w Euclidean Distance\n",dbgMatrix);
         
         ZeroMemory(dbgMatrix);
      }

Выводимая информация:

CS      0       19:33:48.862    TestScript   Matrix w Euclidean Distance
CS      0       19:33:48.862    TestScript   [[51,167,1,6.7082]
CS      0       19:33:48.862    TestScript    [62,182,0,13]
CS      0       19:33:48.862    TestScript    [69,176,0,13.41641]
CS      0       19:33:48.862    TestScript    [64,173,0,7.61577]
CS      0       19:33:48.862    TestScript    [65,172,0,8.24621]
CS      0       19:33:48.862    TestScript    [56,174,1,4.12311]
CS      0       19:33:48.862    TestScript    [58,169,0,1.41421]
CS      0       19:33:48.862    TestScript    [57,173,0,3]
CS      0       19:33:48.862    TestScript    [55,170,0,2]]

Чтобы лучше объяснить, покажу эти данные в виде изображения:

Учитывая, что значение k равно 3, все 3 ближайших соседа попадают в класс Normal, поэтому визуально мы понимаем, что данная точка попадает в категорию Normal. Теперь давайте напишем код для принятия этого решения. 

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

   int size = (int)m_target.Size();

   double tarArr[];
   ArrayResize(tarArr, size);
   double eucArray[];
   ArrayResize(eucArray, size);

   for(ulong i=0; i<m_target.Size(); i++)  //преобразуем векторы в массивы
     {
      tarArr[i] = m_target[i];
      eucArray[i] = euc_dist[i];
     }

   double track[], NN[];
   ArrayCopy(track, tarArr);


   int max; 
   for(int i=0; i<(int)m_target.Size(); i++)
     {
      if(ArraySize(track) > (int)k)
        {
         max = ArrayMaximum(eucArray);
         ArrayRemove(eucArray, max, 1);
         ArrayRemove(track, max, 1);
        }
     }
   ArrayCopy(NN, eucArray);

   Print("NN ");
   ArrayPrint(NN);
   Print("Track ");
   ArrayPrint(track);

В коде выше мы определяем ближайших соседей и сохраняем их в массиве NN, мы также отслеживаем их значения класса/какой класс они занимают в глобальном векторе целевых значений. Кроме того, мы удаляем максимальные значения в массиве, пока не останется массив меньших значений размером k (ближайшие соседи).

Результат будет таким:

CS      0       05:40:33.825    TestScript    NN 
CS      0       05:40:33.825    TestScript    1.4 3.0 2.0
CS      0       05:40:33.825    TestScript    Track 
CS      0       05:40:33.825    TestScript    0.0 0.0 0.0

Процесс голосования:

//--- процесс голосования

   vector votes(m_classesVector.Size());

   for(ulong i=0; i<votes.Size(); i++)
     {
      int count = 0;
      for(ulong j=0; j<track.Size(); j++)
        {
         if(m_classesVector[i] == track[j])
            count++;
        }

      votes[i] = (double)count;

      if(votes.Sum() == k)  //все участники проголосовали
         break;
     }
   Print("votes ", votes);

Выводимая информация:

2022.10.31 05:40:33.825 TestScript   votes [0,3]

Помните, что вектор голосов упорядочивает голоса на основе глобального вектора классов в наборе данных?

2022.10.31 06:43:30.095 TestScript   classes vector | Neighbors [1,0]

Здесь из 3 соседей, которые были выбраны для голосования, 3 проголосовали за то, что данные принадлежат классу нулей (0), и ни один член не проголосовал за класс единиц (1).

Давайте посмотрим, что могло бы произойти, если бы для голосования было выбрано 5 соседей, т. е. значение K было бы равно 5.

CS      0       06:43:30.095    TestScript   NN 
CS      0       06:43:30.095    TestScript   6.7 4.1 1.4 3.0 2.0
CS      0       06:43:30.095    TestScript   Track 
CS      0       06:43:30.095    TestScript   1.0 1.0 0.0 0.0 0.0
CS      0       06:43:30.095    TestScript   votes [2,3]

Теперь окончательное решение принять легко: класс с наибольшим количеством голосов выиграет. В этом случае вес относится к классу нормального, индекс которого 0.

   if(isdebug)
      Print(vector_, " belongs to class ", (int)m_classesVector[votes.ArgMax()]);

Выводимая информация:

2022.10.31 06:43:30.095 TestScript      [57,170] belongs to class 0

Теперь все отлично работает. Давайте изменим тип KNNAlgorithm с void на int, чтобы функция возвращала значение класса, к которому принадлежит данное значение. Это может пригодиться в реальной торговле, мы будем подавать новые значения, для которых будет ожидать немедленный вывод от алгоритма.

   int               KNNAlgorithm(vector &vector_);


Тестируем модель и определяем ее точность.

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

float TrainTest(double train_size=0.7)

По умолчанию 70% набора данных будут использоваться для обучения, а остальные 30% — для тестирования.

Напишем код функции для разделения набора данных на этап обучения и этап тестирования:

//--- Разделим матрицу
   
   matrix default_Matrix = Matrix; 
   
   int train = (int)MathCeil(m_rows*train_size),
       test  = (int)MathFloor(m_rows*(1-train_size));
   
   if (isdebug) printf("Train %d test %d",train,test);

   matrix TrainMatrix(train,m_cols), TestMatrix(test,m_cols);
   int train_index = 0, test_index =0;

//---
   
   for (ulong r=0; r<Matrix.Rows(); r++)
      {
         if ((int)r < train)
           {
             TrainMatrix.Row(Matrix.Row(r),train_index);
             train_index++;
           }
         else
           {
             TestMatrix.Row(Matrix.Row(r),test_index);
             test_index++;
           }     
      }

   if (isdebug) Print("TrainMatrix\n",TrainMatrix,"\nTestMatrix\n",TestMatrix);
   

Выводимая информация:

CS      0       09:51:45.136    TestScript   TrainMatrix
CS      0       09:51:45.136    TestScript   [[51,167,1]
CS      0       09:51:45.136    TestScript    [62,182,0]
CS      0       09:51:45.136    TestScript    [69,176,0]
CS      0       09:51:45.136    TestScript    [64,173,0]
CS      0       09:51:45.136    TestScript    [65,172,0]
CS      0       09:51:45.136    TestScript    [56,174,1]
CS      0       09:51:45.136    TestScript    [58,169,0]]
CS      0       09:51:45.136    TestScript   TestMatrix
CS      0       09:51:45.136    TestScript   [[57,173,0]
CS      0       09:51:45.136    TestScript    [55,170,0]]

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

Обучение:

   Matrix.Copy(TrainMatrix); //That's it ???

Тестирование:

//--- тестирование алгоритма
   
   vector TestPred(TestMatrix.Rows());
   vector v_in = {};
   
   for (ulong i=0; i<TestMatrix.Rows(); i++)
     {
        v_in = TestMatrix.Row(i);
        v_in.Resize(v_in.Size()-1); //Удаляем независимую переменную
        
        TestPred[i] = KNNAlgorithm(v_in);
        
        Print("v_in ",v_in," out ",TestPred[i]);
     }   

Выводимая информация:

CS      0       09:51:45.136    TestScript   v_in [57,173] out 0.0
CS      0       09:51:45.136    TestScript   v_in [55,170] out 0.0

Все тесты были бы напрасными без измерения того, насколько точна наша модель на данном наборе данных.

Матрица путаницы

Об этом мы говорили ранее, во второй статье из этой серии.

matrix CKNNNearestNeighbors::ConfusionMatrix(vector &A,vector &P)
 {
   ulong size = m_classesVector.Size();
   matrix mat_(size,size);
   
   if (A.Size() != P.Size()) 
      Print("Cant create confusion matrix | A and P not having the same size ");
   else
     {
      
         int tn = 0,fn =0,fp =0, tp=0;
         for (ulong i = 0; i<A.Size(); i++)
            {              
               if (A[i]== P[i] && P[i]==m_classesVector[0])
                  tp++; 
               if (A[i]== P[i] && P[i]==m_classesVector[1])
                  tn++;
               if (P[i]==m_classesVector[0] && A[i]==m_classesVector[1])
                  fp++;
               if (P[i]==m_classesVector[1] && A[i]==m_classesVector[0])
                  fn++;
            }
            
       mat_[0][0] = tn; mat_[0][1] = fp;
       mat_[1][0] = fn; mat_[1][1] = tp;

    }
     
   return(mat_);    
 }

Внутри TrainTest() в конце функции я добавил следующий код, чтобы завершить функцию и вернуть точность:

   matrix cf_m = ConfusionMatrix(TargetPred,TestPred);
   vector diag = cf_m.Diag();
   float acc = (float)(diag.Sum()/cf_m.Sum())*100;
   
   Print("Confusion Matrix\n",cf_m,"\nAccuracy ------> ",acc,"%");
   
   return(acc);      

Выводимая информация:

CS      0       10:34:26.681    TestScript   Confusion Matrix
CS      0       10:34:26.681    TestScript   [[2,0]
CS      0       10:34:26.681    TestScript    [0,0]]
CS      0       10:34:26.681    TestScript   Accuracy ------> 100.0%

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

На данный момент у нас есть полнофункциональная библиотека K-Nearest Neighbours. Давайте посмотрим, как можно использовать ее для прогнозирования цен на различные форекс-символы и акции.

Подготовка набора данных

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

Независимыми переменными являются показатели индикатора ATR и Индикатор объемов. Целевая переменная будет равна 1, если рынок пошел вверх, и 0, если рынок пошел вниз. Это будет сигналом на покупку и сигналом на продажу соответственно при тестировании и использовании модели для торговли.

int OnInit()
  {
//--- подготовка набора данных 

    atr_handle = iATR(Symbol(),timeframe,period);
    volume_handle = iVolumes(Symbol(),timeframe,applied_vol);
    
    CopyBuffer(atr_handle,0,1,bars,atr_buffer);
    CopyBuffer(volume_handle,0,1,bars,volume_buffer); 
    
    Matrix.Col(atr_buffer,0); //Независимая переменная 1
    Matrix.Col(volume_buffer,1); //Независимая переменная 2
    
//--- целевые переменные

    vector Target_vector(bars);
    
    MqlRates rates[];
    ArraySetAsSeries(rates,true);
    CopyRates(Symbol(),PERIOD_D1,1,bars,rates);
    
    for (ulong i=0; i<Target_vector.Size(); i++) //устанавливаем метки
     {
       if (rates[i].close > rates[i].open)
          Target_vector[i] = 1; //bullish
       else
          Target_vector[i] = 0;
     }

   Matrix.Col(Target_vector,2);
        
//---

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

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

Информация не является финансовым или торговым советом.

Давайте выведем последние 10 значений баров, 10 строк нашей матрицы набора данных:

CS      0       12:20:10.449    NearestNeighorsEA (EURUSD,D1)   ATR,Volumes,Class Matrix
CS      0       12:20:10.449    NearestNeighorsEA (EURUSD,D1)   [[0.01139285714285716,12295,0]
CS      0       12:20:10.449    NearestNeighorsEA (EURUSD,D1)    [0.01146428571428573,12055,0]
CS      0       12:20:10.449    NearestNeighorsEA (EURUSD,D1)    [0.01122142857142859,10937,0]
CS      0       12:20:10.449    NearestNeighorsEA (EURUSD,D1)    [0.01130000000000002,13136,0]
CS      0       12:20:10.449    NearestNeighorsEA (EURUSD,D1)    [0.01130000000000002,15305,0]
CS      0       12:20:10.449    NearestNeighorsEA (EURUSD,D1)    [0.01097857142857144,13762,1]
CS      0       12:20:10.449    NearestNeighorsEA (EURUSD,D1)    [0.0109357142857143,12545,1]
CS      0       12:20:10.449    NearestNeighorsEA (EURUSD,D1)    [0.01116428571428572,18806,1]
CS      0       12:20:10.449    NearestNeighorsEA (EURUSD,D1)    [0.01188571428571429,19595,1]
CS      0       12:20:10.449    NearestNeighorsEA (EURUSD,D1)    [0.01137142857142859,15128,1]]


Данные сгруппированы по соответствующим свечам и индикаторам, теперь передадим их алгоритму.

    nearest_neigbors = new CKNNNearestNeighbors(Matrix,k); 
    nearest_neigbors.TrainTest(); 

Выводимая информация:


Получилась точность около 43,33%, что неплохо, учитывая, что мы не искали оптимальное значение k. Пройдемся в цикле по разным значениям k и выберем то, которое обеспечивает лучшую точность.

    for(uint i=0; i<bars; i++)
      {
        printf("<<< k %d >>>",i);
        nearest_neigbors = new CKNNNearestNeighbors(Matrix,i); 
        nearest_neigbors.TrainTest(); 
        
        delete(nearest_neigbors);
      }

Выводимая информация:

......
CS      0       16:22:28.013    NearestNeighorsEA (EURUSD,D1)   <<< k 24 >>>
CS      0       16:22:28.013    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 46.66666793823242%
CS      0       16:22:28.014    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 46.66666793823242%
CS      0       16:22:28.014    NearestNeighorsEA (EURUSD,D1)   <<< k 26 >>>
CS      0       16:22:28.014    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 40.0%
CS      0       16:22:28.014    NearestNeighorsEA (EURUSD,D1)   <<< k 27 >>>
CS      0       16:22:28.015    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 40.0%
CS      0       16:22:28.015    NearestNeighorsEA (EURUSD,D1)   <<< k 28 >>>
CS      0       16:22:28.015    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 56.66666793823242%
CS      0       16:22:28.016    NearestNeighorsEA (EURUSD,D1)   <<< k 29 >>>
CS      0       16:22:28.016    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 56.66666793823242%
CS      0       16:22:28.016    NearestNeighorsEA (EURUSD,D1)   <<< k 30 >>>
.....
.....
CS      0       16:22:28.017    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 60.000003814697266%
CS      0       16:22:28.017    NearestNeighorsEA (EURUSD,D1)   <<< k 31 >>> 
CS      0       16:22:28.017    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 60.000003814697266%
CS      0       16:22:28.017    NearestNeighorsEA (EURUSD,D1)   <<< k 32 >>> 
CS      0       16:22:28.018    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 56.66666793823242%
CS      0       16:22:28.018    NearestNeighorsEA (EURUSD,D1)   <<< k 33 >>> 
CS      0       16:22:28.018    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 56.66666793823242%
CS      0       16:22:28.018    NearestNeighorsEA (EURUSD,D1)   <<< k 34 >>> 
CS      0       16:22:28.019    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 50.0%
CS      0       16:22:28.019    NearestNeighorsEA (EURUSD,D1)   <<< k 35 >>> 
CS      0       16:22:28.019    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 50.0%
CS      0       16:22:28.019    NearestNeighorsEA (EURUSD,D1)   <<< k 36 >>> 
CS      0       16:22:28.020    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 53.333335876464844%
CS      0       16:22:28.020    NearestNeighorsEA (EURUSD,D1)   <<< k 37 >>> 
CS      0       16:22:28.020    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 53.333335876464844%
CS      0       16:22:28.020    NearestNeighorsEA (EURUSD,D1)   <<< k 38 >>> 
CS      0       16:22:28.021    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 56.66666793823242%
CS      0       16:22:28.021    NearestNeighorsEA (EURUSD,D1)   <<< k 39 >>> 
CS      0       16:22:28.021    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 56.66666793823242%
CS      0       16:22:28.021    NearestNeighorsEA (EURUSD,D1)   <<< k 40 >>> 
CS      0       16:22:28.022    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 56.66666793823242%
CS      0       16:22:28.022    NearestNeighorsEA (EURUSD,D1)   <<< k 41 >>>
.....
....
CS      0       16:22:28.023    NearestNeighorsEA (EURUSD,D1)   <<< k 42 >>>
CS      0       16:22:28.023    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 63.33333206176758%
CS      0       16:22:28.023    NearestNeighorsEA (EURUSD,D1)   <<< k 43 >>>
CS      0       16:22:28.024    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 63.33333206176758%
CS      0       16:22:28.024    NearestNeighorsEA (EURUSD,D1)   <<< k 44 >>>
CS      0       16:22:28.024    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 66.66667175292969%
CS      0       16:22:28.024    NearestNeighorsEA (EURUSD,D1)   <<< k 45 >>>
CS      0       16:22:28.025    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 66.66667175292969%
CS      0       16:22:28.025    NearestNeighorsEA (EURUSD,D1)   <<< k 46 >>>
CS      0       16:22:28.025    NearestNeighorsEA (EURUSD,D1)   Accuracy ------> 56.66666793823242%
CS      0       16:22:28.025    NearestNeighorsEA (EURUSD,D1)   <<< k 47 >>>
....
....

Это не лучший метод для определения значения k. Для нахождения оптимальных значений можно использовать метод перекрестной проверки. Итак, у нас пик производительности был при значении k в районе сорока. Теперь давайте попробуем этот алгоритм в торговой среде.

void OnTick()
  {
  
    vector x_vars(2); //вектор для хранения значений atr и volumes
    double atr_val[], volume_val[];
    
    CopyBuffer(atr_handle,0,0,1,atr_val);
    CopyBuffer(volume_handle,0,0,1,volume_val);
    
    x_vars[0] = atr_val[0]; 
    x_vars[1] = volume_val[0];
    
//---
    int signal = 0;
    
    double volume = SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN);
    
    MqlTick ticks;
    SymbolInfoTick(Symbol(),ticks);
    
    double ask = ticks.ask, bid = ticks.bid;
    
    
      if (isNewBar() == true) //мы на новой свече
         {
            signal = nearest_neigbors.KNNAlgorithm(x_vars); //вызов алгоритма
            
            if (signal == 1)
              {
                 if (!CheckPosionType(POSITION_TYPE_BUY))
                  {
                    m_trade.Buy(volume,Symbol(),ask,0,0);
                    if (ClosePosType(POSITION_TYPE_SELL))
                      printf("Failed to close %s Err = %d",EnumToString(POSITION_TYPE_SELL),GetLastError());
                  }
              }
            else 
              {
                if (!CheckPosionType(POSITION_TYPE_SELL))
                  {
                    m_trade.Sell(volume,Symbol(),bid,0,0);
                    if (ClosePosType(POSITION_TYPE_BUY))
                      printf("Failed to close %s Err = %d",EnumToString(POSITION_TYPE_BUY),GetLastError());
                  }
              }
         }
     
  }
  

Советник научился открывать сделки и закрывать их, теперь проверим его в тестере стратегий. Но для начала взглянем на вызов всего алгоритма в эксперте:

#include "KNN_nearest_neighbors.mqh";
CKNNNearestNeighbors *nearest_neigbors;

matrix Matrix;
//+------------------------------------------------------------------+
//| Expert initialization function                                     |
//+------------------------------------------------------------------+
int OnInit()
  { 
// сбор данных в матрицу проигнорирован
    
     nearest_neigbors = new CKNNNearestNeighbors(Matrix,_k); 
 
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- 

    delete(nearest_neigbors);
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+

void OnTick()
  {
  
    vector x_vars(2); //вектор для хранения значений atr и volumes

//добавление живых значений индикатора с рынка проигнорировано 

//---
    int signal = 0;

      if (isNewBar() == true) //мы на новой свече
         {
            signal = nearest_neigbors.KNNAlgorithm(x_vars);
            //торговые действия
         }
}

Тестирование в тестере стратегий на паре EURUSD с 01.06.2022 по 03.11.2022 (в режиме "Все тики"):



Когда использовать алгоритм KNN?

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

  • Когда набор данных размечен
  • Когда в наборе данных нет шума
  • Когда набор данных небольшой (это также нужно для производительности)

Преимущества:

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

Недостатки

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


Заключительные мысли

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

        if (isNewBar() == true) //new candlestick
         { 
           if (MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION)) 
             {
               gather_data();      
               nearest_neigbors = new CKNNNearestNeighbors(Matrix,_k); 
               signal = nearest_neigbors.KNNAlgorithm(x_vars);
               
               delete(nearest_neigbors);
             }      

Спасибо за внимание!

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

Прикрепленные файлы |
Возможности Мастера MQL5, которые вам нужно знать (Часть 04): Линейный дискриминантный анализ Возможности Мастера MQL5, которые вам нужно знать (Часть 04): Линейный дискриминантный анализ
Современный трейдер почти всегда находится в поиске новых идей. Он постоянно пробует новые стратегии, модифицирует их и отбрасывает те, что не оправдали себя. В этой серии статей я постараюсь доказать, что Мастер MQL5 является настоящей опорой трейдера в его поисках.
Горная карта, или График "Айсберг" Горная карта, или График "Айсберг"
Как вам идея добавить новый тип графика в платформу MetaTrader 5? Многие говорят, что в ней не хватает несколько вещей, которые есть в других платформах. Но на самом деле MetaTrader 5 — очень практичная платформа, которая позволяет делать то, что невозможно сделать во многих других платформах, или по крайней мере, в них это сделать не так легко.
Популяционные алгоритмы оптимизации: Алгоритм летучих мышей (Bat algorithm - BA) Популяционные алгоритмы оптимизации: Алгоритм летучих мышей (Bat algorithm - BA)
Сегодня изучим алгоритм летучих мышей (Bat algorithm - BA), который отличается удивительной сходимостью на гладких функциях.
DoEasy. Элементы управления (Часть 31): Прокрутка содержимого элемента управления "ScrollBar" DoEasy. Элементы управления (Часть 31): Прокрутка содержимого элемента управления "ScrollBar"
В статье создадим функционал прокрутки содержимого контейнера кнопками горизонтальной полосы прокрутки.