English Deutsch 日本語
preview
Возможности Мастера MQL5, которые вам нужно знать (Часть 09): Сочетание кластеризации k-средних с фрактальными волнами

Возможности Мастера MQL5, которые вам нужно знать (Часть 09): Сочетание кластеризации k-средних с фрактальными волнами

MetaTrader 5Торговые системы | 12 апреля 2024, 11:49
288 0
Stephen Njuki
Stephen Njuki

Введение

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

bannr

Поэтому, прежде чем мы начнем, возможно, будет полезно вспомнить иерархическую кластеризацию и ее отличие от кластеризации k-средних. Алгоритм агломеративной иерархической кластеризации инициализируется, рассматривая каждую точку данных в наборе данных, подлежащую классификации, как кластер. Затем алгоритм итеративно объединяет их в кластеры в зависимости от близости. Обычно количество кластеров не определяется заранее, но аналитик может определить это, просматривая построенную дендрограмму, которая является окончательным результатом, когда все точки данных объединяются в один кластер. Однако в качестве альтернативы, как мы видели в этой статье, если аналитик имеет в виду определенное количество кластеров, то выходная дендрограмма завершится на уровне/высоте, где количество кластеров соответствует исходному значению аналитика. Фактически, разные числа кластеров могут быть получены в зависимости от того, где обрезана дендрограмма.

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

По умолчанию метод k-средних очень медленный и неэффективный, поэтому его часто называют "наивным" в том смысле, что существуют более быстрые реализации. Частично это связано со случайным назначением начальных центроидов набору данных в начале оптимизации. Кроме того, после того, как были выбраны случайные центроиды, часот используется алгоритм Ллойда для получения правильного центроида и, следовательно, значений категории. Существуют дополнения и альтернативы алгоритму Ллойда, в том числе:естественные перерывы Дженкса (Jenks’ Natural Breaks), которые фокусируются на среднем значении кластера, а не на расстоянии до выбранных центроидов; k-медианы, которые, как следует из названия, используют медиану кластера, а не центроид или среднее значение, в качестве посредника для достижения идеальной классификации; k-медоиды, которые используют фактические точки данных внутри каждого кластера в качестве потенциального центроида, что делает его более устойчивым к шуму и выбросам, согласно Википедии; и наконец кластеризация в нечетком режиме, при которой границы кластера нечеткие, а точки данных имеют тенденцию принадлежать более чем одному кластеру. Этот последний формат интересен тем, что вместо "классификации" каждой точки данных, используется регрессивный вес, который количественно определяет, насколько данная точка данных принадлежит каждому из применимых кластеров.

Наша цель в этой статье - продемонстрировать еще один тип реализации k-средних, который преподносится как более эффективный, а именно:k-means++. Этот алгоритм опирается на методы Ллойда, такие как наивные k-средние по умолчанию, но отличается первоначальным подходом к выбору случайных центроидов. Этот подход не такой "случайный", как наивные k-средние, и из-за этого он имеет тенденцию сходиться гораздо быстрее и эффективнее.


Сравнение алгоритмов

K-средние vs K-медианы

K-средние минимизируют квадраты евклидовых расстояний между точками кластера и их центроидом, тогда как k-медианы минимизируют сумму абсолютных расстояний точек от их медианы внутри данного кластера (L1-Norm). Утверждается, что это различие делает k-медианы менее восприимчивыми к выбросам и делает кластер более репрезентативным для всех точек данных, поскольку центр кластера является медианой всех точек, а не их средним значением. Подход к вычислениям также отличается, поскольку k-медианы основаны на алгоритмах, основанных на L1-Norm, тогда как k-средние используют k-means++ и алгоритм Ллойда. Таким образом, варианты использования рассматривают k-средние как более способные обрабатывать сферические или равномерно распределенные наборы данных, в то время как k-медианы могут быть более приспособлены к наборам данных нерегулярной формы. Наконец, k-медианы более предпочтительны, когда дело доходит до интерпретации, поскольку медианы, как правило, лучше представляют кластер, чем их средние значения.

K-средние vs Естественные перерывы Дженкса

