Изучение стандартной библиотеки MQL5 (Часть 9): Использование ALGLIB для фильтрации избыточного числа сигналов пересечения скользящих средних
Содержание:
Введение
Пересечение скользящих средних, вероятно, является первой стратегией, которую пробует каждый трейдер. Пересечение быстрой скользящей средней снизу вверх даёт сигнал на покупку, а обратное пересечение — сигнал на продажу. Она проста, интуитивно понятна и удивительно эффективна на трендовых рынках. Проблема возникает, когда рынок движется в боковом направлении: цена колеблется вокруг средних, линии многократно пересекаются, и мы получаем череду убыточных сделок — печально известные ложные пересечения в “пиле”.
Многие трейдеры пытаются решить эту проблему, увеличивая периоды (делая средние более плавными, но медленными) или добавляя фильтры (RSI, ADX и т. д.). Эти подходы могут помочь, но они остаются эвристическими корректировками, наложенными поверх исходной структуры.
ALGLIB — это кроссплатформенная библиотека численного анализа, которая интегрирует передовые математические вычисления непосредственно в индикаторы и советники MetaTrader 5. Библиотека ALGLIB, первоначально разработанная для C++, C# и Python, была портирована в MQL5 и вошла в состав стандартной библиотеки, предоставляя нам доступ к сотням оптимизированных алгоритмов для линейной алгебры, оптимизации, обработки сигналов и анализа данных.
Тем, кто может испытывать страх перед сложной математикой, не стоит беспокоиться. Мы начнем с простых понятий и будем двигаться постепенно. ALGLIB берёт на себя всю тяжёлую численную работу — нам просто нужно понять, что делает каждая функция концептуально. Представьте это как вождение автомобиля: вам не нужно понимать термодинамику, чтобы нажать на педаль газа.
В этой статье мы преобразуем базовое пересечение скользящей средней в математически обработанную систему, используя три метода ALGLIB:
- базовые статистические фильтры,
- сингулярный спектральный анализ (SSA),
- и анализ производных на основе сплайнов.
В конце вы увидите, как математическая предварительная обработка может уменьшить частоту сигналов в условиях бокового рынка, сохраняя при этом быстродействие во время трендов.
Концепция
ALGLIB — это библиотека численного анализа, разрабатываемая на протяжении десятилетий. Она предоставляет оптимизированные реализации алгоритмов, на написание кода с нуля которых потребовалось бы значительное время. Среди её многочисленных функций есть инструменты для:
- Линейной алгебры — матричные операции, разложения, решения уравнений
- Оптимизации — минимизация с ограничениями и без ограничений
- Интерполяции и подгонки — сплайны, метод наименьших квадратов, рациональные приближения
- Анализа данных — кластеризация, анализ главных компонентов
- Обработки сигналов — FFT, свертка, фильтрация
- Статистики — распределения, проверка гипотез, корреляционный анализ
ALGLIB в основном работает со специализированными структурами, такими как CRowDouble для одномерных массивов и CMatrixDouble для матриц. Их можно преобразовывать в собственные векторные и матричные типы MQL5 и обратно.
Базовые фильтры (SMA, EMA, LRMA)
ALGLIB предоставляет готовые к использованию функции фильтрации, которые более эффективны и численно устойчивы, чем реализации, выполненные вручную. FilterSMA вычисляет простую скользящую среднюю с использованием скользящих сумм с защитой от переполнения. FilterEMA реализует экспоненциальную скользящую среднюю с точной обработкой коэффициентов. FilterLRMA выполняет линейную регрессию на скользящем окне — по сути, аппроксимирует прямую линию по последним K точкам и возвращает значение в текущей точке.
Математическая основа LRMA особенно интересна: для каждого окна она решает простую линейную регрессию y = a + b*x, где x представляет позицию внутри окна. Значение в текущей точке затем равно a + b*(окно‑1). Это дает нам фильтр, который реагирует на линейные тренды быстрее, чем простая средняя.
Значение фильтра в последней точке вычисляется из линии регрессии, что делает его более отзывчивым к трендовому движению, чем простая скользящая средняя.
Сингулярный спектральный анализ (SSA)
SSA разлагает временной ряд на структурные компоненты, такие как тренд, циклы и шум. Он работает путем построения матрицы траекторий, выполнения сингулярного разложения (SVD) и восстановления выбранных компонентов. Концептуально он отделяет долгосрочную структуру от краткосрочных флуктуаций.
В ALGLIB за этот процесс отвечает модель CSSAModel: достаточно задать размер окна и ранг реконструкции.
Сплайновая интерполяция и производные
Кубический сплайн — это гладкая кусочно-полиномиальная функция, проходящая через точки данных, с непрерывными первой и второй производными. Первая производная представляет собой мгновенную скорость изменения. Пересечения производной через ноль могут указывать на сдвиги в направлении.
Мы сравниваем четыре подхода:
- Базовый — пересечение EMA на исходных ценах
- Базовый фильтрованный — пересечение EMA на предварительно сглаженных ценах
- Фильтрованный с SSA — пересечение EMA на тренде SSA
- Производная сплайна — пересечения производной через ноль как индикаторы направления
Внедрив все четыре подхода в один индикатор, мы можем наглядно сравнить их поведение и увидеть, как предварительная обработка влияет на время появления сигналов и их надежность.
Реализация
Теперь мы будем пошагово создавать наш индикатор. Запустите MetaEditor, создайте новый индикатор и давайте углубимся в код. Назовем его ALGLIB_FilterDemo.mq5. Этот индикатор предназначен для демонстрации нескольких передовых методов фильтрации, предоставляемых библиотекой численного анализа ALGLIB. Применяя различные фильтры к одному и тому же ценовому ряду, мы можем сравнить их сглаживающие характеристики и влияние на простую торговую систему на пересечении средних. На итоговом графике будут отображаться несколько линий, каждая из которых представляет собой различную отфильтрованную версию цены, а также сигналы, возникающие при пересечении исходных скользящих средних и отфильтрованных рядов.
Шаг 1: Настройка структуры индикатора
Сначала необходимо подключить библиотеку ALGLIB и определить свойства нашего индикатора. Директива #include <Math/Alglib/alglib.mqh> добавляет все необходимые функции ALGLIB для фильтрации, SSA, сплайнов и линейной алгебры. Операторы #property задают основное поведение индикатора: он будет отображаться в главном окне графика (indicator_chart_window), и мы задаём десять буферов и десять графических построений. Выделение десяти буферов позволяет нам хранить и отображать исходную цену, исходную цену, три базовые линии, включая две EMA, используемые в кроссовере, пять отфильтрованных рядов и два набора сигналов. Одновременное отображение всех этих линий на графике может показаться перегруженным, но это позволяет сразу наглядно сравнить, как каждый фильтр преобразует ценовые данные.
//+------------------------------------------------------------------+ //| ALGLIB_FilterDemo.mq5 | //| Copyright 2026, Clemence Benjamin | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2026" #property link "https://www.mql5.com" #property version "1.00" #property indicator_chart_window #property indicator_buffers 10 #property indicator_plots 10 //--- include ALGLIB #include <Math/Alglib/alglib.mqh>
Значение indicator_buffers и indicator_plots, равное 10, означает, что у нас будет десять линий или маркеров на графике. Это много, но мы хотим видеть все сразу для сравнения.
Шаг 2: Определение свойств построений
Для каждого построения нужно задать метку, тип, цвет и стиль. Мы будем использовать линии для непрерывных рядов и стрелки для сигналов пересечения. Метки будут отображаться в DataWindow и помогут идентифицировать каждую линию. Присваивая характерные цвета — черный для цены закрытия, синий/красный для базовой EMA, зеленый для SMA-фильтра, яркий оттенок лазурного цвета (dodger blue) для EMA-фильтра, оранжевый для LRMA-фильтра, пурпурный для тренда SSA и коричневый для производной, умноженной на коэффициент масштабирования сплайна — мы делаем график читаемым. Построения 9 и 10 отображаются стрелками; они появляются только там, где происходит пересечение, отмечая потенциальные точки покупки (ниже бара) и продажи (выше бара). Код стрелки и пустое значение будут установлены позже в OnInit.
//--- plot definitions #property indicator_label1 "Close" #property indicator_type1 DRAW_LINE #property indicator_color1 clrBlack #property indicator_width1 1 #property indicator_label2 "Baseline Fast EMA" #property indicator_type2 DRAW_LINE #property indicator_color2 clrBlue #property indicator_width2 1 #property indicator_label3 "Baseline Slow EMA" #property indicator_type3 DRAW_LINE #property indicator_color3 clrRed #property indicator_width3 1 #property indicator_label4 "SMA Filtered" #property indicator_type4 DRAW_LINE #property indicator_color4 clrGreen #property indicator_width4 1 #property indicator_label5 "EMA Filtered" #property indicator_type5 DRAW_LINE #property indicator_color5 clrDodgerBlue #property indicator_width5 1 #property indicator_label6 "LRMA Filtered" #property indicator_type6 DRAW_LINE #property indicator_color6 clrOrange #property indicator_width6 1 #property indicator_label7 "SSA Trend" #property indicator_type7 DRAW_LINE #property indicator_color7 clrMagenta #property indicator_width7 1 #property indicator_label8 "Spline Derivative (scaled)" #property indicator_type8 DRAW_LINE #property indicator_color8 clrBrown #property indicator_width8 1 #property indicator_label9 "Baseline Crossover Signals" #property indicator_type9 DRAW_ARROW #property indicator_color9 clrBlue #property indicator_width9 1 #property indicator_label10 "Filtered Crossover Signals" #property indicator_type10 DRAW_ARROW #property indicator_color10 clrRed #property indicator_width10 1
Обратите внимание, что построения 9 и 10 отображаются стрелками. Мы будем использовать их для обозначения сигналов на покупку и продажу.
Шаг 3: Входные параметры
Мы хотим, чтобы пользователи могли настраивать все ключевые параметры. Определим их:
//--- input parameters input int FastMAPeriod = 5; // Fast MA period input int SlowMAPeriod = 20; // Slow MA period input int FilterWindow = 14; // Window for SMA/LRMA and EMA alpha (alpha=2/(period+1)) input int SSAWindow = 30; // Window for SSA (must be < data length) input int SSARank = 5; // Number of SSA components to keep (rank) input double SplineDerivScale= 10.0; // Scaling factor for spline derivative (for visibility) input int LookbackBars = 500; // Number of recent bars to process (speed optimization)
Параметр LookbackBars критически важен для производительности. Операции ALGLIB, особенно SSA, могут быть вычислительно затратными при обработке тысяч баров. Обрабатывая только самые последние бары (по умолчанию 500), мы сохраняем быстродействие индикатора, одновременно отображая достаточно большой объем истории. Каждый входной параметр управляет определенным аспектом индикатора: для пересечения базовой линии используются FastMAPeriod и SlowMAPeriod; FilterWindow определяет длину простых, экспоненциальных и линейных регрессионных скользящих средних, применяемых к цене; SSAWindow устанавливает длину окна для матрицы траекторий в сингулярном спектральном анализе, а SSARank выбирает, сколько наибольших собственных значений сохраняется для восстановления тренда; SplineDerivScale просто умножает производную, чтобы она стала видимой при построении графика; а LookbackBars ограничивает количество баров, передаваемых в процедуры ALGLIB.
Шаг 4: Объявления буферов
Нам нужно десять буферов для хранения наших данных:
//--- indicator buffers double closeBuffer[]; double baselineFastBuffer[]; double baselineSlowBuffer[]; double smaFilteredBuffer[]; double emaFilteredBuffer[]; double lrmaFilteredBuffer[]; double ssaTrendBuffer[]; double splineDerivBuffer[]; double baselineSignalBuffer[]; double filteredSignalBuffer[];
Каждый буфер соответствует одному из графиков, определенных ранее. Буфер closeBuffer содержит исходные цены закрытия; baselineFastBuffer и baselineSlowBuffer будут заполнены встроенной функцией iMA; smaFilteredBuffer, emaFilteredBuffer и lrmaFilteredBuffer будут содержать результаты применения соответствующих фильтров ALGLIB; ssaTrendBuffer будет хранить тренд, реконструированный с помощью SSA; splineDerivBuffer будет хранить цену плюс производную, умноженную на коэффициент масштабирования; а два буфера сигналов будут содержать позиции стрелок для пересечений. Все буферы объявляются как динамические массивы; их размеры будут управляться терминалом.
Шаг 5: Хэндлы для встроенных скользящих средних
Для базового пересечения мы будем использовать встроенную функцию iMA MQL5. Она уже оптимизирована и служит нам эталоном:
//--- handles for built-in iMA (for baseline) int fastMAHandle, slowMAHandle;
Использование хэндлов iMA эффективно, поскольку терминал вычисляет скользящие средние внутри себя и кэширует результаты. Эти хэндлы будут созданы в OnInit и освобождены в OnDeinit.
Шаг 6: Функция инициализации
В OnInit() мы настраиваем буферы, определяем стили стрелок и создаём хэндлы MA:
//+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- set index buffers SetIndexBuffer(0, closeBuffer, INDICATOR_DATA); SetIndexBuffer(1, baselineFastBuffer, INDICATOR_DATA); SetIndexBuffer(2, baselineSlowBuffer, INDICATOR_DATA); SetIndexBuffer(3, smaFilteredBuffer, INDICATOR_DATA); SetIndexBuffer(4, emaFilteredBuffer, INDICATOR_DATA); SetIndexBuffer(5, lrmaFilteredBuffer, INDICATOR_DATA); SetIndexBuffer(6, ssaTrendBuffer, INDICATOR_DATA); SetIndexBuffer(7, splineDerivBuffer, INDICATOR_DATA); SetIndexBuffer(8, baselineSignalBuffer, INDICATOR_DATA); SetIndexBuffer(9, filteredSignalBuffer, INDICATOR_DATA); //--- set arrow codes (159 = small circle) PlotIndexSetInteger(8, PLOT_ARROW, 159); PlotIndexSetInteger(9, PLOT_ARROW, 159); PlotIndexSetDouble(8, PLOT_EMPTY_VALUE, EMPTY_VALUE); PlotIndexSetDouble(9, PLOT_EMPTY_VALUE, EMPTY_VALUE); //--- indicator short name IndicatorSetString(INDICATOR_SHORTNAME, "ALGLIB Filter Demo (Optimized)"); //--- get handles for baseline MAs fastMAHandle = iMA(_Symbol, _Period, FastMAPeriod, 0, MODE_EMA, PRICE_CLOSE); slowMAHandle = iMA(_Symbol, _Period, SlowMAPeriod, 0, MODE_EMA, PRICE_CLOSE); if(fastMAHandle == INVALID_HANDLE || slowMAHandle == INVALID_HANDLE) return(INIT_FAILED); return(INIT_SUCCEEDED); }
Код стрелки 159 даёт нам небольшой круг. Мы устанавливаем пустые значения для буферов сигналов, чтобы они отображались только тогда, когда мы явно задаём значение. Присваивая INDICATOR_DATA каждому буферу, мы сообщаем терминалу, что эти массивы содержат значения для отображения. Хэндлы для базовых экспоненциальных скользящих средних создаются с использованием выбранных пользователем периодов, текущего символа и таймфрейма. Если любой из хэндлов не удалось создать, инициализация индикатора прерывается.
Шаг 7: Деинициализация
Всегда освобождайте хэндлы после завершения:
//+------------------------------------------------------------------+ //| Custom indicator deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if(fastMAHandle != INVALID_HANDLE) IndicatorRelease(fastMAHandle); if(slowMAHandle != INVALID_HANDLE) IndicatorRelease(slowMAHandle); }
Освобождение хэндлов — это хорошая практика для освобождения ресурсов, выделенных терминалом. Хотя терминал делает это автоматически при удалении индикатора, явное освобождение делает код более надёжным и дисциплинированным.
Шаг 8: Вспомогательные функции для фильтров
Чтобы наш код оставался чистым, мы создадим вспомогательные функции, которые инкапсулируют преобразование между векторами MQL5 и CRowDouble из ALGLIB:
//+------------------------------------------------------------------+ //| Helper to convert vector to CRowDouble and back after filter | //+------------------------------------------------------------------+ void ApplySMA(vector<double> &data, int window) { CRowDouble row(data); CAlglib::FilterSMA(row, (int)data.Size(), window); data = row.ToVector(); } void ApplyEMA(vector<double> &data, int window) { double alpha = 2.0 / (window + 1.0); CRowDouble row(data); CAlglib::FilterEMA(row, (int)data.Size(), alpha); data = row.ToVector(); } void ApplyLRMA(vector<double> &data, int window) { CRowDouble row(data); CAlglib::FilterLRMA(row, (int)data.Size(), window); data = row.ToVector(); }
Эти функции берут вектор по ссылке, преобразуют его в CRowDouble, применяют фильтр и преобразуют обратно. Такой подход необходим, поскольку ALGLIB работает с объектами CRowDouble, но нам нужно удобство использования типа вектора в MQL5. Функции изменяют исходный вектор на месте, что позволяет избежать ненужного копирования и делает основной код более читаемым. Обратите внимание, что для фильтра EMA требуется константа сглаживания альфа, которая вычисляется из длины периода по стандартной формуле альфа = 2/(период+1).
Шаг 9: Основной цикл вычислений
Теперь перейдем к сути нашего индикатора — функции OnCalculate. Именно здесь происходит вся магия ALGLIB. Я разберу ее по разделам.
Определение диапазона обработки
Сначала мы решаем, какие бары обрабатывать на основе LookbackBars:
//+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- Determine the range of bars we need to process int startIdx = (rates_total > LookbackBars) ? rates_total - LookbackBars : 0; int len = rates_total - startIdx;
Эта компактная строка вычисляет начальный индекс. Если у нас больше баров, чем LookbackBars, мы начинаем LookbackBars с конца; в противном случае мы начинаем с бара 0. Это гарантирует, что в подпрограммы ALGLIB будет передаваться только ограниченное и разумное количество баров. Длина len — это количество баров, которые мы фактически обработаем с помощью фильтров.
Валидация данных
Нам необходимо убедиться, что у нас достаточно баров на самый длительный период, который мы будем использовать:
//--- Need enough data for the longest period int maxPeriod = MathMax(SlowMAPeriod, MathMax(FilterWindow, SSAWindow)); if(len < maxPeriod) { // Not enough bars in the lookback window – fill with EMPTY and return for(int i = startIdx; i < rates_total; i++) { smaFilteredBuffer[i] = EMPTY_VALUE; emaFilteredBuffer[i] = EMPTY_VALUE; lrmaFilteredBuffer[i] = EMPTY_VALUE; ssaTrendBuffer[i] = EMPTY_VALUE; splineDerivBuffer[i] = EMPTY_VALUE; } ArrayInitialize(baselineSignalBuffer, EMPTY_VALUE); ArrayInitialize(filteredSignalBuffer, EMPTY_VALUE); return(rates_total); }
Если окно ретроспективного анализа слишком короткое, мы заполняем все пустыми значениями и возвращаемся. Это предотвращает отображение индикатором неполных или некорректных данных. Параметр maxPeriod выбирает наибольший из трех ключевых периодов (медленная скользящая средняя, окно фильтра, окно SSA), чтобы гарантировать наличие достаточного количества данных для всех фильтров и получения осмысленного результата.
Базовое копирование данных
Мы копируем цены закрытия в наш первый буфер (для отображения) и получаем базовые скользящие средние:
//--- Copy close prices to buffer 0 (for plotting) for(int i = 0; i < rates_total; i++) closeBuffer[i] = close[i]; //--- Get baseline MAs (they already cover all bars) if(CopyBuffer(fastMAHandle, 0, 0, rates_total, baselineFastBuffer) <= 0) return(0); if(CopyBuffer(slowMAHandle, 0, 0, rates_total, baselineSlowBuffer) <= 0) return(0);
Цены закрытия просто копируются в первый буфер. Два буфера базовых EMA заполняются путем копирования из хэндлов iMA. Поскольку эти хэндлы содержат значения для каждого бара, мы копируем весь диапазон. Если копирование не удается (возвращает <= 0), мы выходим досрочно.
Подготовка вектора данных
Мы создаем вектор, содержащий только цены, которые будем обрабатывать:
//--- Create a vector of close prices for the lookback window vector<double> vecClose(len); for(int i = 0; i < len; i++) vecClose[i] = close[startIdx + i];
Этот вектор, vecClose, содержит данные о ценах, которые будут переданы во все фильтры ALGLIB. Работая с непрерывным сегментом массива цен, мы избегаем передачи огромных массивов и снижаем нагрузку на память и вычислительные затраты.
Применение базовых фильтров
Теперь мы применяем каждый фильтр, используя наши вспомогательные функции:
//--- 1) SMA Filter vector<double> vecSMA = vecClose; ApplySMA(vecSMA, FilterWindow); for(int i = 0; i < len; i++) smaFilteredBuffer[startIdx + i] = vecSMA[i]; //--- 2) EMA Filter vector<double> vecEMA = vecClose; ApplyEMA(vecEMA, FilterWindow); for(int i = 0; i < len; i++) emaFilteredBuffer[startIdx + i] = vecEMA[i]; //--- 3) LRMA Filter vector<double> vecLRMA = vecClose; ApplyLRMA(vecLRMA, FilterWindow); for(int i = 0; i < len; i++) lrmaFilteredBuffer[startIdx + i] = vecLRMA[i];
Обратите внимание, как каждый фильтр изменяет свою собственную копию данных. Исходный vecClose остается неизменным для последующих операций. Отфильтрованные результаты затем помещаются в соответствующие буферы по корректным индексам (startIdx + i). Простая скользящая средняя (SMA) присваивает одинаковый вес всем точкам в окне; экспоненциальная скользящая средняя (EMA) присваивает больший вес недавним ценам; а скользящая средняя на основе линейной регрессии (LRMA) строит прямую линию по данным и использует конечную точку в качестве отфильтрованного значения. Эти три фильтра являются классическими и служат базовым уровнем для сравнения с более сложными методами SSA и сплайн-интерполяции.
Сингулярный спектральный анализ
Это наиболее продвинутая часть. Мы создаем модель SSA, добавляем свои данные, настраиваем ее и извлекаем тренд:
//--- 4) SSA Trend CSSAModel ssa; CAlglib::SSACreate(ssa); CRowDouble priceRow(vecClose); // convert once CAlglib::SSAAddSequence(ssa, priceRow); CAlglib::SSASetAlgoTopKRealtime(ssa, SSARank); CAlglib::SSASetWindow(ssa, SSAWindow); CRowDouble trend, noise; CAlglib::SSAAnalyzeLast(ssa, len, trend, noise); if(trend.Size() == len) { vector<double> vecTrend = trend.ToVector(); for(int i = 0; i < len; i++) ssaTrendBuffer[startIdx + i] = vecTrend[i]; } else { for(int i = 0; i < len; i++) ssaTrendBuffer[startIdx + i] = EMPTY_VALUE; }
Ключевая строка — SSASetAlgoTopKRealtime(ssa, SSARank). Она указывает SSA использовать верхние компоненты SSARank (наибольшие собственные значения) для реконструкции. Сохраняя только самые большие компоненты, мы, по сути, отфильтровываем шум. Размер SSAWindow определяет, сколько прошлых точек учитывается при построении матрицы траекторий — больший период позволяет зафиксировать долгосрочные паттерны, но требует больше данных. После анализа мы проверяем, имеет ли тренд ожидаемую длину. Если SSA не срабатывает (что может произойти из-за недостатка данных или численных проблем), мы заполняем соответствующий буфер пустыми значениями. Полученная линия тренда должна быть более плавной, чем любая из простых скользящих средних, и может выявить скрытые циклические компоненты.
Вычисление производной сплайна
Для производной сплайна нам нужна ось x, представляющая индексы баров:
//--- 5) Spline derivative // X axis = bar index within the lookback window (0..len-1) vector<double> xVec(len); for(int i = 0; i < len; i++) xVec[i] = i; vector<double> priceVec = priceRow.ToVector(); // already from vecClose double xArr[], priceArr[]; ArrayResize(xArr, len); ArrayResize(priceArr, len); for(int i = 0; i < len; i++) { xArr[i] = xVec[i]; priceArr[i] = priceVec[i]; } CSpline1DInterpolantShell spline; CAlglib::Spline1DBuildCubic(xArr, priceArr, len, 0, 0.0, 0, 0.0, spline);
Мы преобразуем наши векторы в обычные массивы, поскольку Spline1DBuildCubic ожидает параметры типа double. Параметры граничных условий (0, 0.0, 0, 0.0) задают натуральный кубический сплайн с нулевыми вторыми производными на концах — стандартный выбор, который хорошо работает для большинства ценовых рядов. Затем интерполянт сплайна сохраняется в spline.
Теперь мы вычисляем производную в каждой точке:
for(int i = 0; i < len; i++) { double val, deriv, deriv2; CAlglib::Spline1DDiff(spline, i, val, deriv, deriv2); splineDerivBuffer[startIdx + i] = close[startIdx + i] + deriv * SplineDerivScale; }
Мы добавляем производную, умноженную на коэффициент масштабирования, к фактической цене, чтобы линия отображалась вблизи уровня цены. Без этого сдвига производная была бы близка к нулю, и её было бы трудно увидеть. Производная показывает скорость изменения сглаженной кривой; положительные значения указывают на восходящий импульс, а отрицательные — на нисходящий. Коэффициент масштабирования делает эти колебания видимыми в виде линии, колеблющейся вокруг цены.
Обнаружение сигналов пересечения
Теперь мы генерируем наши торговые сигналы. Сначала мы инициализируем буферы сигналов пустыми значениями:
//--- 6) Crossover signals (only within lookback window) ArrayInitialize(baselineSignalBuffer, EMPTY_VALUE); ArrayInitialize(filteredSignalBuffer, EMPTY_VALUE);
Для отфильтрованного пересечения нам нужны быстрые и медленные экспоненциальные скользящие средние отфильтрованной по EMA серии (vecEMA). Мы вычислим их рекурсивно в векторы:
double alphaFast = 2.0 / (FastMAPeriod + 1.0); double alphaSlow = 2.0 / (SlowMAPeriod + 1.0); vector<double> fastFilteredVec(len); vector<double> slowFilteredVec(len); if(len > 0) { fastFilteredVec[0] = vecEMA[0]; slowFilteredVec[0] = vecEMA[0]; } for(int i = 1; i < len; i++) { fastFilteredVec[i] = alphaFast * vecEMA[i] + (1 - alphaFast) * fastFilteredVec[i-1]; slowFilteredVec[i] = alphaSlow * vecEMA[i] + (1 - alphaSlow) * slowFilteredVec[i-1]; }
Эта рекурсивная формула является стандартным расчетом EMA. Предварительно вычисляя весь ряд, мы избегаем пересчета на каждом тике и делаем код чище. Обратите внимание, что мы используем те же быстрые и медленные периоды, что и базовый, но применяем их к отфильтрованной по EMA цене (vecEMA), а не к исходной цене закрытия. Это позволит получить сигнал пересечения, который потенциально менее зашумлен, поскольку базовый ряд уже сглажен.
Теперь мы будем искать точки пересечения:
for(int i = 1; i < len; i++) { int idx = startIdx + i; // Baseline crossover signals (using the built‑in MA buffers) if(baselineFastBuffer[idx] > baselineSlowBuffer[idx] && baselineFastBuffer[idx-1] <= baselineSlowBuffer[idx-1]) baselineSignalBuffer[idx] = low[idx] - 10 * _Point; // buy arrow below price if(baselineFastBuffer[idx] < baselineSlowBuffer[idx] && baselineFastBuffer[idx-1] >= baselineSlowBuffer[idx-1]) baselineSignalBuffer[idx] = high[idx] + 10 * _Point; // sell arrow above price // Filtered crossover signals if(fastFilteredVec[i] > slowFilteredVec[i] && fastFilteredVec[i-1] <= slowFilteredVec[i-1]) filteredSignalBuffer[idx] = low[idx] - 20 * _Point; // buy if(fastFilteredVec[i] < slowFilteredVec[i] && fastFilteredVec[i-1] >= slowFilteredVec[i-1]) filteredSignalBuffer[idx] = high[idx] + 20 * _Point; // sell }
Для сигналов на покупку мы размещаем стрелку ниже минимума бара; для сигналов на продажу - выше максимума. Отфильтрованные стрелки размещаются на расстоянии 20 пунктов (против 10 для базовой линии), чтобы они были визуально различимы. Условие проверяет пересечение, сравнивая текущие и предыдущие значения быстрой и медленной линий. Эта стандартная логика генерирует сигнал точно в тот момент, когда быстрая линия пересекает медленную линию.
Очистка
Наконец, мы заполняем любые бары до окна ретроспективного анализа пустыми значениями для отфильтрованных линий:
//--- Fill any bars before the lookback window with EMPTY_VALUE for the filtered lines for(int i = 0; i < startIdx; i++) { smaFilteredBuffer[i] = EMPTY_VALUE; emaFilteredBuffer[i] = EMPTY_VALUE; lrmaFilteredBuffer[i] = EMPTY_VALUE; ssaTrendBuffer[i] = EMPTY_VALUE; splineDerivBuffer[i] = EMPTY_VALUE; } //--- return value of prev_calculated for next call return(rates_total); }
И это всё! Наш индикатор готов.
После компиляции в MetaEditor индикатор загружается без ошибок. Применительно к USDJPY M15 с параметрами по умолчанию инициализация происходит эффективно благодаря оптимизации LookbackBars. На графике вы увидите исходную цену закрытия (чёрная), две базовые EMA (синяя и красная) и пять отфильтрованных линий (зелёная, лазурная, оранжевая, пурпурная и коричневая). Синие и красные стрелки отмечают сигналы пересечения базовых EMA, а красные стрелки (расположенные дальше) — сигналы пересечения отфильтрованных EMA. Сравнивая два набора сигналов, вы можете судить, помогает ли дополнительное сглаживание уменьшить избыточные сигналы или вносит слишком большой лаг.
Реализация включает:
- Буферы пересечений базовой EMA — стандартные быстрая и медленная EMA, рассчитываемые терминалом, обеспечивающие привычный ориентир.
- Буферы отфильтрованных серий (SMA, EMA, LRMA) — три классические скользящие средние, применяемые через ALGLIB, показывающие, как каждая из них сглаживает цену.
- Метод реконструкции тренда SSA — современный метод, адаптирующийся к данным, разлагающий ряд на тренд и шум, выявляя скрытые паттерны.
- Вычисление производной сплайна — непрерывная мера скорости изменения сглаженной цены, масштабированная для наглядности.
- Логика визуализации сигнала — два набора сигналов пересечения (базовый и отфильтрованный), отображаемые стрелками, что позволяет напрямую сравнивать время появления и частоту сделок.
Оптимизация производительности направлена на минимизацию перерасчетов и ограничение вычислений SSA и сплайнов последними барами. Параметр LookbackBars позволяет выбирать между скоростью и исторической глубиной. С помощью этих компонентов можно экспериментировать с различными параметрами фильтра и даже заменять отфильтрованное EMA пересечение любым другим отфильтрованным рядом, чтобы увидеть, как они влияют на генерацию сигналов.
Тестирование
Теперь, когда наш индикатор успешно скомпилирован, пришло время увидеть его в действии. Если вы новичок в этом процессе, не волнуйтесь — мы вместе пройдем каждый шаг. Цель здесь не в оценке прибыльности (помните, это визуальный инструмент, а не торговый робот). Вместо этого мы хотим понаблюдать, как математическая предварительная обработка изменяет поведение пересечений скользящих средних — особенно на боковом рынке.
Шаг 1: Добавление индикатора на график
После компиляции кода в MetaEditor индикатор ALGLIB_FilterDemo.ex5 появится в вашем окне Navigator (View → Navigator или Ctrl+N) в папке "Indicators". Если вы его не видите, щелкните правой кнопкой мыши в Navigator и выберите "Refresh".
Чтобы применить его к графику:
- Откройте новый график — в качестве примера мы будем использовать USDJPY, M15 (15-минутный).
- Перетащите индикатор из Navigator на график или дважды щелкните по нему.
- Появится окно параметров ввода индикатора. Пока оставьте все значения по умолчанию:
- Период быстрой скользящей средней: 5
- Период медленной скользящей средней: 20
- Окно фильтра: 14
- Окно SSA: 30
- Ранг SSA: 5
- Масштаб производной сплайна: 10.0
- Бары ретроспективного анализа: 500
- Нажмите OK.
Индикатор немедленно нарисует несколько линий на вашем графике. Не беспокойтесь, если поначалу он выглядит перегруженным — каждая линия имеет свое назначение, и мы сейчас их идентифицируем.
Шаг 2: Наблюдение за индикатором в действии (график в реальном времени)
Уделите немного времени изучению того, что появляется на вашем графике. Вы должны видеть:
- Черная линия: исходная цена закрытия (ваша контрольная отметка).
- Синяя и красная линии: базовые быстрые (5) и медленные (20) экспоненциальные скользящие средние — классическое пересечение, которое мы все знаем.
- Зеленая линия: цена, отфильтрованная с помощью простой скользящей средней (SMA) с использованием ALGLIB.
- Линия лазурного цвета: цена, отфильтрованная экспоненциальной скользящей средней (EMA) с использованием ALGLIB.
- Оранжевая линия: цена, отфильтрованная скользящей средней на основе линейной регрессии (LRMA) с использованием ALGLIB.
- Пурпурная линия: компонент тренда, извлеченный с помощью сингулярного спектрального анализа (SSA).
- Коричневая линия: производная сплайна — показатель скорости изменения сглаженной цены, масштабированный и добавленный обратно к цене, чтобы он был виден на графике.
- Синие стрелки: сигналы пересечения базовых экспоненциальных скользящих средних (расположены ниже баров сигналов на покупку, выше баров сигналов на продажу).
- Красные стрелки: сигналы пересечения отфильтрованных экспоненциальных скользящих средних (расположены немного дальше, чтобы быть отчетливыми).
Теперь посмотрите на боковой участок графика — цена движется горизонтально без четкого тренда. Смотрите, что происходит:
- Базовые экспоненциальные скользящие средние (синие и красные) часто пересекаются. Каждое пересечение генерирует синюю стрелку. На рынке с боковым движением эти стрелки часто появляются несколько раз подряд. Это проблема "избыточных сигналов", которую мы обсуждали ранее.
- Теперь посмотрим на отфильтрованные линии. Обратите внимание, как зеленая (SMA), лазурная (EMA) и оранжевая (LRMA) линии более сглажены, чем исходная цена. Они не реагируют на каждое незначительное колебание.
- Обратите особое внимание на красные стрелки (отфильтрованные пересечения). В том же боковом участке, посчитайте их. Вероятно, красных стрелок будет меньше, чем синих. Предварительная обработка отфильтровала многие ложные сигналы.
- Пурпурная линия SSA часто является самой плавной из всех — она может почти не двигаться в боковых областях, давая очень мало сигналов пересечения.
- Коричневая линия производной сплайна колеблется вокруг цены. Когда она пересекает линию снизу или сверху, это сигнализирует об изменении импульса. Вы можете заметить, что она реагирует еще до фактического пересечения — это означает, что производная обнаруживает изменения наклона на ранней стадии.

Рис. 1. График USDJPY M15 в реальном времени
Шаг 3: Тестирование в тестере стратегий (визуальный режим)
График в реальном времени показывает нам недавнее поведение, но что, если мы хотим увидеть, как индикатор работает за более длительный исторический период — скажем, за несколько месяцев данных? Вот тут-то и пригодится тестер стратегий, даже если мы не тестируем советник. Мы можем запустить его в тестере в визуальном режиме, чтобы наблюдать за его сигналами в различных рыночных условиях.
Вот как это настроить:
- Откройте тестер стратегий (View → Strategy Tester или Ctrl+R).
- В окне тестера:
- Советник: оставьте поле пустым (мы не запускаем советник).
- Символ: USDJPY (или любая пара, которую вы предпочитаете).
- Период: M15.
- Модель: выберите "Every tick" или "1 minute OHLC" (оба варианта подходят для визуального наблюдения).
- Диапазон дат: выберите период, включающий как трендовые, так и боковые рынки — например, последние 6-12 месяцев.
- Визуальный режим: отметьте галочкой эту ячейку. Это важно — это позволит нам наблюдать за графиком индикатора в реальном времени во время работы тестера.
- Нажмите "Start". Тестер начнет работу, и откроется окно графика, показывающее применение индикатора тик за тиком или бар за баром.
Во время работы тестера наблюдайте:
- в периоды сильного тренда стрелки базовой линии (синие) и стрелки фильтрации (красные) будут отображаться в одном направлении. Отфильтрованные стрелки могут появляться немного позже (лаг — цена сглаживания), но в целом они будут совпадать с направлением тренда.
- В боковых или рваных участках рынка следите за разницей. Синие стрелки будут повторяться снова и снова — вверх, вниз, вверх, вниз — по мере того, как быстрая EMA будет отслеживать каждое колебание. Красные стрелки, однако, будут появляться гораздо реже. На некоторых боковых участках красные стрелки могут вообще отсутствовать. Это и есть работа фильтра.
- Обратите внимание на поведение линии SSA (пурпурная). Во flat-участках она почти горизонтальна и в значительной степени игнорирует рыночный шум.
- Производная сплайна (коричневая) может проявлять активность и во время бокового движения рынка. Тем не менее, поскольку мы умножили её на коэффициент масштабирования и добавили к цене, её колебания видны как небольшие колебания вокруг ценового уровня. Вы можете поэкспериментировать с параметром SplineDerivScale, чтобы сделать их более или менее выраженными.

Рис. 2. Визуальный режим тестера стратегий
Шаг 4: Игра с параметрами
Один из лучших способов обучения — это настраивать входные параметры и сразу же видеть эффект. Удалите индикатор с графика (щелкните правой кнопкой мыши → Indicators → выберите → Delete), затем перетащите его обратно и изменяйте по одному параметру за раз. Наблюдайте, что происходит:
- Увеличьте FilterWindow (например, с 14 до 30): все отфильтрованные линии (зеленая, лазурная, оранжевая) становятся более плавными. Они медленнее реагируют на изменения цены. Сигналы пересечения (красные стрелки) будут появляться реже, но с большим лагом. Это классический компромисс: сглаживание против отзывчивости.
- Уменьшите значение FilterWindow (например, до 5): отфильтрованные линии теперь будут более точно соответствовать исходной цене. Вы увидите больше красных стрелок — некоторые из них могут даже появиться почти одновременно с синими стрелками. Эффект фильтрации уменьшится.
- Отрегулируйте значение SSAWindow (попробуйте 20, затем 50): плавность пурпурной линии SSA изменится. Меньший размер окна делает график более отзывчивым (но все же более плавным, чем в случае с исходной ценой); больший размер окна создает очень медленно движущуюся линию, которая фиксирует только самые долгосрочные движения. Если сделать окно слишком большим, линия может исчезнуть в левой части графика — для вычислений SSA требуется достаточно данных.
- Измените SSARank (попробуйте 3, затем 8): это определяет, сколько компонентов сохраняется при реконструкции. Более низкий ранг = более плавное движение (более агрессивная фильтрация). Более высокий ранг = больше сохраненных деталей (ближе к исходной цене). Значения от 4 до 7 часто хорошо работают для данных форекс на M15.
- Измените SplineDerivScale: сделайте его больше (например, 20), и колебания коричневой линии станут более резкими — их будет легче увидеть, но они могут сильно отклоняться от цены. Сделайте его меньше (например, 2), и линия будет плотно прилегать к цене, что затруднит обнаружение пересечений производной через ноль. Найдите значение, которое делает колебания видимыми, не отвлекая внимания.
В ходе этих практических экспериментов вы разовьете интуитивное понимание того, как каждый математический метод влияет на данные. Вы больше не просто читаете об ALGLIB — вы видите, как он работает в реальном времени на ваших собственных графиках.
Заключение
Теперь, когда мы увидели индикатор в действии — как на реальных графиках, так и на исторических данных в тестере стратегий — давайте подведем итоги того, чего мы достигли вместе.
Теперь у нас есть работающий индикатор, позволяющий нам наглядно сравнивать пересечения отфильтрованных скользящих средних бок о бок. На своих графиках мы четко видим, как ведут себя базовые EMA по сравнению с их отфильтрованными аналогами. Отфильтрованные пересечения появляются реже во время бокового движения рынка — именно то, на что мы надеялись с помощью математической предварительной обработки.
Мы узнали, что можем экспериментировать с параметрами, чтобы выяснить, как ведет себя каждый фильтр. Регулируя FilterWindow, SSAWindow, SSARank и SplineDerivScale, мы можем сразу увидеть влияние на плавность, лаг и частоту сигналов. Такое практическое изучение развивает интуицию гораздо лучше, чем простое чтение теории.
Мы увидели конкретные примеры применения математики в торговле. Фильтры SMA, EMA и LRMA продемонстрировали знакомые концепции сглаживания. Сингулярный спектральный анализ показал нам, как адаптивное разложение данных может выявлять скрытые тренды. Производная сплайна показала, как скорость изменения может быть визуализирована непосредственно на ценовом графике. Ничто из этого не требовало от нас математических знаний — ALGLIB выполнял сложные вычисления, а мы сосредоточились на интерпретации.
На основе простого пересечения скользящих средних мы создали многофильтровую аналитическую платформу. Базовая стратегия не изменилась — это по-прежнему пересечение двух скользящих средних — но данные, поступающие в нее, были преобразованы. Это основная идея, которую мы исследовали вместе: предварительная обработка имеет значение. Одна и та же торговая логика, применяемая к данным, отфильтрованным по-разному, дает разные сигналы, и теперь у нас есть инструмент для непосредственного изучения этих различий.
Благодаря модульной структуре кода мы можем:
- Заменить отфильтрованное по EMA пересечение пересечениями из любой другой отфильтрованной серии (отфильтрованной по SMA, отфильтрованной по LRMA, тренда SSA и т. д.) и сравнить их.
- Извлечь отфильтрованные сигналы для генерации оповещений или уведомлений при возникновении пересечений.
- Использовать продемонстрированные здесь методы предварительной обработки в качестве основы для советника — взяв нашу логику отфильтрованных пересечений и автоматизировав ее для бэктестинга и форвард-тестирования.
- Экспериментировать с совершенно разными функциями ALGLIB — процедурами оптимизации, алгоритмами кластеризации или расширенными статистическими тестами — и применять их к нашим торговым идеям.
В этой статье мы обнаружили, что ALGLIB превращает MQL5 из платформы, ограниченной встроенными индикаторами, в настоящую среду для численных исследований. Будь мы трейдеры, ищущие более чистые сигналы, разработчики, создающие аналитические инструменты, или просто интересуемся прикладной математикой, ALGLIB открывает двери, которые ранее были для нас закрыты.
Загрузите индикатор из прикрепленного файла ниже. Примените его к вашим любимым парам и таймфреймам. Покрутите параметры. Пробуйте, ломайте, исправляйте, улучшайте. Пусть визуальная обратная связь поможет вам понять. А когда будете готовы, сделайте следующий шаг — создайте робота, запустите оптимизацию, изучите новый фильтр.
Я буду рад услышать ваши наблюдения, вопросы и эксперименты в обсуждении. Код доступен для использования, изменения и обучения. Ваш вклад помогает совершенствовать эти идеи и укреплять наше сообщество.
Удачного программирования всем!
Вложения
| Имя исходного файла | Тип | Версия | Описание |
|---|---|---|---|
| ALGLIB_FilterDemo | Индикатор | 1.00 | Демонстрационный индикатор, показывающий продвинутые методы фильтрации тренда с использованием стандартной библиотеки ALGLIB в MetaTrader 5. Он сравнивает традиционные пересечения EMA с сигналами на основе отфильтрованных рядов, включая LRMA, а также содержит визуализацию наклона с помощью производной кубического сплайна для улучшения обнаружения тренда и уменьшения ложных сигналов на рынках с боковым движением. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21279
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Возможности Мастера MQL5, которые вам нужно знать (Часть 90): Управление капиталом на основе дерева Фенвика и одномерной CNN в MQL5
Неопределённость как модель (Часть 8): Обобщение МНК
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования