K-Means в MQL5: Фильтрация ложных сигналов MACD | FinFluct

K-Means в MQL5: Фильтрация ложных сигналов MACD | FinFluct MQL4 / MQL5 (MetaTrader)
Подробное руководство по реализации алгоритма кластеризации K-Means в MQL5 для эффективной фильтрации ложных торговых сигналов индикатора MACD. Улучшите точность вашей торговой стратегии.
Суть: Реализация алгоритма K-Means в MQL5 для кластеризации сигналов MACD (линия MACD, сигнальная линия, гистограмма) и определения наиболее надежных торговых точек, отсеивая ложные входы.

Исходный код

Представленный ниже код включает в себя два основных компонента: класс CKMeans, реализующий алгоритм кластеризации K-Means, и пример экспертного советника (EA) на MQL5, который использует этот класс для фильтрации торговых сигналов индикатора MACD. Класс CKMeans способен работать с многомерными данными, что позволяет нам использовать значения линии MACD, сигнальной линии и гистограммы как отдельные измерения для каждого сигнала.

Экспертный советник на этапе инициализации (OnInit()) собирает исторические данные MACD, обучает модель K-Means, а затем анализирует полученные центроиды для определения, какие кластеры соответствуют сильным бычьим, медвежьим или нейтральным сигналам. В дальнейшем, на каждом новом баре (OnTick()), текущие значения MACD классифицируются, и торговые действия предпринимаются только в случае принадлежности сигнала к ‘сильным’ кластерам, отфильтровывая тем самым ложные или слабые сигналы.


//+------------------------------------------------------------------+
//| K-Means Clustering Class for MQL5                                |
//+------------------------------------------------------------------+
class CKMeans
{
private:
    int m_num_clusters;       // Количество кластеров (K)
    int m_num_dimensions;     // Размерность данных (например, 3 для MACD, Signal, Histogram)
    double m_centroids[][];   // Центроиды кластеров: [cluster_idx][dimension_idx]
    double m_data_points[][]; // Точки данных: [point_idx][dimension_idx]
    int m_assignments[];      // Принадлежность точек к кластерам: [point_idx] = cluster_idx
    int m_data_count;         // Текущее количество добавленных точек данных
    int m_data_capacity;      // Текущая емкость массива m_data_points

public:
    // Конструктор класса K-Means
    CKMeans(int num_clusters, int num_dimensions)
    {
        m_num_clusters = num_clusters;
        m_num_dimensions = num_dimensions;
        m_data_count = 0;
        m_data_capacity = 1000; // Начальная емкость, будет увеличиваться при необходимости

        // Инициализация массивов для центроидов
        ArrayResize(m_centroids, m_num_clusters);
        for (int i = 0; i < m_num_clusters; i++)
        {
            ArrayResize(m_centroids[i], m_num_dimensions);
        }

        // Инициализация массивов для точек данных и их принадлежности
        ArrayResize(m_data_points, m_data_capacity);
        for (int i = 0; i < m_data_capacity; i++)
        {
            ArrayResize(m_data_points[i], m_num_dimensions);
        }
        ArrayResize(m_assignments, m_data_capacity);
    }

    // Деструктор (MQL5 автоматически управляет памятью для динамических массивов-членов)
    ~CKMeans()
    {
    }

    // Добавление новой точки данных для обучения
    void AddDataPoint(const double& data[])
    {
        if (ArraySize(data) != m_num_dimensions)
        {
            Print("Error: Data point dimensions mismatch.");
            return;
        }

        // Увеличение емкости массива, если необходимо
        if (m_data_count >= m_data_capacity)
        {
            m_data_capacity *= 2; // Удваиваем емкость
            ArrayResize(m_data_points, m_data_capacity);
            for (int i = m_data_count; i < m_data_capacity; i++)
            {
                ArrayResize(m_data_points[i], m_num_dimensions);
            }
            ArrayResize(m_assignments, m_data_capacity);
        }

        // Копирование данных
        for (int i = 0; i < m_num_dimensions; i++)
        {
            m_data_points[m_data_count][i] = data[i];
        }
        m_data_count++;
    }

    // Обучение алгоритма K-Means
    bool Train(int max_iterations)
    {
        if (m_data_count < m_num_clusters)
        {
            Print("Error: Not enough data points to form clusters.");
            return false;
        }

        InitializeCentroids(); // Инициализация центроидов

        bool changed = true;
        for (int iter = 0; iter < max_iterations && changed; iter++)
        {
            changed = AssignToClusters(); // Назначение точек кластерам
            UpdateCentroids();            // Обновление центроидов
        }
        return true;
    }