Алгоритм естественных перерывов Дженкса, такой как k-средние, стремится максимально минимизировать точку данных до расстояния до центроида, при этом неочевидная на первый взгляд разница заключается в том, что этот алгоритм также стремится нарисовать эти классы как можно дальше друг от друга для лучшего различения. Это достигается за счет определения "естественных группировок" (natural groupings) данных. Эти "естественные группировки" идентифицируются внутри кластеров в точках, где дисперсия значительно увеличивается, и эти точки называются разрывами, отсюда и название алгоритма. Разрывы подчеркиваются за счет минимизации дисперсии внутри каждого кластера. Алгоритм лучше подходит для наборов данных в стиле классификации, чем для регрессивных или непрерывных типов. При всем этом, как и алгоритм k-медиан, он получает преимущества в чувствительности к выбросам, а также в общей интерпретации по сравнению с типичными k-средними.

K-средние vs k-медоиды

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

K-средние vs Нечеткая кластеризация

Как уже упоминалось, нечеткая кластеризация обеспечивает регрессивный вес каждой точки данных, который будет иметь векторный формат в зависимости от количества задействованных кластеров. Этот вес будет находиться в диапазоне 0,0–1,0 для каждого кластера при использовании нечеткого прототипа (функции принадлежности) в отличие от k-средних, в которых используется определенный центроид. Таким образом алгоритм предоставляет больше информации и, следовательно, лучше представляет данные. Он превосходит типичные k-средние по всем пунктам, упомянутым выше, при этом основным недостатком является интенсивность вычислений.

K-means++

Чтобы сделать наивную кластеризацию k-средних более эффективной, обычно используется инициализация k-means++, при которой начальные центроиды менее случайны, но более пропорционально распределены по данным. Результаты тестирования показали более быстрое принятие решений и сходимость к целевым центроидам. Достигается лучшее качество кластера в целом и меньшая чувствительность не только к выбросам точек данных, но и к первоначальному выбору точек центроида.


Данные

Как и в статье об агломеративной иерархической кластеризации, мы будем использовать готовые классы k-средних AlgLib для разработки аналогичного простого алгоритма, который мы использовали для этой статьи, и посмотрим, сможем ли мы получить результат перекрестной проверки. Тестируемый символ - GBPUSD. Мы проводим тесты с 2022.01.01 по 2023.02.01, а затем выполняем переход от этой даты до 2023.10.01. Мы будем использовать дневной интервал и выполнять финальные прогоны на реальных тиках в течение тестового периода.


Структура

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

      double _dbl_min=-1000.0,_dbl_max=1000.0;
      
      for(int i=0;i<m_training_points;i++)
      {
         for(int ii=0;ii<m_point_features;ii++)
         {
            double _value=m_close.GetData(StartIndex()+i)-m_close.GetData(StartIndex()+ii+i+1);
            if(_dbl_min>=_value||!MathIsValidNumber(_value)||_value>=_dbl_max){ _value=0.0; }
            m_data.x.Set(i,ii,_value);
            matrix _m=m_data.x.ToMatrix();if(_m.HasNan()){ _m.ReplaceNan(0.0); }m_data.x=CMatrixDouble(_m);
         }
         
         if(i>0)//assign classifier only for data points for which eventual bar range is known
         {
            double _value=m_close.GetData(StartIndex()+i-1)-m_close.GetData(StartIndex()+i);
            if(_dbl_min>=_value||!MathIsValidNumber(_value)||_value>=_dbl_max){ _value=0.0; }
            m_data.y.Set(i-1,_value);
            vector _v=m_data.y.ToVector();if(_v.HasNan()){ _v.ReplaceNan(0.0); }m_data.y=CRowDouble(_v);
         }
      }


ALGLIB

Библиотека AlgLib уже неоднократно упоминалась в этой серии статей, поэтому мы сразу перейдем к коду формирования наших кластеров. В центре нашего внимания будут две функции в библиотеке: SelectInitialCenters, которая имеет решающее значение для ускорения всего процесса, поскольку, как уже упоминалось, слишком случайный первоначальный выбор кластеров имеет тенденцию увеличивать время сходимости к нужным кластерам. После запуска этой функции мы будем использовать алгоритм Ллойда для точной настройки первоначального выбора кластера, и для этого мы обратимся к функции KMeansGenerateInternal.

