Конструирование признаков для машинного обучения (Часть 12): Фрактальные признаки в MQL5
Содержание
- Введение
- Что означает задержка подтверждения внутри функции OnCalculate
- Архитектура класса и контракт доступа через iCustom
- Окно обнаружения и измерение силы
- Поддержка и сопротивление: кольцевой буфер событий, а не баров
- Пробои, трендовый блок и сигналы
- Индикатор: две группы буферов с разными правилами
- Три дефекта, обнаруженных при переносе
- Результаты: вывод признаков и численное соответствие эталонной реализации на Python
- Чтение признаков из советника (EA)
- Заключение
- Список литературы
- Прикрепленные файлы
Введение
В части 11 был проведён аудит модуля фракталов на Python и обнаружена утечка будущих данных: детектор использовал rolling(2n+1, center=True), поэтому значения в столбцах фракталов, силы, валидности и уровней для бара c можно было вычислить лишь после закрытия бара c+n. Исправление свелось к добавлению одного флага leak_safe в интерфейс функции get_fractal_features(): он сдвигает каждый столбец, рассчитанный по центрированному окну, вперёд на n баров и сохраняет доступ к несдвинутым столбцам для единственного случая, когда использование будущих данных допустимо, — формирования целевых меток.
В Python это исправление сводится к одному вызову метода Series.shift. В MQL5 нет операции сдвига, поскольку индикаторы записывают данные в буферы, относящиеся к отдельным барам, а не в столбцы. Ключевые вопросы: (1) на какой бар записывается результат обнаружения и (2) когда разрешена запись. Это два разных вопроса, и ответ только на первый из них вновь приводит к утечке будущих данных в перенесённой реализации, которая в остальном в точности воспроизводит численные результаты Python.
В этой статье исправленный набор признаков реализован в виде CFractalFeatures.mqh — событийно-ориентированного механизма обработки, который принимает по одному бару за раз и публикует восемнадцать признаков без использования данных из будущего относительно бара, на котором они записаны. Также представлены FractalViewer.mq5, который отделяет графические маркеры от буферов признаков, поскольку для них действуют разные правила относительно момента записи, и FractalValidation.mq5, который экспортирует выходные данные механизма обработки для численного сравнения с эталонной реализацией на Python. В ходе портирования выявились три дефекта; каждый из них задокументирован с указанием измерения, в ходе которого он был обнаружен.
Что означает задержка подтверждения внутри функции OnCalculate
Фрактал Вильямса — это локальный экстремум в окне из 2n+1 баров, причём бар-кандидат находится в центре (Williams, 1998). Паттерн симметричен: перед баром-кандидатом должно быть n баров, а после него — n баров. Проблема заключается в барах после бара-кандидата. В момент закрытия бара c окно [c-n, c+n] ещё не сформировано полностью, и оценить паттерн можно не раньше закрытия бара c+n.
Отсюда следуют два обоснованных подхода и один необоснованный. Графическую пометку можно поставить на баре c, поскольку маркер указывает, где возник экстремум, и каждый трейдер, следящий за графиком в реальном времени, знает, что маркер появился на n баров позже. В буфер признаков нельзя записывать значение на баре c, поскольку программа, считывающая buffer[c], никак не может узнать, что значение появилось с опозданием. Необоснован подход, при котором обе записи выполняются в один и тот же буфер.