    // Классификация новой точки данных
    int Classify(const double& data[]) const
    {
        if (ArraySize(data) != m_num_dimensions)
        {
            Print("Error: Data point dimensions mismatch for classification.");
            return -1; // Индикация ошибки
        }

        int closest_cluster = -1;
        double min_distance = DBL_MAX;

        // Поиск ближайшего центроида
        for (int i = 0; i < m_num_clusters; i++)
        {
            double dist = CalculateDistance(data, m_centroids[i]);
            if (dist < min_distance)
            {
                min_distance = dist;
                closest_cluster = i;
            }
        }
        return closest_cluster;
    }

    // Получение значения центроида по индексу кластера и размерности
    double GetCentroidValue(int cluster_idx, int dim_idx) const
    {
        if (cluster_idx < 0 || cluster_idx >= m_num_clusters ||
            dim_idx < 0 || dim_idx >= m_num_dimensions)
        {
            Print("Error: Invalid centroid index.");
            return 0.0;
        }
        return m_centroids[cluster_idx][dim_idx];
    }

    // Получение количества точек данных
    int GetDataCount() const { return m_data_count; }

private:
    // Инициализация центроидов случайным выбором из существующих точек данных
    void InitializeCentroids()
    {
        MathSrand(GetTickCount()); // Инициализация генератора случайных чисел

        int picked_indices[];
        ArrayResize(picked_indices, m_num_clusters);
        ArrayInitialize(picked_indices, -1);

        for (int i = 0; i < m_num_clusters; i++)
        {
            int rand_idx;
            bool unique = false;
            while (!unique)
            {
                rand_idx = MathRand() % m_data_count;
                unique = true;
                for (int j = 0; j < i; j++)
                {
                    if (picked_indices[j] == rand_idx)
                    {
                        unique = false;
                        break;
                    }
                }
            }
            picked_indices[i] = rand_idx;

            for (int d = 0; d < m_num_dimensions; d++)
            {
                m_centroids[i][d] = m_data_points[rand_idx][d];
            }
        }
    }

    // Вычисление Евклидова расстояния между двумя точками
    double CalculateDistance(const double& p1[], const double& p2[]) const
    {
        double sum_sq_diff = 0.0;
        for (int i = 0; i < m_num_dimensions; i++)
        {
            sum_sq_diff += MathPow(p1[i] - p2[i], 2);
        }
        return MathSqrt(sum_sq_diff);
    }

    // Назначение каждой точки данных ближайшему центроиду
    bool AssignToClusters()
    {
        bool changed = false;
        for (int i = 0; i < m_data_count; i++)
        {
            int closest_cluster = -1;
            double min_distance = DBL_MAX;

            for (int j = 0; j < m_num_clusters; j++)
            {
                double dist = CalculateDistance(m_data_points[i], m_centroids[j]);
                if (dist < min_distance)
                {
                    min_distance = dist;
                    closest_cluster = j;
                }
            }

            if (m_assignments[i] != closest_cluster)
            {
                m_assignments[i] = closest_cluster;
                changed = true;
            }
        }
        return changed;
    }

    // Обновление позиций центроидов на основе текущих назначений
    void UpdateCentroids()
    {
        double new_centroids[][];
        int cluster_counts[];

        ArrayResize(new_centroids, m_num_clusters);
        for (int i = 0; i < m_num_clusters; i++)
        {
            ArrayResize(new_centroids[i], m_num_dimensions);
            ArrayInitialize(new_centroids[i], 0.0);
        }
        ArrayResize(cluster_counts, m_num_clusters);
        ArrayInitialize(cluster_counts, 0);

        for (int i = 0; i < m_data_count; i++)
        {
            int cluster_idx = m_assignments[i];
            if (cluster_idx != -1)
            {
                for (int d = 0; d < m_num_dimensions; d++)
                {
                    new_centroids[cluster_idx][d] += m_data_points[i][d];
                }
                cluster_counts[cluster_idx]++;
            }
        }

        for (int i = 0; i < m_num_clusters; i++)
        {
            if (cluster_counts[i] > 0)
            {
                for (int d = 0; d < m_num_dimensions; d++)
                {
                    m_centroids[i][d] = new_centroids[i][d] / cluster_counts[i];
                }
            }
            // Если кластер становится пустым, его центроид остается на месте.
            // В более сложных реализациях можно было бы переинициализировать пустые кластеры.
        }
    }
};