Выбор исходных кластеров с доступной функцией может осуществляться одним из трех способов: либо случайным образом, либо с помощью k-means++, либо с помощью быстрой жадной инициализации. Давайте кратко пробежимся по каждому. При случайном выборе кластера, как и в двух других случаях, выходные кластеры сохраняются в выходной матрице с именем ct, при этом каждая строка представляет собой кластер, в котором количество строк ct соответствует предполагаемому номеру кластера, а столбцы будут равны характеристикам или кардиналу вектора каждой точки данных в наборе данных. Таким образом, случайный выбор просто присваивает один раз каждой строке ct точку данных, выбранную случайным образом из набора входных данных. Это показано ниже:

//--- Random initialization
   if(initalgo==1)
     {
      for(i=0; i<k; i++)
        {
         j=CHighQualityRand::HQRndUniformI(rs,npoints);
         ct.Row(i,xy[j]+0);
        }
      return;
     }


При использовании K-means++ мы также начинаем с выбора случайного центра, но только для первого кластера. Раньше мы делали выбор для всех кластеров. Затем мы измеряем расстояние между каждой точкой набора данных и случайно выбранным центром кластера, регистрируя квадратную сумму этих расстояний для каждой строки (или потенциального кластера), и в случае, если эта сумма равна нулю, мы просто выбираем случайный центроид для кластера. Для всех ненулевых сумм, хранящихся в переменной s, мы выбираем точку, наиболее удаленную от нашего случайно выбранного исходного кластера. Код довольно сложен, но этот краткий фрагмент с комментариями сделает его понятнее:

//--- k-means++ initialization
   if(initalgo==2)
     {
      //--- Prepare distances array.
      //--- Select initial center at random.
      initbuf.m_ra0=vector<double>::Full(npoints,CMath::m_maxrealnumber);
      ptidx=CHighQualityRand::HQRndUniformI(rs,npoints);
      ct.Row(0,xy[ptidx]+0);
      //--- For each newly added center repeat:
      //--- * reevaluate distances from points to best centers
      //--- * sample points with probability dependent on distance
      //--- * add new center
      for(cidx=0; cidx<k-1; cidx++)
        {
         //--- Reevaluate distances
         s=0.0;
         for(i=0; i<npoints; i++)
           {
            v=0.0;
            for(j=0; j<=nvars-1; j++)
              {
               vv=xy.Get(i,j)-ct.Get(cidx,j);
               v+=vv*vv;
              }
            if(v<initbuf.m_ra0[i])
               initbuf.m_ra0.Set(i,v);
            s+=initbuf.m_ra0[i];
           }
         //
         //--- If all distances are zero, it means that we can not find enough
         //--- distinct points. In this case we just select non-distinct center
         //--- at random and continue iterations. This issue will be handled
         //--- later in the FixCenters() function.
         //
         if(s==0.0)
           {
            ptidx=CHighQualityRand::HQRndUniformI(rs,npoints);
            ct.Row(cidx+1,xy[ptidx]+0);
            continue;
           }
         //--- Select point as center using its distance.
         //--- We also handle situation when because of rounding errors
         //--- no point was selected - in this case, last non-zero one
         //--- will be used.
         v=CHighQualityRand::HQRndUniformR(rs);
         vv=0.0;
         lastnz=-1;
         ptidx=-1;
         for(i=0; i<npoints; i++)
           {
            if(initbuf.m_ra0[i]==0.0)
               continue;
            lastnz=i;
            vv+=initbuf.m_ra0[i];
            if(v<=vv/s)
              {
               ptidx=i;
               break;
              }
           }
         if(!CAp::Assert(lastnz>=0,__FUNCTION__": integrity error"))
            return;
         if(ptidx<0)
            ptidx=lastnz;
         ct.Row(cidx+1,xy[ptidx]+0);
        }
      return;
     }

AlgLib располагает некоторой общедоступной справочной документацией.

Наконец, для алгоритма быстрой жадной инициализации, который был вдохновлен вариантом k-средних, называемым k-means++, выполняется несколько раундов, где для каждого раунда: производятся расчеты расстояния, ближайшего к выбранному в данный момент центроиду; затем выполняется независимая выборка примерно половины ожидаемого размера кластера, где вероятность выбора точки пропорциональна ее расстоянию от текущего центроида. Процесс повторяется до тех пор, пока количество выбранных точек не станет вдвое больше, чем могло бы заполнить кластер. Затем при выборе очень большой выборки из нее выполняется "жадный выбор" до тех пор, пока не будет достигнут меньший размер выборки с приоритетом, отдаваемым точкам, наиболее удаленным от центроидов. Процесс очень трудоемкий и запутанный. Код комментариями приведен ниже:

//--- "Fast-greedy" algorithm based on "Scalable k-means++".
//--- We perform several rounds, within each round we sample about 0.5*K points
//--- (not exactly 0.5*K) until we have 2*K points sampled. Before each round
//--- we calculate distances from dataset points to closest points sampled so far.
//--- We sample dataset points independently using distance xtimes 0.5*K divided by total
//--- as probability (similar to k-means++, but each point is sampled independently;
//--- after each round we have roughtly 0.5*K points added to sample).
//--- After sampling is done, we run "greedy" version of k-means++ on this subsample
//--- which selects most distant point on every round.
   if(initalgo==3)
     {
      //--- Prepare arrays.
      //--- Select initial center at random, add it to "new" part of sample,
      //--- which is stored at the beginning of the array
      samplesize=2*k;
      samplescale=0.5*k;
      CApServ::RMatrixSetLengthAtLeast(initbuf.m_rm0,samplesize,nvars);
      ptidx=CHighQualityRand::HQRndUniformI(rs,npoints);
      initbuf.m_rm0.Row(0,xy[ptidx]+0);
      samplescntnew=1;
      samplescntall=1;
      initbuf.m_ra1=vector<double>::Zeros(npoints);
      CApServ::IVectorSetLengthAtLeast(initbuf.m_ia1,npoints);
      initbuf.m_ra0=vector<double>::Full(npoints,CMath::m_maxrealnumber);
      //--- Repeat until samples count is 2*K
      while(samplescntall<samplesize)
        {
         //--- Evaluate distances from points to NEW centers, store to RA1.
         //--- Reset counter of "new" centers.
         KMeansUpdateDistances(xy,0,npoints,nvars,initbuf.m_rm0,samplescntall-samplescntnew,samplescntall,initbuf.m_ia1,initbuf.m_ra1);
         samplescntnew=0;
         //--- Merge new distances with old ones.
         //--- Calculate sum of distances, if sum is exactly zero - fill sample
         //--- by randomly selected points and terminate.
         s=0.0;
         for(i=0; i<npoints; i++)
           {
            initbuf.m_ra0.Set(i,MathMin(initbuf.m_ra0[i],initbuf.m_ra1[i]));
            s+=initbuf.m_ra0[i];
           }
         if(s==0.0)
           {
            while(samplescntall<samplesize)
              {
               ptidx=CHighQualityRand::HQRndUniformI(rs,npoints);
               initbuf.m_rm0.Row(samplescntall,xy[ptidx]+0);
               samplescntall++;
               samplescntnew++;
              }
            break;
           }
         //--- Sample points independently.
         for(i=0; i<npoints; i++)
           {
            if(samplescntall==samplesize)
               break;
            if(initbuf.m_ra0[i]==0.0)
               continue;
            if(CHighQualityRand::HQRndUniformR(rs)<=(samplescale*initbuf.m_ra0[i]/s))
              {
               initbuf.m_rm0.Row(samplescntall,xy[i]+0);
               samplescntall++;
               samplescntnew++;
              }
           }
        }
      //--- Run greedy version of k-means on sampled points
 
      initbuf.m_ra0=vector<double>::Full(samplescntall,CMath::m_maxrealnumber);
      ptidx=CHighQualityRand::HQRndUniformI(rs,samplescntall);
      ct.Row(0,initbuf.m_rm0[ptidx]+0);
      for(cidx=0; cidx<k-1; cidx++)
        {
         //--- Reevaluate distances
         for(i=0; i<samplescntall; i++)
           {
            v=0.0;
            for(j=0; j<nvars; j++)
              {
               vv=initbuf.m_rm0.Get(i,j)-ct.Get(cidx,j);
               v+=vv*vv;
              }
            if(v<initbuf.m_ra0[i])
               initbuf.m_ra0.Set(i,v);
           }
         //--- Select point as center in greedy manner - most distant
         //--- point is selected.
         ptidx=0;
         for(i=0; i<samplescntall; i++)
           {
            if(initbuf.m_ra0[i]>initbuf.m_ra0[ptidx])
               ptidx=i;
           }
         ct.Row(cidx+1,initbuf.m_rm0[ptidx]+0);
        }
      return;
     }