Рисунок 1. Иллюстрация из одной панели, показывающая два подхода к публикации центрированного фрактала
- Вверху: окно из пяти баров при n=2, в котором локальный максимум находится на центральном баре.
- Слева: наивно перенесённая реализация записывает результат обнаружения на баре c, поэтому программа, считывающая данные на баре c, видит значение, которого тогда ещё не существовало.
- Справа: перенесённая реализация без утечки будущих данных публикует тот же результат обнаружения на баре c+n, когда все бары, используемые для его расчёта, уже закрыты.
Механизм обработки повсеместно использует привязку к правой границе окна. Каждое значение, возвращаемое механизмом для бара t, относится к фракталу с центром на баре t-n, благодаря чему соглашение, используемое в Python, становится явным, а не подразумеваемым:
fractal_high[t] = 1 if high[t-n] == max(high[t-2n : t+1]) high_strength[t] = high[t-n] / mean(high[t-2n : t+1]) - 1 breakout_up[t] = close[t] > high[t-n] and fractal_low[t] == 1
Границы среза задаются по правилам Python, поэтому верхняя граница t+1 означает, что окно включает бар t. При такой записи окно заканчивается на баре t и никогда не выходит за его пределы. Те же три строки в модуле Python выполняют расчёт по центрированному скользящему окну с последующим сдвигом; это эквивалентная запись, но в ней задержка скрыта в аргументе pandas.
Архитектура классов и контракт доступа через iCustom
Механизму обработки необходимо сохранять состояние. Две группы признаков зависят от истории, которую невозможно восстановить по текущему окну: уровни поддержки и сопротивления зависят от последних lookback_period подтверждённых фракталов, которые могут находиться на сотни баров в прошлом, а трендовый блок — от последних ma_period флагов пробоя. Обе истории хранятся во внутренних кольцевых буферах, что позволяет вызывать ProcessBar для каждого бара без повторного просмотра истории на каждом тике.
class CFractalFeatures { private: int m_n; // half-window; pattern length is 2n+1 int m_lookback; // confirmed fractal events kept per side int m_ma_period; // trend average and breakout density double m_threshold; // strength floor, or volatility multiplier bool m_dynamic; // scale the floor by center-bar volatility //--- level state: the last m_lookback confirmed extremes per side double m_res_events[]; double m_sup_events[]; double m_resistance; double m_support; //--- breakout history ring, length m_ma_period double m_bo_up[]; double m_bo_down[]; int m_bo_cursor; int m_bo_filled; int m_last_bar; // last index passed to ProcessBar public: bool Init(const int n = 2, const int lookback_period = 20, const int ma_period = 20, const double threshold = 0.001, const bool dynamic_threshold = false); void Reset(void); bool ProcessBar(const double &high[], const double &low[], const double &close[], const double &volatility[], const int t, FractalRow &row); bool Compute(const double &high[], const double &low[], const double &close[], const double &volatility[], const int total, FractalBuffers &out);
ProcessBar — базовая операция; Compute сначала сбрасывает состояние, а затем вызывает её в цикле. Оба способа в силу своей структуры дают идентичные численные результаты. Это позволяет избежать типичной ситуации, при которой инкрементные обновления отклоняются от результатов полного пересчета после повторной синхронизации исторических данных.
Соглашение об использовании сторожевого значения соответствует части 8: недоступное значение записывается как FRAC_EMPTY, определённое как -1e38, а проверка допустимости выполняется с помощью FRAC_IS_VALID, а не сравнением на равенство, поэтому накопленная погрешность вычислений с плавающей точкой в скопированном буфере не может привести к тому, что сторожевое значение будет воспринято как данные.
Доступны восемнадцать признаков, каждый из которых можно получить через iCustom. Два из них — сопротивление и поддержка — также отображаются в виде линий; вместе с двумя стрелочными маркерами, которые служат только для отображения и не входят в число восемнадцати признаков, это даёт четыре буфера отображения на графике, а остальные шестнадцать являются расчётными буферами:
Буфер | Имя | Значение на баре t |
|---|---|---|
| 0, 1 | Стрелка верхнего/нижнего фрактала | Только для отображения; записывается на баре t-n |
| 2, 3 | Сопротивление, поддержка | Уровень по последним подтверждённым фракталам |
| 4, 5 | Верхний/нижний фрактал | Флаг обнаружения фрактала на баре t-n |
| 6, 7 | Сила верхнего/нижнего фрактала | Превышение экстремума над средним значением по окну |
| 8, 9 | Верхний/нижний фрактал, прошедший фильтр | Обнаруженный фрактал, прошедший фильтр по минимальному порогу силы |
| 10, 11 | Пробой вверх/вниз | Цена закрытия за пределами подтверждённого экстремума |
| 12, 13 | Расстояние до уровня сопротивления/поддержки | Уровень относительно середины диапазона бара |
| 14, 15 | Сила и направление тренда | Плотность пробоев и их баланс с учётом знака |
| 16 | Отношение к скользящей средней (MA) | Цена закрытия относительно скользящей средней тренда |
| 17, 18, 19 | Покупка, продажа, сила сигнала | Сигналы входа при пробое с фильтрацией по тренду и их величина |
Окно обнаружения и измерение силы
За один проход по окну закрытых баров вычисляются четыре величины, от которых зависят дальнейшие расчёты для бара: максимум окна, минимум окна и две суммы для расчёта средних значений по окну. Их совместный расчет позволяет сохранить количество сравнений на один бар на уровне 2n+1, независимо от того, сколько признаков запрашивается.
//--- window [t-2n, t] is closed; its center is the candidate fractal const int start = t - 2 * m_n; const int center = t - m_n; const int width = 2 * m_n + 1; double win_high_max = high[start]; double win_low_min = low[start]; double sum_high = high[start]; double sum_low = low[start]; for(int i = start + 1; i <= t; i++) { if(high[i] > win_high_max) win_high_max = high[i]; if(low[i] < win_low_min) win_low_min = low[i]; sum_high += high[i]; sum_low += low[i]; } const double is_fractal_high = (high[center] == win_high_max) ? 1.0 : 0.0; const double is_fractal_low = (low[center] == win_low_min) ? 1.0 : 0.0;
Проверка на равенство точно воспроизводит работу детектора на Python, включая его поведение на плато: когда несколько баров в окне имеют одинаковое экстремальное значение, каждый из них регистрируется как фрактал, когда становится центральным баром. Это важнее при работе с потоком котировок брокера, чем при моделировании, поскольку котировки дискретны, а на инструментах с низкой волатильностью часто встречаются одинаковые максимумы.
Показатель «Сила» измеряет, насколько экстремум удален от среднего значения окна — именно это отличает структурную точку разворота от бара, который просто оказался немного выше соседних:
if(is_fractal_high == 1.0) high_strength = high[center] / (sum_high / width) - 1.0; if(is_fractal_low == 1.0) low_strength = 1.0 - low[center] / (sum_low / width); //--- the floor is either fixed or a multiple of volatility at the center bar const double floor_value = m_dynamic ? m_threshold * volatility[center] : m_threshold;
Ветвь m_dynamic представляет собой исправление второго недостатка, указанного в части 11. В модуле Python пороговое значение описывалось как динамическое, но показатель волатильности никогда не учитывался. В результате одно и то же фиксированное значение применялось к торговым сессиям как с узким, так и с широким ценовым диапазоном. Здесь выбор явно задаётся входным параметром: если InpDynamicThreshold включён, InpThreshold становится множителем для ATR, выраженного как доля цены и вычисленного на центральном баре, а не на баре подтверждения, поскольку проверяемая сила является свойством центрального бара.
Поддержка и сопротивление: кольцевой буфер событий, а не баров
Реализация на Python выделяет из ценового ряда бары с фракталом, прошедшим фильтр, вычисляет для этого подмножества скользящий максимум с длиной окна lookback_period и переиндексирует результат обратно на полный индекс баров с заполнением последним известным значением. Следствие этого легко понять неверно: lookback_period задаёт число фрактальных событий, а не баров. Если на инструменте с низкой волатильностью один подтверждённый фрактал появляется каждые сорок баров, то окно из двадцати событий охватывает примерно восемьсот баров.
Механизм обработки сохраняет ту же семантику и делает её явной: он хранит в небольшом массиве последние m_lookback подтверждённых экстремумов каждого типа и пересчитывает уровень при поступлении нового события:
if(valid_high == 1.0) { PushEvent(m_res_events, m_res_count, high[center]); m_resistance = MaxOf(m_res_events, m_res_count); } if(valid_low == 1.0) { PushEvent(m_sup_events, m_sup_count, low[center]); m_support = MinOf(m_sup_events, m_sup_count); }
Между событиями уровень остаётся неизменным — именно это обеспечивает заполнение последним известным значением в реализации на Python. Затем расстояния рассчитываются относительно середины текущего бара, поэтому они обновляются при каждом новом баре, даже если уровень не изменяется:
const double mid = 0.5 * (high[t] + low[t]); if(FRAC_IS_VALID(m_resistance)) row.distance_to_resistance = (m_resistance - mid) / mid; if(FRAC_IS_VALID(m_support)) row.distance_to_support = (mid - m_support) / mid;
Обратите внимание на асимметрию, которую это создаёт для модели, использующей эти данные. Ряд уровней кусочно-постоянен и изменяется только при подтверждении фракталов, тогда как ряд расстояний непрерывно зависит от цены. Модель на основе деревьев, получающая оба ряда, будет извлекать большую часть прогностической информации из расстояний; сами уровни практически бесполезны в качестве признаков и публикуются потому, что советнику (EA), который строит уровни стоп-лосса или тейк-профита, нужна цена, а не отношение.
Пробои, блок тренда и сигналы
Определение пробоя — единственное место, где модуль Python уже был каузальным. Он сравнивает текущую цену закрытия с экстремумом подтверждённого фрактала, причём оба значения берутся на t-n; именно поэтому исходный код применяет shift(n) здесь и нигде больше. Именно из-за этой несогласованности утечку будущих данных в другом месте модуля было легко пропустить при первом прочтении.
const double breakout_up = (close[t] > high[center] && is_fractal_low == 1.0) ? 1.0 : 0.0; const double breakout_down = (close[t] < low[center] && is_fractal_high == 1.0) ? 1.0 : 0.0;
Такое сочетание условий выбрано намеренно, и это стоит оговорить, поскольку оно выглядит как ошибка при переносе: для пробоя вверх цена закрытия должна превысить максимум центрального бара окна, хотя на этом баре расположен бычий (нижний) фрактал. Таким образом, условие срабатывает, когда цена отскакивает от локального минимума, а затем пробивает максимум того же бара, а не когда она преодолевает предыдущий уровень сопротивления. В перенесённой реализации это условие сохранено, чтобы обе реализации оставались сопоставимыми; читателю, предпочитающему определение через пробой сопротивления, следует изменить обе реализации одновременно.
Блок тренда использует кольцевой буфер пробоев вместо повторного просмотра истории:
double density = 0.0; double balance = 0.0; for(int i = 0; i < m_ma_period; i++) { density += m_bo_up[i] + m_bo_down[i]; balance += m_bo_up[i] - m_bo_down[i]; } row.trend_strength = density / m_ma_period; row.trend_direction = Sign(balance);
Сигналы представляют собой пробои, отфильтрованные по направлению; их величина определяется силой породившего их фрактала и нормируется по волатильности. В Python множитель величины сигнала имеет вид fractal_low_strength.shift(2): здесь жёстко задана задержка, которая соответствует значению по умолчанию n=2 и при любом другом значении незаметно нарушает временное выравнивание. В перенесённой реализации нет аналогичного выражения, в котором можно было бы допустить ошибку, поскольку сохраняемая сила уже относится к фракталу на баре t-n:
double strength = 0.0; if(buy == 1.0) strength = low_strength / (volatility[t] + 1e-8); else if(sell == 1.0) strength = high_strength / (volatility[t] + 1e-8);
Индикатор: две группы буферов с разными правилами
FractalViewer.mq5 содержит 20 буферов, разделённых на два класса: 2 буфера отображения и 18 буферов признаков. Буферы стрелок служат для отображения, и значения в них записываются на центральном баре, поскольку именно там на графике должен находиться экстремум. Значения в буферы признаков записываются на баре подтверждения, и только эти буферы разрешено читать советнику (EA).
//--- markers are display only: the value is known at t, drawn at t-n const int center = t - g_engine.HalfWindow(); if(center >= 0) { if(row.valid_high == 1.0) ArrowHighBuffer[center] = high[center]; if(row.valid_low == 1.0) ArrowLowBuffer[center] = low[center]; }
Второе правило касается того, какие бары вообще поступают в механизм обработки. ProcessBar поддерживает внутренние кольцевые буферы, поэтому повторная обработка бара привела бы к тому, что один и тот же фрактал дважды попал бы в кольцевой буфер уровней, а история пробоев сдвинулась бы на одну позицию. На графике в реальном времени функция OnCalculate вызывается на каждом тике; формирующийся бар занимает последнюю позицию, а его максимум, минимум и цена закрытия меняются до закрытия бара. Поэтому цикл останавливается, не доходя одного бара до конца массива:
//--- only closed bars are fed: the forming bar would be reprocessed on //--- every tick, and the engine consumes each bar exactly once const int last_closed = rates_total - 2; FractalRow row; for(int t = g_processed; t <= last_closed; t++) { ClearBar(t); if(!g_engine.ProcessBar(high, low, close, g_volatility, t, row)) return(0); StoreRow(t, row, high, low); g_processed = t + 1; }
Механизм обработки со своей стороны также обеспечивает соблюдение этого правила. ProcessBar отклоняет любой индекс, который не равен предыдущему плюс один, и выводит ожидаемое значение: так незаметное нарушение состояния становится явным благодаря записи в журнале уже при первом тестовом запуске.
Три дефекта, обнаруженных при переносе
Подача данных с индексацией как в таймсерии меняет направление задержки на противоположное без видимых изменений
Буферы индикатора по умолчанию имеют хронологическую индексацию, тогда как направление индексации массивов, передаваемых в OnCalculate, зависит от вызывающего кода. Если подать в механизм обработки копию данных с индексацией как в таймсерии, где индекс 0 соответствует самому новому бару, результат выдержит любую поверхностную проверку. Запуск поставляемого механизма обработки на копии того же ряда из 1 200 баров с обратным порядком элементов дал 180 обнаружений против 180 при корректном порядке входных данных — одинаковое количество, поскольку максимум окна не зависит от направления обхода.
Изменяется именно выравнивание. Сравнение обоих результатов с результатом центрированного детектора показывает, что при корректном порядке входных данных результат на 100 % баров совпадает с результатом детектора, сдвинутым вперёд на n баров, а при обратном порядке — с результатом, сдвинутым назад на n баров: возникает утечка будущих данных на 2n баров. Оба результата совпадают на 76.3 % баров, то есть эта ошибка не приводит ни к аварийному завершению, ни к полному отсутствию эффекта. Именно по этой причине индикатор вызывает ArraySetAsSeries(..., false) для каждого входного массива и каждого буфера.
Один столбец не прошёл проверку численного соответствия эталонной реализации на Python, и причина была не в механизме обработки
Первый полный прогон проверки численного соответствия эталонной реализации на Python прошёл успешно для семнадцати из восемнадцати столбцов, но не для signal_strength: отклонение составило 2.5e-9 при допуске 1e-9. Механизм обработки работал правильно: при прямом сравнении, без промежуточного файла, результаты совпали с точностью до 4.4e-16. Причиной стал экспорт. При записи столбца волатильности с помощью DoubleToString(value, 12) получаются двенадцать знаков после запятой, что для отношения ATR, близкого к 1e-3, соответствует девяти значащим цифрам. Деление на это значение в множителе величины сигнала увеличивает погрешность усечения примерно в тысячу раз, и только в этом столбце отклонение превышает порог.
Решение — использовать вариант с отрицательным значением параметра digits: DoubleToString(value, -16), который записывает шестнадцать значащих цифр в экспоненциальной форме вместо фиксированного числа знаков после запятой. После изменения все восемнадцать столбцов проходят проверку при допуске 1e-10. Общий вывод: проверка экспортированных данных на соответствие допуску оценивает не только код, но и формат экспорта, а сбой проверки лишь в одном столбце скорее указывает на проблему в сериализаторе, чем в алгоритме.
Механизм обработки с сохранением состояния и цикл, запускаемый на каждом тике, — неудачное сочетание по умолчанию
Именно кольцевой буфер уровней сделал эту проблему заметной. Механизм обработки, хранящий состояние только в пределах текущего окна, можно без последствий многократно вызывать на одном и том же баре; именно это обычно предполагается при реализации тела OnCalculate, которое пересчитывает последний бар на каждом тике. Здесь повторный вызов добавляет в кольцевой буфер дубликат события, поэтому уровень, который должен строиться по двадцати подтверждённым фракталам, в итоге может оказаться построенным по одному фракталу, учтённому двадцать раз. Механизм обработки можно было бы реализовать с поддержкой повторного вызова на том же баре, сохраняя теневую копию состояния, но ценой копирования каждого кольцевого буфера на каждом баре. Менее затратный контракт — отклонять входные данные, поступающие с нарушением порядка, и обрабатывать только закрытые бары. Это также позволяет явно выявлять ошибки.
Результаты: выходные значения признаков и численное соответствие эталонной реализации на Python
Механизм обработки с сохранением состояния был запущен на 1 200 синтетических часовых барах, сгенерированных с помощью рекуррентного расчёта дисперсии с кластеризацией волатильности; при этом локальные экстремумы возникают с частотой, сопоставимой с частотой их появления у торгуемого инструмента. На рисунке 2 показан фрагмент этого прогона длиной 160 баров.

Рисунок 2. Иллюстрация из трёх панелей, показывающая результаты работы механизма обработки в окне из 160 баров
- Панель (a): цена закрытия с маркерами подтверждённых фракталов, нанесёнными на центральный бар, и два ряда уровней, которые изменяются ступенчато только при поступлении нового подтверждённого фрактала в кольцевой буфер.
- Панель (b): расстояния от середины бара до каждого уровня — непрерывные признаки, используемые моделью.
- Панель (c): плотность пробоев за последние 20 баров и баланс с учётом знака, который служит фильтром сигналов входа.
На панели (a) показано поведение уровней, описанное в разделе 5. Уровень сопротивления остаётся неизменным на длинных отрезках и изменяется ступенчато только при поступлении подтверждённого медвежьего (верхнего) фрактала в кольцевой буфер, тогда как расстояние на панели (b) меняется на каждом баре. Направление тренда на панели (c) на протяжении большей части окна равно нулю, что корректно отражает баланс, равный ровно нулю, когда пробои вверх и вниз компенсируют друг друга.
Численное соответствие проверялось путём сопоставления результатов работы поставляемого механизма обработки с эталонной реализацией на Python для пяти наборов параметров, охватывающих значения n от 1 до 4, оба режима пороговой фильтрации и кольцевые буферы уровней вместимостью от 5 до 20 событий. Все восемнадцать столбцов совпали для каждого набора параметров; максимальное относительное отклонение составило 1.1e-13.

Рисунок 3. Иллюстрация из четырёх панелей, демонстрирующая численное совпадение результатов двух реализаций
- Панель (a): сила верхнего фрактала, показанная только для баров с медвежьим фракталом.
- Панель (b): расстояние до уровня сопротивления — непрерывный признак с наибольшей плотностью значений.
- График (c): отношение цены к фрактальной скользящей средней.
- График (d): сила сигнала, показанная только на барах с сигналом.
Сравнение после записи данных в файл и их последующего чтения — проверка, которую читатель может воспроизвести с помощью прилагаемых скриптов, — даёт следующие результаты:
Столбец | Максимальное абсолютное отклонение | r² |
|---|---|---|
| Флаги обнаружения и валидности | 0.0 | 1.000000 |
| Столбцы силы фракталов | 8.9e-16 | 1.000000 |
| Уровни | 0.0 | 1.000000 |
| Расстояния | 1.1·10⁻¹⁵ | 1.000000 |
| Трендовый блок | 1.1·10⁻¹⁵ | 1.000000 |
| Сила сигнала | 2.3·10⁻¹³ | 1.000000 |
Что это доказывает, а что нет. Это доказывает, что модель, обученная на признаках, рассчитанных на Python, и развёрнутая для работы с признаками, рассчитанными на MQL5, получает те же числовые значения — и только это свойство придаёт смысл конвейеру ONNX из серии «Blueprint». Это ничего не доказывает относительно того, позволяют ли эти признаки что-либо предсказывать. В синтетическом ряду нет структуры, которую можно было бы обнаружить, а проверка численного соответствия не позволяет отличить корректный признак от бесполезного, одинаково вычисляемого на двух языках.
Чтение признаков в советнике (EA)
Индикатор загружается один раз, а его значения считываются по индексу буфера. Безопасно считывать только буферы бара подтверждения; буферы 0 и 1 содержат маркеры отображения, значения которых записываются на n баров позже того бара, на котором они размещены.
int handle = iCustom(_Symbol, _Period, "FractalViewer", 2, 20, 20, 0.001, false, 14); if(handle == INVALID_HANDLE) return(INIT_FAILED); double buy[]; double size[]; double distance[]; //--- shift 1 is the most recent closed bar if(CopyBuffer(handle, 17, 1, 1, buy) < 1) return; if(CopyBuffer(handle, 19, 1, 1, size) < 1) return; if(CopyBuffer(handle, 12, 1, 1, distance) < 1) return; if(buy[0] == 1.0 && distance[0] > 0.0) OpenLong(size[0]);
Считывание со сдвигом 1, а не 0 — вторая часть контракта, обеспечивающего работу без утечки будущих данных. Сдвиг 0 соответствует формирующемуся бару, для которого индикатор намеренно оставляет буферы пустыми; тестирование на истории со считыванием этого бара из потока брокерских котировок с неполными данными по барам даст результаты, которые невозможно воспроизвести в реальной торговле. То же правило применяется к экспорту признаков для обучения модели: строка с временной меткой бара t содержит только информацию, доступную на момент закрытия бара t.
Заключение
Проблема утечки будущих данных из-за центрированного окна, описанная для Python в части 11, в MQL5 решается не сдвигом, поскольку в этом языке нет столбца, который можно было бы сдвинуть. В MQL5 нужно решить, по какому индексу бара записывать обнаруженный фрактал и в какой момент допустима запись; оба вытекающих из этого решения варианта допустимы для разметки графика, но лишь один — для буфера признаков. CFractalFeatures.mqh публикует каждый признак на баре подтверждения и хранит маркеры отображения в отдельных буферах, которые не должен считывать ни один потребитель данных.
Перенос выявил три проблемы, которые не могли возникнуть в реализации на Python. Поток данных с индексацией как в таймсерии превращает задержку подтверждения в смещение из-за использования будущих данных, хотя число обнаруженных фракталов остаётся неизменным. При экспорте в формате с недостаточным числом значащих цифр проверка на соответствие допуску не проходит в единственном столбце, где выполняется деление на малое число; это указывает на проблему в сериализаторе, а не в алгоритме. Механизм обработки с сохранением состояния, вызываемый из обработчика тиков, обрабатывает одни и те же бары повторно, если и цикл, и сам механизм не предотвращают такую обработку.
Численное соответствие эталонной реализации на Python достигается с точностью до 1.1e-13 для пяти наборов параметров и всех восемнадцати столбцов; это необходимое условие для обучения на одной стороне конвейера и торговли на другой. Это не доказывает, что признаки содержат прогностическую информацию. Этот вопрос рассматривается при оценке важности признаков в серии «Blueprint», а в следующей статье текущей серии пять созданных к настоящему моменту семейств признаков будут объединены в единый панельный индикатор, чтобы их можно было изучать вместе на одном графике.
Список литературы
- Лопес де Прадо, М. (2018). «Достижения в области финансового машинного обучения». Wiley.
- Вильямс, Б. (1998). «Торговый хаос: максимизация прибыли с помощью проверенных методов технического анализа». Wiley.
- Мандельброт, Б. (2004). «(Не)поведение рынков: фрактальный взгляд на финансовую турбулентность». Basic Books.
- Мерфи, Дж. Дж. (1999). Технический анализ финансовых рынков. Нью-Йоркский институт финансов.
Прикрепленные файлы
Файл | Расположение | Описание | |
|---|---|---|---|
| 1. | CFractalFeatures.mqh | MQL5\Include\Features | Модуль расчёта признаков. Init, Reset, ProcessBar для побарной обработки и Compute для обработки всего ряда. |
| 2. | FractalViewer.mq5 | MQL5\Indicators\Features | Индикатор для графика с четырьмя буферами отображения (два стрелочных маркера и две линии уровней) и шестнадцатью расчётными буферами; восемнадцать из двадцати буферов доступны через iCustom. |
| 3. | FractalValidation.mq5 | MQL5\Scripts\Features | Экспортирует бары и все восемнадцать признаков в CSV с точностью до шестнадцати значащих цифр. |
| 4. | fractals_reference.py | Python | Формулировка контракта без утечки будущих данных в терминах индексов баров и генератор синтетических рядов, использованный на рисунках 2 и 3. |
| 5. | validate_fractals.py | Python | Средство проверки численного соответствия эталонной реализации на Python. Выводит максимальное абсолютное отклонение и коэффициент r² для каждого столбца, проверяя соответствие заданному допуску. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/23657
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Я не торопясь прочитал это, и могу только сказать: спасибо за этот шедевр