//+------------------------------------------------------------------+
//| Expert Advisor (EA) using K-Means for MACD signal filtering      |
//+------------------------------------------------------------------+
#property version   "1.00"
#property copyright "FinFluct"
#property link      "https://finfluct.com"
#property description "K-Means clustering for MACD signal filtering"
#property strict

// --- Входные параметры K-Means ---
input int K_Clusters = 4;             // Количество кластеров для K-Means
input int MaxIterations = 100;        // Максимальное количество итераций для обучения K-Means
input int HistoryBarsForTraining = 500; // Количество исторических баров для обучения K-Means

// --- Входные параметры MACD ---
input int MACD_FastPeriod = 12;
input int MACD_SlowPeriod = 26;
input int MACD_SignalPeriod = 9;
input double MinSignalStrengthThreshold = 0.01; // Порог для определения "силы" сигнала кластера

// Глобальные переменные
CKMeans *kmeans = NULL; // Указатель на объект K-Means
int macd_handle;        // Хэндл индикатора MACD

// Размерыность данных для MACD: (MACD_Line, Signal_Line, Histogram_Value)
const int MACD_DIMENSIONS = 3;

// Глобальный массив для хранения типа сигнала, ассоциированного с каждым кластером.
// 1 = Bullish (покупка), -1 = Bearish (продажа), 0 = Neutral/Weak (бездействовать)
int cluster_signal_type[];

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    // Инициализация индикатора MACD
    macd_handle = iMACD(_Symbol, _Period, MACD_FastPeriod, MACD_SlowPeriod, MACD_SignalPeriod);
    if (macd_handle == INVALID_HANDLE)
    {
        Print("Failed to create MACD indicator handle. Error: ", GetLastError());
        return INIT_FAILED;
    }

    // Создание объекта K-Means
    kmeans = new CKMeans(K_Clusters, MACD_DIMENSIONS);
    if (kmeans == NULL)
    {
        Print("Failed to create CKMeans object.");
        return INIT_FAILED;
    }

    // --- Сбор исторических данных для обучения K-Means ---
    double macd_buffer[], signal_buffer[], hist_buffer[];
    ArraySetAsSeries(macd_buffer, true);
    ArraySetAsSeries(signal_buffer, true);
    ArraySetAsSeries(hist_buffer, true);

    // Копирование данных MACD из буферов индикатора
    int copied = CopyBuffer(macd_handle, INDICATOR_DATA_MACD, 0, HistoryBarsForTraining, macd_buffer);
    if (copied != HistoryBarsForTraining)
    {
        Print("Failed to copy MACD buffer. Copied: ", copied, ", Expected: ", HistoryBarsForTraining);
        delete kmeans;
        return INIT_FAILED;
    }
    copied = CopyBuffer(macd_handle, INDICATOR_DATA_SIGNAL, 0, HistoryBarsForTraining, signal_buffer);
    if (copied != HistoryBarsForTraining)
    {
        Print("Failed to copy Signal buffer. Copied: ", copied, ", Expected: ", HistoryBarsForTraining);
        delete kmeans;
        return INIT_FAILED;
    }
    copied = CopyBuffer(macd_handle, INDICATOR_DATA_HISTOGRAM, 0, HistoryBarsForTraining, hist_buffer);
    if (copied != HistoryBarsForTraining)
    {
        Print("Failed to copy Histogram buffer. Copied: ", copied, ", Expected: ", HistoryBarsForTraining);
        delete kmeans;
        return INIT_FAILED;
    }

    // Добавление исторических данных в K-Means для обучения
    double data_point[MACD_DIMENSIONS];
    for (int i = HistoryBarsForTraining - 1; i >= 0; i--) // Итерация от старых баров к новым
    {
        data_point[0] = macd_buffer[i];
        data_point[1] = signal_buffer[i];
        data_point[2] = hist_buffer[i];
        kmeans.AddDataPoint(data_point);
    }

    // --- Обучение K-Means ---
    Print("Training K-Means with ", kmeans.GetDataCount(), " data points...");
    if (!kmeans.Train(MaxIterations))
    {
        Print("K-Means training failed.");
        delete kmeans;
        return INIT_FAILED;
    }
    Print("K-Means training completed.");

    // --- Анализ центроидов и присвоение типов сигналов ---
    // Этот этап требует доменных знаний. Здесь используется простая эвристика:
    // Если MACD_Line > 0, Signal_Line > 0, Histogram > MinSignalStrengthThreshold -> Bullish
    // Если MACD_Line < 0, Signal_Line < 0, Histogram < -MinSignalStrengthThreshold -> Bearish
    // В противном случае -> Neutral/Weak
    ArrayResize(cluster_signal_type, K_Clusters);
    for (int i = 0; i < K_Clusters; i++)
    {
        double macd_val = kmeans.GetCentroidValue(i, 0);
        double signal_val = kmeans.GetCentroidValue(i, 1);
        double hist_val = kmeans.GetCentroidValue(i, 2);

        PrintFormat("Centroid %d: MACD=%.4f, Signal=%.4f, Hist=%.4f", i, macd_val, signal_val, hist_val);

        if (macd_val > 0 && signal_val > 0 && hist_val > MinSignalStrengthThreshold)
        {
            cluster_signal_type[i] = 1; // Bullish
            PrintFormat("  -> Cluster %d identified as BULLISH", i);
        }
        else if (macd_val < 0 && signal_val < 0 && hist_val < -MinSignalStrengthThreshold)
        {
            cluster_signal_type[i] = -1; // Bearish
            PrintFormat("  -> Cluster %d identified as BEARISH", i);
        }
        else
        {
            cluster_signal_type[i] = 0; // Neutral/Weak
            PrintFormat("  -> Cluster %d identified as NEUTRAL/WEAK", i);
        }
    }

    return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
    // Освобождение памяти, занятой объектом K-Means
    if (kmeans != NULL)
    {
        delete kmeans;
        kmeans = NULL;
    }
    // Освобождение хэндла индикатора
    if (macd_handle != INVALID_HANDLE)
    {
        IndicatorRelease(macd_handle);
    }
}