Процесс обеспечивает репрезентативные центроиды и эффективность на следующем этапе.

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

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


Прогнозирование

K-средние, такие как агломеративная иерархическая кластеризация, по своей сути являются классификацией, которая не контролируется, поэтому, как и раньше, если мы хотим выполнить какую-либо регрессию или прогнозирование, нам нужно будет добавить данные столбца "y", которые отстают от нашего кластеризованного набора данных. Таким образом, эти данные "y" также будут изменены по цене закрытия, но на 1 бар впереди кластеризованных данных, чтобы эффективно маркировать кластеры, а для эффективности набор данных "y" заполняется тем же циклом for, который заполняет для кластеризации набор данных матрицы x. Это показано в коротком фрагменте кода ниже:

         if(i>0)//assign classifier only for data points for which eventual bar range is known
         {
            double _value=m_close.GetData(StartIndex()+i-1)-m_close.GetData(StartIndex()+i);
            if(_dbl_min>=_value||!MathIsValidNumber(_value)||_value>=_dbl_max){ _value=0.0; }
            m_data.y.Set(i-1,_value);
            vector _v=m_data.y.ToVector();if(_v.HasNan()){ _v.ReplaceNan(0.0); }m_data.y=CRowDouble(_v);
         }

После того, как матрица "x" и массив "y" заполнены данными, определение кластера переходит к шагам, уже упомянутым выше, а затем определяется кластер текущих изменений цены закрытия или верхняя строка матрицы "x". Поскольку она обрабатывается для кластеризации вместе с другими точками данных, она будет иметь индекс кластера. С помощью этого кластерного индекса мы сравниваем его с уже "помеченными" точками данных, для которых известно возможное изменение цены закрытия, чтобы получить сумму этих возможных изменений. С этой суммой мы можем легко использовать получение среднего изменения, которое, когда мы нормализуем текущий диапазон (или волатильность), дает нам вес в диапазоне 0 – 1.

//+------------------------------------------------------------------+
//| "Voting" that price will fall.                                   |
//+------------------------------------------------------------------+
int CSignalKMEANS::ShortCondition(void)
  {
      ...
      
      double _output=GetOutput();
      
      int _range_size=1;
      
      double _range=m_high.GetData(m_high.MaxIndex(StartIndex(),StartIndex()+_range_size))-m_low.GetData(m_low.MinIndex(StartIndex(),StartIndex()+_range_size));
      
      _output/=fmax(_range,m_symbol.Point());
      _output*=100.0;
      
      if(_output<0.0){ result=int(fmax(-100.0,round(_output)))*-1; }
      
      ...
  }


Функции LongCondition и ShortCondition возвращают значения в диапазоне от 0 до 100, поэтому наше нормализованное значение необходимо умножить на 100.


Оценка и результаты

По результатам тестирования на истории за период с 2022.01.01 по 2023.02.01 мы получаем следующий отчет:

b_1

Отчет основывался на следующих входных данных, полученных в ходе оптимизации:

i_1

При форвард-тестировании с этими настройками с 2023.02.02 по 2023.10.01 мы получаем следующий отчет:

f_1

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


Реализация с помощью фрактальных волн

Давайте теперь рассмотрим вариант, который использует данные индикатора фракталов в отличие от изменений цены закрытия. Индикатор фракталов немного сложно использовать "из коробки", особенно при попытке реализовать его с помощью советника, поскольку при обновлении буферы не содержат значений индикаторов или цен для каждого индекса. Нужно проверить каждый индекс буфера и посмотреть, действительно ли существует "фрактал" (то есть цена), и если "фрактала" у него нет, то заполнителем по умолчанию является максимальное двойное значение. Вот как мы подготавливаем фрактальные данные в обновленной функции GetOutput:

//+------------------------------------------------------------------+
//| Get k-means cluster output from identified cluster.              |
//+------------------------------------------------------------------+
double CSignalKMEANS::GetOutput()
   {
      ...
      
      int _size=m_training_points+m_point_features+1,_index=0;
      
      for(int i=0;i<m_fractals.Available();i++)
      {
         double _0=m_fractals.GetData(0,i);
         double _1=m_fractals.GetData(1,i);
         
         if(_0!=DBL_MAX||_1!=DBL_MAX)
         { 
            double _v=0.0;
            if(_0!=DBL_MAX){_v=_0;}
            if(_1!=DBL_MAX){_v=_1;}
            if(!m_loaded){ m_wave[_index]=_v; _index++; } 
            else
            {
               for(int i=_size-1;i>0;i--){ m_wave[i]=m_wave[i-1]; }
               m_wave[0]=_v; break;
            }
         }
         
         if(_index>=int(m_wave.Size())){ break; }
      }
      
      if(!m_loaded){ m_loaded=true; }
      
      if(m_wave[_size-1]==0.0){ return(0.0); }

      ...
      
      ...
   }


Чтобы получить фактические ценовые фракталы, нам необходимо прежде всего правильно обновить объект фрактального индикатора. Как только это будет сделано, нам нужно получить общее количество доступных индексов буфера, и это значение показывает, сколько индексов нам нужно пройти в цикле for при поиске фрактальных ценовых точек. При этом нам нужно помнить, что фрактальный индикатор имеет два буфера с индексами 0 и 1. Индекс буфера 0 предназначен для высоких фракталов, а индекс буфера 1 — для фракталов низкого уровня. Это означает, что в нашем цикле for у нас будет 2 значения, одновременно проверяющих эти индексные буферы на наличие фрактальных ценовых точек, и когда любой из них регистрирует цену (только один из них может регистрировать цену одновременно), мы добавляем это значение к нашему вектору m_wave.

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

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

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

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

b_2


f_2

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

g

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


Заключение

Мы рассмотрели кластеризацию k-средних и то, как готовая реализация благодаря AlgLib может быть реализована в двух вариантах: с необработанными ценами закрытия и с фрактальными ценовыми данными.

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


Ссылки

Википедия

ResearchGate


Приложение

Перед использованием приложенных файлов рекомендую ознакомиться со статьей о Мастере MQL5.


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

Прикрепленные файлы |
Kmeans.mq5 (6.66 KB)
kmeans_r1.mq5 (6.85 KB)
SignalWZ_9.mqh (10.17 KB)
SignalWZ_9_r1.mqh (11.5 KB)
Нейросети — это просто (Часть 85): Многомерное прогнозирование временных рядов Нейросети — это просто (Часть 85): Многомерное прогнозирование временных рядов
В данной статье хочу познакомить Вас с новым комплексным методом прогнозирования временных рядов, который гармонично сочетает в себе преимущества линейных моделей и трансформеров.
Фильтрация и извлечение признаков в частотной области Фильтрация и извлечение признаков в частотной области
В этой статье мы рассмотрим применение цифровых фильтров к временным рядам, представленным в частотной области, с целью извлечения уникальных признаков, которые могут быть полезными для моделей прогнозирования.
Машинное обучение и Data Science (Часть 17): Растут ли деньги на деревьях? Случайные леса в форекс-трейдинге Машинное обучение и Data Science (Часть 17): Растут ли деньги на деревьях? Случайные леса в форекс-трейдинге
Эта статья познакомит вас с секретами алгоритмической алхимии, познакомит с искусством и точностью особенностей финансовых ландшафтов. Вы узнаете, как случайные леса преобразуют данные в прогнозы и помогают ориентироваться в сложностях финансовых рынков. Мы постараемся определить роль случайных лесов в отношении финансовых данных и проверить, смогут ли они помочь увеличить прибыль.
Модифицированный советник Grid-Hedge в MQL5 (Часть I): Создание простого хеджирующего советника Модифицированный советник Grid-Hedge в MQL5 (Часть I): Создание простого хеджирующего советника
Мы будем создавать простой хеджирующий советник в качестве основы для нашего более продвинутого советника Grid-Hedge, который будет представлять собой смесь классической сетки и классических стратегий хеджирования. К концу этой статьи вы узнаете, как создать простую стратегию хеджирования, а также что говорят люди о прибыльности этой стратегии.