//+------------------------------------------------------------------+
//| New bar event handler                                            |
//+------------------------------------------------------------------+
void OnTick()
{
    static datetime prev_time = 0;
    datetime current_time = iTime(_Symbol, _Period, 0);

    // Обработка только на новом баре
    if (current_time == prev_time)
    {
        return;
    }
    prev_time = current_time;

    // --- Получение текущих значений MACD ---
    double macd_current[], signal_current[], hist_current[];
    ArraySetAsSeries(macd_current, true);
    ArraySetAsSeries(signal_current, true);
    ArraySetAsSeries(hist_current, true);

    if (CopyBuffer(macd_handle, INDICATOR_DATA_MACD, 0, 1, macd_current) != 1 ||
        CopyBuffer(macd_handle, INDICATOR_DATA_SIGNAL, 0, 1, signal_current) != 1 ||
        CopyBuffer(macd_handle, INDICATOR_DATA_HISTOGRAM, 0, 1, hist_current) != 1)
    {
        Print("Failed to get current MACD values.");
        return;
    }

    double current_data_point[MACD_DIMENSIONS];
    current_data_point[0] = macd_current[0];
    current_data_point[1] = signal_current[0];
    current_data_point[2] = hist_current[0];

    // --- Классификация текущего сигнала с помощью K-Means ---
    int assigned_cluster = kmeans.Classify(current_data_point);

    if (assigned_cluster == -1)
    {
        Print("Error classifying current data point.");
        return;
    }

    int signal_type = cluster_signal_type[assigned_cluster];

    // --- Действие на основе отфильтрованного сигнала ---
    if (signal_type == 1) // Сильный бычий сигнал
    {
        Print("K-Means identified a strong BULLISH signal (Cluster ", assigned_cluster, ")");
        // Здесь реализуйте вашу логику покупки.
        // Например, проверьте наличие открытых позиций, откройте ордер на покупку.
        // Для более продвинутого управления ордерами и уведомлениями, рассмотрите интеграцию с внешними системами,
        // как описано в статье MQL5 и Telegram API: Отправка скриншотов при активации отложенных ордеров.
        // Также, для учета фундаментального контекста, можно использовать MQL5: Парсинг Экономического Календаря Встроенными Средствами.
    }
    else if (signal_type == -1) // Сильный медвежий сигнал
    {
        Print("K-Means identified a strong BEARISH signal (Cluster ", assigned_cluster, ")");
        // Здесь реализуйте вашу логику продажи.
        // Не забудьте управлять стоп-лоссами и тейк-профитами, возможно, используя динамические методы,
        // такие как MQL5: Трейлинг-стоп по Parabolic SAR с динамическим шагом.
    }
    else // Нейтральный/слабый сигнал
    {
        Print("K-Means identified a NEUTRAL/WEAK signal (Cluster ", assigned_cluster, "). No action.");
        // В этом случае торговые действия не предпринимаются, или можно рассмотреть закрытие слабых позиций.
    }
}

Разбор параметров

  • K_Clusters: Количество кластеров, на которые алгоритм K-Means будет разделять данные. Оптимальное значение зависит от характера данных и может быть найдено путем экспериментов. Слишком малое или слишком большое количество кластеров может снизить эффективность фильтрации.
  • MaxIterations: Максимальное количество итераций, которое алгоритм K-Means будет выполнять во время обучения. Обычно 100-300 итераций достаточно для сходимости.
  • HistoryBarsForTraining: Количество исторических баров, данные с которых будут использоваться для обучения модели K-Means. Чем больше данных, тем потенциально точнее модель, но и дольше время инициализации. Рекомендуется использовать достаточно большой объем данных, чтобы охватить различные рыночные условия.
  • MACD_FastPeriod: Период быстрой скользящей средней для расчета MACD. Стандартное значение — 12.
  • MACD_SlowPeriod: Период медленной скользящей средней для расчета MACD. Стандартное значение — 26.
  • MACD_SignalPeriod: Период сигнальной линии MACD. Стандартное значение — 9.
  • MinSignalStrengthThreshold: Порог, используемый для эвристической классификации центроидов на ‘сильные’ бычьи/медвежьи и ‘нейтральные/слабые’ сигналы. Центроиды с гистограммой MACD выше этого порога (для бычьих) или ниже отрицательного значения этого порога (для медвежьих) будут считаться сильными.

Как запустить

Для запуска алгоритма кластеризации K-Means в MQL5 и его использования для фильтрации сигналов MACD, выполните следующие шаги:

  1. Создайте новый экспертный советник: Откройте MetaEditor (IDE для MQL5), перейдите в ‘Файл’ -> ‘Создать’ -> ‘Экспертный советник (шаблон)’ и дайте ему имя, например, KMeansMACDFilter.mq5.
  2. Вставьте код: Скопируйте весь предоставленный выше исходный код и вставьте его в файл KMeansMACDFilter.mq5, заменив содержимое по умолчанию.
  3. Скомпилируйте код: Нажмите кнопку ‘Компилировать’ в MetaEditor (или F7). Убедитесь, что нет ошибок компиляции. Если есть ошибки, проверьте правильность копирования кода и синтаксиса.
  4. Прикрепите к графику: Откройте терминал MetaTrader 5, выберите нужную валютную пару и таймфрейм. Перетащите скомпилированный экспертный советник KMeansMACDFilter из окна ‘Навигатор’ на график.
  5. Настройте параметры: Во вкладке ‘Входные параметры’ (Inputs) в окне настроек эксперта вы можете изменить значения K_Clusters, MaxIterations, HistoryBarsForTraining, а также параметры MACD и MinSignalStrengthThreshold. Экспериментируйте с этими значениями для достижения наилучших результатов на выбранном инструменте и таймфрейме.
  6. Анализ и оптимизация: После запуска эксперта, в журнале (вкладка ‘Эксперты’ в терминале) вы увидите информацию об обучении K-Means и классификации кластеров. Важно понимать, что эвристика для определения ‘сильных’ сигналов (cluster_signal_type) является базовой и может быть доработана. Вы можете вручную проанализировать значения центроидов после обучения и скорректировать логику в OnInit() для более точной классификации кластеров. Рекомендуется использовать тестер стратегий для бэктестинга и оптимизации параметров.

Этот подход позволяет динамически адаптироваться к рыночным условиям, кластеризуя паттерны MACD и отфильтровывая те, которые исторически не приводили к сильным движениям. Это значительно повышает качество торговых сигналов и снижает количество ложных входов. Для дальнейшего улучшения вашей торговой системы, вы можете рассмотреть интеграцию с внешними источниками данных, например, используя MQL5: Парсинг Экономического Календаря Встроенными Средствами для учета фундаментальных факторов, или настроить уведомления о сделках через MQL5 и Telegram API: Отправка скриншотов при активации отложенных ордеров. Не забывайте также о продвинутых методах управления позициями, таких как MQL5: Трейлинг-стоп по Parabolic SAR с динамическим шагом.

Оцените статью
FinFluct