Разработка инструментария для анализа Price Action (Часть 40): ДНК-профиль рынка
Содержание
Введение
В биологии дезоксирибонуклеиновая кислота (ДНК) – это молекула, которая кодирует генетический план, уникальный для каждого организма. ДНК определяет биологическую идентичность и передается из поколения в поколение. Однажды я наблюдал случай, когда мужчина отрицал отцовство, пока тест ДНК, проведенный после рождения ребенка, не доказал, что он действительно отец. Можно спросить, как это связано с анализом динамики цены: связь здесь в идее устойчивого, узнаваемого отпечатка.
В ходе исследования я обнаружил, что каждая валютная пара имеет собственный характерный профиль динамики цены. Некоторые пары могут вести себя похоже под воздействием общих факторов (например, EURUSD и GBPUSD), тогда как другие заметно отличаются. Чтобы выявлять эти паттерны, я создал автоматизированную систему, которая сканирует каждый инструмент и формирует Market DNA – компактный отпечаток на основе волатильности, фрактальной структуры, ритмов сессий и поведения откатов. Инструмент Market DNA Passport количественно описывает эти характеристики, позволяя сравнивать инструменты, обнаруживать структурные сдвиги (мутации) и выбирать или адаптировать стратегии к преобладающим рыночным режимам.
Мутация - это существенное изменение рыночного отпечатка, который формирует советник; она фиксируется, когда косинусное или нормализованное L2-расстояние между предыдущими и текущими DNAMetrics превышает заданные пороги, и обычно означает смену режима рынка (например, изменились ATR, всплески, доминирующая сессия или поведение откатов). Рассматривайте это как раннее предупреждение: проверьте, какие метрики изменились, ужесточите правила исполнения или временно приостановите торговлю (меньший размер позиции, более широкие ATR-стопы, более высокий порог сигнала), а затем подтвердите новый профиль несколькими перерасчетами или торговлей на демо-счете, прежде чем возвращаться к реальным сделкам.
// Show top N metric changes when a mutation is detected. // Call: ShowMutationDetails(gDNA_prev, gDNA, 3); void ShowMutationDetails(const DNAMetrics &prev_in, const DNAMetrics &cur_in, int topN=3) { // make local copies because DNAVector expects a non-const reference DNAMetrics prev = prev_in; DNAMetrics cur = cur_in; // metric names must follow the order in DNAVector() string names[] = { "wick_body_ratio_avg", "pct_close_near_high", "pct_close_near_low", "pct_doji", "atr_norm", "pct_spikes", "vol_clustering", "swing_cycle_bars_avg", "fractal_density", "breakout_follow_through", "retr_38_freq", "retr_50_freq", "retr_62_freq", "asia_range_share", "london_range_share", "ny_range_share", "smoothness_index" }; double va[], vb[]; DNAVector(cur, va); DNAVector(prev, vb); int n = ArraySize(va); if(n != ArraySize(names)) { Print("[ShowMutationDetails] vector/name-size mismatch"); return; } // diffs and indices double diffs[]; int idxs[]; ArrayResize(diffs, n); ArrayResize(idxs, n); for(int i=0; i<n; ++i) { diffs[i] = va[i] - vb[i]; idxs[i] = i; } // simple selection sort by absolute diff (descending) for(int i=0; i<n-1; ++i) { int best = i; for(int j=i+1; j<n; ++j) if(MathAbs(diffs[j]) > MathAbs(diffs[best])) best = j; // swap diffs double td = diffs[i]; diffs[i] = diffs[best]; diffs[best] = td; int ti = idxs[i]; idxs[i] = idxs[best]; idxs[best] = ti; } int show = MathMin(topN, n); string out = ""; for(int k=0; k<show; ++k) { int id = idxs[k]; double d = diffs[k]; double prevVal = vb[id]; double pct = (MathAbs(prevVal) < 1e-12 ? 0.0 : (d / MathAbs(prevVal) * 100.0)); string sign = (d >= 0.0 ? "+" : "-"); string line = StringFormat("%d) %s %s%.4f (%.1f%%)", k+1, names[id], sign, MathAbs(d), MathAbs(pct)); out += line + "\\n"; PrintFormat("[MarketDNA][Mutation] %s", line); } // show on-panel (adjust offsets/sizes if needed) int w = 360; int h = 18 * (show + 1); CreateOrSetRect("mut_detail_bg", InpCorner, InpX + 380, InpY + 340, w, h, BgColor()); CreateOrSetLabel("mut_detail_lbl", InpCorner, InpX + 388, InpY + 344, SafeText(out, 800), 9, MakeColor(200,120,40)); }
В следующих разделах я опишу используемые метрики, реализацию на MQL5 и практические примеры, показывающие, как этот паспорт помогает проводить надежный анализ.
Концептуальные метрики
Market DNA Passport сводит сотни баров необработанных ценовых данных к стабильным, объяснимым метрикам – всплескам, фрактальным свингам, частоте откатов, волатильности, нормализованной по ATR, долям диапазона по сессиям, плавности и т.д. Это позволяет трейдерам, работающим с данными ценового движения (Price Action), видеть структурное поведение рынка, а не только текущее положение цены, и принимать более быстрые и последовательные решения.
Почему эти метрики важны для трейдеров, работающих с Price Action:
| Важность | Пояснение |
|---|---|
| Объективное определение рыночного режима | Вместо того чтобы гадать, находится ли рынок в тренде или в диапазоне, советник количественно оценивает плавность, плотность фракталов и продолжение после пробоя, чтобы вы могли менять тактику на основе данных (следование за трендом или возврат к среднему). |
| Более точный выбор направления для входа | Такие метрики, как продолжение после пробоя и закрытие у максимума/минимума, дают дополнительное подтверждение для сигналов (продолжение пробоя против свечей ложного пробоя или разворота). |
| Размер позиции и фильтрация с учетом риска | Нормализованные по ATR показатели и частота всплесков помогают выбирать размер стопов и понимать, когда лучше не входить в рынок (высокий ATR или частые всплески = более широкие стопы или меньше сделок). |
| Проверка паттернов и матожидание | Гистограммы откатов показывают, насколько глубокими обычно бывают коррекции после импульсов на данном символе и таймфрейме; это важно для постановки реалистичных целей и оценки того, дает ли "откат до 50%" вероятное преимущество. |
| Эффективность отбора сделок | Советник сводит большой объем предторговой подготовки к компактному паспорту, который читается с одного взгляда, позволяя быстро просматривать символы и таймфреймы и выбирать только те, которые соответствуют вашему преимуществу в Price Action. |
| Объяснимость | Для каждого сигнала в лог записываются числовые основания (например, "высокое продолжение после пробоя + мало всплесков + плавный рынок"), чтобы вы могли проверить, почему вошли в сделку, и постепенно улучшать набор правил. |
Метрики анализа свечей и волатильности
1. Структура свечей и закрытие
wick_body_ratio_avg – среднее отношение суммарной длины теней к размеру тела свечи.
- Высокое значение → длинные тени → признаки разворота (пин-бар, падающая звезда).
- Низкое значение → чистые трендовые свечи → устойчивое давление покупателей или продавцов.
- Введен минимальный порог размера тела, чтобы избежать искажений.
pct_close_near_high – процент свечей, закрывшихся в верхних 20% диапазона.
- Высокое значение → сильный спрос, бычий перевес.
- На неликвидном рынке это может давать ложные сигналы.
pct_close_near_low – процент свечей, закрывшихся в нижних 20% диапазона.
- Высокое значение → доминирующее давление продавцов, медвежий перевес.
- Сессии с особыми паттернами могут искажать результат.
pct_doji – доля свечей-доджи (тело < ~10% диапазона).
- Высокое значение → неопределенность рынка.
- Серия доджи может предшествовать сильным пробоям.
nearHigh_count / nearLow_count – количество свечей, закрывшихся близко к максимуму или минимуму.
- Используется для оценки сдвига баланса спроса и предложения.
doji_count – абсолютное количество свечей-доджи.
- Помогает увидеть фазы ожидания перед движением.
2. Волатильность и моментум
atr_mean – среднее значение ATR (абсолютная волатильность в единицах цены).
- Используется для стопов и целей.
atr_pips – ATR, переведенный в пипсы.
- Удобно для сопоставления разных инструментов.
atr_norm – ATR, нормализованный по цене (ATR / Close).
- Дает относительную меру волатильности.
- Особенно чувствителен для инструментов с низкой ценой.
pct_spikes – доля свечей, у которых диапазон превышает ATR, умноженный на заданный коэффициент.
- Высокое значение → частые импульсы и шум.
spikes_count – абсолютное количество импульсных свечей.
bigTotal_count – общее количество больших свечей (> ATR).
bigThenBig_count – количество случаев, когда за большой свечой следует еще одна большая свеча.
- Высокое значение → кластеры волатильности, склонность к трендовому движению.
vol_clustering – доля больших свечей, за которыми следует еще одна большая свеча.
- Мера устойчивости повышенной волатильности.
3. Фракталы и циклы
swing_cycle_bars_avg – среднее количество баров между сменой фрактальных экстремумов.
- Длина цикла колебаний.
- В боковике это значение может заметно снижаться.
fractal_density – плотность фракталов (процент свечей с разворотами).
- Высокое значение → рынок неровный, боковой.
- Низкое значение → трендовый рынок.
sw_count – общее количество фрактальных точек.
breakout_follow_through – доля фрактальных уровней, подтвержденных продолжением после пробоя (на ≥ ATR).
- Высокое значение → пробои надежны.
- Низкое значение → много ложных сигналов.
4. Коррекции и откаты
retr_38_freq – частота откатов примерно на 38%.
- Типичны неглубокие коррекции.
retr_50_freq – частота откатов на 50% (44–56%).
- Классический уровень коррекции.
retr_62_freq – частота глубоких коррекций (~62%).
- Часто указывает на изменение волатильности или режима рынка.
retr_gt70_freq – частота откатов >70%.
- Высокое значение → слабые импульсы, возврат в диапазон.
avg_max_retr – средняя максимальная глубина отката после импульсов.
- Помогает задавать реалистичные стопы и цели.
retr_count – количество импульсов, по которым считались откаты (важно для надежности).
5. Активность сессий
asia_range_share / london_range_share / ny_range_share – доля общего диапазона, пришедшаяся на соответствующую сессию.
- Показывает, какая сессия вносит наибольший вклад в движение.
asia_range / london_range / ny_range – абсолютный диапазон от максимума до минимума в рамках сессии.
- Характеризует волатильность в определенное время суток.
6. Сводные индексы и служебные параметры
smoothness_index (0-1) – индекс плавности.
- Высокое значение → трендовый рынок, меньше шума.
- Низкое значение → боковой, неровный рынок.
atr_cache_used – служебный флаг использования кэшированных данных ATR.
sample_bars – объем выборки (количество баров).
- Большие выборки → более стабильные, но менее чувствительные к изменениям.
Реализация на MQL5
На MQL5 движок Market DNA реализован как компактная система. Она считывает исторические бары выбранного символа и таймфрейма, вычисляет отпечаток структуры цены (свечи, ATR, фрактальные свинги, откаты и доли диапазона по сессиям), отображает на графике "ДНК-паспорт", при необходимости сравнивает второй символ, записывает метрики и сигналы в CSV и выдает простые рекомендации BUY/SELL на основе набора правил и предупреждения о мутациях, когда отпечаток заметно меняется. В этом одном предложении и заключена цель: дальше код сосредоточен на создании воспроизводимых, объяснимых метрик, которые можно проверять вручную или использовать в дальнейшем анализе.
Чтобы вам было легче ориентироваться в реализации, в этом разделе описаны условия сборки и работы, рекомендованы разумные стартовые входные параметры, а также выделены ключевые вспомогательные функции и обработчики жизненного цикла, которые встретятся в файле, например OnInit, OnTimer, Recalculate, BuildDNA и DrawPassport. Освоив эти базовые вещи с самого начала, вы сможете легче пройти последующее пятишаговое руководство и не потеряться в деталях.
Перед началом убедитесь, что ваша среда соответствует нескольким базовым требованиям. Вам нужен MetaTrader 5 с возможностью компилировать и запускать советники (в коде используются #property strict и Trade/Trade.mqh). Советник записывает данные в общую папку файлов терминала, поэтому убедитесь, что операции ввода-вывода с файлами разрешены. Наконец, выберите символ и таймфрейм, для которых доступно не менее ~300 исторических баров; для устойчивой статистики рекомендуется 1 200 баров. Если истории недостаточно, советник предупредит об этом, а результаты будут шумными.
#property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com/ru/users/lynnchris" #property version "1.0" #property strict #include <Trade/Trade.mqh>
Если вы хотите как можно быстрее опробовать систему, скопируйте файл Market DNA Passport.mq5 в папку MQL5/Experts, скомпилируйте его в MetaEditor, прикрепите скомпилированный советник к графику, задайте InpTF и InpBars (по умолчанию PERIOD_H1 и 1200), а при необходимости включите InpLogCSV, чтобы сохранять метрики. Советник работает по таймеру и пересчитывается на каждом новом закрытом баре; после короткого запуска проверьте папку Files\Common – там должны появиться файлы MarketDNA_log.csv и MarketDNA_signals.csv, чтобы просмотреть записанные результаты.
Базовый набор рекомендуемых входных параметров: InpTF = PERIOD_H1, InpBars = 1200, InpATRPeriod = 14, InpSpikeATRMult = 2.0, InpDojiBodyPct = 0.10, InpFractalDepth = 5, InpRetrWindowBars = 50 и InpFT_ATR_Mult = 0.5. Для приемлемой отзывчивости и умеренной нагрузки на процессор установите InpRecalcSeconds = 10 и InpCacheATR = true. Рассматривайте их как отправные точки для настройки под конкретный символ и торговый горизонт.
//============================== INPUTS ==============================// input ENUM_TIMEFRAMES InpTF = PERIOD_H1; // Analysis timeframe input int InpBars = 1200; // Bars to analyze (>= 300 recommended) input int InpATRPeriod = 14; // ATR period input double InpSpikeATRMult = 2.0; // Spike threshold in ATR multiples input double InpDojiBodyPct = 0.10; // Doji body <= % of candle range input int InpFractalDepth = 5; // Fractal depth (ZigZag-like swings) input int InpRetrLookbackSwings = 80; // Max impulses to evaluate retracements input int InpRetrWindowBars = 50; // How many bars forward to scan for retracement input double InpFT_ATR_Mult = 0.5; // Breakout follow-through threshold (in ATR multiples) input string InpCompareSymbol = ""; // Optional second symbol to compare input int InpRecalcSeconds = 10; // Recalc cadence (seconds) input int InpCorner = 0; // Panel corner (0-3) input int InpX = 12; // Panel X offset input int InpY = 24; // Panel Y offset input bool InpDarkTheme = true; // Dark panel theme input int InpMetricPalette = 1; // Metric color palette (0=Warm Brown,1=DarkGray,2=NearBlack,3=Lilac,4=RichBrown,5=HighContrast) input bool InpAlertsOnMutation = true; // Alert on DNA shifts input double InpMutationThresh = 0.12; // Cosine distance to flag change input double InpMutationL2Thresh = 0.05; // Normalized L2 change to flag mutation input bool InpLogCSV = false; // Append results to CSV in common Files folder input string InpCSVFileName = "MarketDNA_log.csv"; // CSV filename (FILE_COMMON) input bool InpCacheATR = true; // Cache ATR array between runs input bool InpSelfTest = false; // Run small synthetic self-test at init input int InpAsiaStart = 0; // Session hour boundaries (server hours) input int InpAsiaEnd = 7; input int InpLondonStart = 8; input int InpLondonEnd = 15; input int InpNYStart = 16; input int InpNYEnd = 23; input bool InpDebugRetr = false; // Print retracement debug lines to Experts
Практические замечания по именованию, безопасности и производительности: советник добавляет префикс MDNA_<symbol>_<TF>_ к объектам на графике, чтобы избежать конфликтов; CSV-файлы лежат в папке FILE_COMMON с фиксированными именами. Для безопасности и производительности советник поддерживает минимальный интервал таймера (≥5 с) и избегает тяжелых пересчетов, если последний закрытый бар не изменился. Если вы запускаете много экземпляров или используете большое окно истории (например, >5k баров), увеличьте интервал таймера или уменьшите InpBars, чтобы ограничить нагрузку на процессор и память. Также обратите внимание: несколько экземпляров советника, записывающих один и тот же CSV, могут вызвать состояния гонки; для надежного логирования с нескольких графиков лучше использовать уникальные имена файлов для каждого символа и таймфрейма.
После этой вводной переходим к реализации: сбору и предварительной обработке данных, метрикам анатомии свечей и волатильности, структурному анализу свингов, откатов и пробоев, сравнению снимков и генерации сигналов, а затем к интерфейсу, сохранению данных и управлению жизненным циклом. Каждый раздел напрямую соответствует функциям и логическим блокам в файле .mq5, поэтому вы сможете прослеживать код, локально воспроизводить результаты и адаптировать движок под свои исследовательские или торговые задачи.
Сбор данных и предварительная обработка
Сначала через LoadRates(sym, tf, bars) мы загружаем историю цен функцией CopyRates, причем берем запас по барам, чтобы для расчетов с окном просмотра (ATR и окна откатов) хватало контекста. Мы вычисляем True Range для каждого бара через BarTR(), а затем ATR на основе простой SMA для каждого индекса через CalcATR(r, period, idx). Если включен InpCacheATR и сохраненные параметры совпадают, мы повторно используем глобальный кэш g_atrs[], чтобы не пересчитывать ATR заново (см. проверку кэша внутри BuildDNA). Мы проверяем и нормализуем входные данные и промежуточные значения с помощью таких вспомогательных функций, как SafeDiv, Clamp и LTrim, переводим ATR в пипсы и нормализованные единицы (atr_pips, atr_norm) и контролируем минимальное число баров – если N < 300, функция присваивает D.valid значение false и прекращает обработку. Основные результаты этого этапа – массив MqlRates r[] и массив ATR, который используется дальше.
// Load rates with safety margin bool LoadRates(string sym, ENUM_TIMEFRAMES tf, int bars, MqlRates &rates[]) { ArraySetAsSeries(rates, true); int need = MathMax(300, bars + 200); // safety margin for forward scans int got = CopyRates(sym, tf, 0, need, rates); if(got <= 0) return false; return (got >= bars); } // True Range (series layout: 0 newest) double BarTR(MqlRates &r[], int i) { if(i >= ArraySize(r)-1) return 0.0; double prevC = r[i+1].close; double tr1 = r[i].high - r[i].low; double tr2 = MathAbs(r[i].high - prevC); double tr3 = MathAbs(r[i].low - prevC); return MathMax(tr1, MathMax(tr2, tr3)); } // Simple SMA ATR computed over 'period' TRs starting at idx double CalcATR(MqlRates &r[], int period, int idx) { double tr_sum = 0.0; int count = 0; for(int i = idx; i < idx + period && i < ArraySize(r)-1; ++i) { tr_sum += BarTR(r, i); ++count; } return (count > 0 ? tr_sum / count : 0.0); }
Анатомия свечи и метрики волатильности
Далее мы перебираем очищенный ряд, чтобы вычислить характеристики свечей и показатели волатильности. Цикл внутри BuildDNA вычисляет среднее отношение тени к телу (wick_body_ratio_avg), доли закрытий вблизи максимума и минимума (pct_close_near_high, pct_close_near_low), определяет долю свечей доджи (pct_doji) по параметру InpDojiBodyPct и отмечает всплески ATR, когда TR > InpSpikeATRMult * atr_i. Мы также выявляем "большие" бары по ATR и последовательности "большой-затем-большой", чтобы оценить vol_clustering. Все накопители по каждому бару агрегируются в ограниченные метрики и исходные счетчики (например, nearHigh_count, spikes_count, bigThenBig_count), которыми заполняется структура DNAMetrics. Эти поля намеренно ограничиваются и нормализуются, чтобы служить надежной основой для сравнения сходства и эвристик сигналов.
// Example loop computing candle descriptors (place inside BuildDNA after rates & atrs ready) int nearHigh=0, nearLow=0, doji=0, spikes=0; double wickBodyAccum = 0.0; int bigTotal=0, bigThenBig=0; for(int i = 0; i < N; ++i) { double high = r[i].high, low = r[i].low, open = r[i].open, close = r[i].close; double range = high - low; if(range <= 0.0) continue; double body = MathAbs(close - open); if(body < 1e-9) body = 1e-9; double upper = (close >= open ? high - close : high - open); double lower = (close >= open ? open - low : close - low); double minBody = MathMax(body, range * 0.02); // floor to avoid tiny-body noise double wickRatio = (upper + lower) / minBody; wickRatio = MathMin(MathMax(wickRatio, 0.0), 50.0); wickBodyAccum += wickRatio; double pos = (close - low) / range; if(pos >= 0.80) ++nearHigh; else if(pos <= 0.20) ++nearLow; if(body <= InpDojiBodyPct * range) ++doji; double tr = BarTR(r, i); double atr_i = atrs[i]; if(atr_i > 0 && tr > InpSpikeATRMult * atr_i) ++spikes; bool big = (atr_i > 0 && tr > 1.0 * atr_i); if(big) { ++bigTotal; if(i > 0) { double trPrev = BarTR(r, i-1); double atrPrev = atrs[i-1]; if(atrPrev > 0 && trPrev > 1.0 * atrPrev) ++bigThenBig; } } } // Aggregate into DNAMetrics fields (example assignments) D.wick_body_ratio_avg = (N > 0 ? wickBodyAccum / N : 0.0); D.pct_close_near_high = SafeDiv(nearHigh, N); D.pct_close_near_low = SafeDiv(nearLow, N); D.pct_doji = SafeDiv(doji, N); D.pct_spikes = SafeDiv(spikes, N); D.vol_clustering = (bigTotal > 0 ? SafeDiv(bigThenBig, bigTotal) : 0.0); D.nearHigh_count = nearHigh; D.spikes_count = spikes; D.bigThenBig_count = bigThenBig;
Структурный анализ: свинги, откаты и продолжение пробоя
Мы находим фрактальные точки с помощью вызова функции BuildSwings(r, InpFractalDepth, sw[]), которая ищет локальные максимумы и минимумы в окрестности заданной глубины. Затем мы вычисляем статистику цикла свинга (среднее число баров на импульс swing_cycle_bars_avg и fractal_density). Для анализа откатов мы вызываем ComputeRetracementHistogram(r, sw, swN, ...). Для каждого импульса в противоположном направлении функция просматривает следующие InpRetrWindowBars баров и фиксирует максимальную долю отката относительно импульса; затем эти максимумы распределяются по диапазонам 38%/50%/62%/>70% и усредняются (avg_max_retr), что дает retr_38_freq, retr_50_freq, retr_62_freq и retr_gt70_freq.
Отдельно ComputeBreakoutFollowThrough просматривает события свинга и проверяет, достигает ли пробой за пределы экстремума свинга цели, масштабированной по ATR (InpFT_ATR_Mult * atr); на выходе breakout_follow_through представляет собой долю событий с продолжением. Вместе эти структурные метрики количественно описывают ритм тренда, типичную глубину откатов и надежность пробоев.
// Fractal swing builder (returns newest-first series layout) struct Swing { int index; double price; bool isHigh; }; int BuildSwings(MqlRates &r[], int depth, Swing &sw[]) { ArrayResize(sw, 0); int N = ArraySize(r); for(int i = depth; i < N - depth; ++i) { bool highP = true, lowP = true; double h = r[i].high, l = r[i].low; for(int k = 1; k <= depth; ++k) { if(r[i-k].high >= h || r[i+k].high >= h) highP = false; if(r[i-k].low <= l || r[i+k].low <= l) lowP = false; if(!highP && !lowP) break; } if(highP) { int n = ArraySize(sw); ArrayResize(sw, n+1); sw[n].index = i; sw[n].price = h; sw[n].isHigh = true; } if(lowP) { int n = ArraySize(sw); ArrayResize(sw, n+1); sw[n].index = i; sw[n].price = l; sw[n].isHigh = false; } } // sort by index ascending (newest first in series layout) for(int a=0;a<ArraySize(sw);++a) for(int b=a+1;b<ArraySize(sw);++b) if(sw[a].index > sw[b].index) { Swing t = sw[a]; sw[a] = sw[b]; sw[b] = t; } return ArraySize(sw); } // Retracement histogram (core loop) void ComputeRetracementHistogram(MqlRates &r[], Swing &sw[], int swN, double &f38, double &f50, double &f62, double &f70, double &avgRetr, int &counted_out) { int counted=0, c38=0, c50=0, c62=0, c70=0; double sumMaxRetr = 0.0; for(int i=0; i < swN-1 && counted < InpRetrLookbackSwings; ++i) { Swing a = sw[i], b = sw[i+1]; if(a.isHigh == b.isHigh) continue; Swing older = (a.index > b.index ? a : b); Swing newer = (a.index > b.index ? b : a); double impulse = MathAbs(older.price - newer.price); int start = newer.index - 1; int end = MathMax(0, newer.index - InpRetrWindowBars); double maxRetr = 0.0; if(impulse > 0 && start >= 0 && start >= end) { if(older.isHigh && !newer.isHigh) { for(int k = start; k >= end; --k) { double retr = SafeDiv(r[k].high - newer.price, impulse); if(retr > maxRetr) maxRetr = retr; } } else if(!older.isHigh && newer.isHigh) { for(int k = start; k >= end; --k) { double retr = SafeDiv(newer.price - r[k].low, impulse); if(retr > maxRetr) maxRetr = retr; } } } counted++; if(impulse > 0) { sumMaxRetr += maxRetr; if(maxRetr < 0.44) ++c38; else if(maxRetr < 0.56) ++c50; else if(maxRetr < 0.70) ++c62; else ++c70; } } if(counted > 0) { f38 = double(c38) / counted; f50 = double(c50) / counted; f62 = double(c62) / counted; f70 = double(c70) / counted; avgRetr = sumMaxRetr / counted; } else { f38 = f50 = f62 = f70 = avgRetr = 0.0; } counted_out = counted; } // Breakout follow-through check double ComputeBreakoutFollowThrough(MqlRates &r[], Swing &sw[], int swN, int atrPeriod, double ftAtrMult) { if(swN < 2) return 0.0; int events = 0, success = 0; for(int i=0; i < swN-1 && events < 80; ++i) { int s_index = sw[i].index; double s_price = sw[i].price; bool s_isHigh = sw[i].isHigh; int start = s_index - 1; if(start < 0) continue; double atr = CalcATR(r, atrPeriod, s_index); double target = ftAtrMult * atr; if(target <= 0) continue; bool broke = false, followed = false; for(int k = start; k >= 0; --k) { if(s_isHigh) { if(r[k].high > s_price) { broke = true; if((r[k].high - s_price) >= target) { followed = true; break; } } } else { if(r[k].low < s_price) { broke = true; if((s_price - r[k].low) >= target) { followed = true; break; } } } } if(broke) { ++events; if(followed) ++success; } } return (events > 0 ? double(success) / events : 0.0); }
Сравнение снимков, обнаружение мутаций и генерация сигналов
Как только у нас появляется новый экземпляр DNAMetrics, мы преобразуем выбранные поля в фиксированный 17-мерный вектор с помощью DNAVector(D, vec[]). Мы сравниваем текущую и предыдущую версии с помощью CosineDistance(A,B) и NormalizedL2Distance(A,B). Если любое из расстояний превышает InpMutationThresh или InpMutationL2Thresh, система отмечает мутацию на панели и, если опция включена, вызывает Alert(); кроме того, код окрашивает баннер по степени серьезности.
Параллельно с обнаружением мутаций GenerateSignal(constDNAMetrics &D) формирует оценки для BUY и SELL на основе продолжения пробоя, индекса плавности, частоты всплесков, близости к минимуму и максимуму и диапазонов откатов; затем функция применяет штраф на основе ATR, ограничивает оценки и выдает BUY/SELL только тогда, когда оценка достигает InpSignalThreshold и превышает противоположную на InpSignalGap. MaybeNotify вводит таймаут для уведомлений (15 минут); сигналы и полные диагностические строки с причинами записываются в MarketDNA_signals.csv для офлайн-анализа.
// Build vector for comparison (17-dim) void DNAVector(DNAMetrics &D, double &vec[]) { int n = 17; ArrayResize(vec, n); vec[0] = D.wick_body_ratio_avg; vec[1] = D.pct_close_near_high; vec[2] = D.pct_close_near_low; vec[3] = D.pct_doji; vec[4] = D.atr_norm; vec[5] = D.pct_spikes; vec[6] = D.vol_clustering; vec[7] = D.swing_cycle_bars_avg; vec[8] = D.fractal_density; vec[9] = D.breakout_follow_through; vec[10] = D.retr_38_freq; vec[11] = D.retr_50_freq; vec[12] = D.retr_62_freq; vec[13] = D.asia_range_share; vec[14] = D.london_range_share; vec[15] = D.ny_range_share; vec[16] = D.smoothness_index; } // Cosine distance and normalized L2 double CosineDistance(DNAMetrics &A, DNAMetrics &B) { double va[], vb[]; DNAVector(A, va); DNAVector(B, vb); double dot=0, na=0, nb=0; for(int i=0;i<ArraySize(va);++i) { dot += va[i]*vb[i]; na += va[i]*va[i]; nb += vb[i]*vb[i]; } double denom = MathSqrt(na)*MathSqrt(nb); if(denom <= 0) return 1.0; double cos = dot / denom; return 1.0 - MathMax(-1.0, MathMin(1.0, cos)); } double NormalizedL2Distance(DNAMetrics &A, DNAMetrics &B) { double va[], vb[]; DNAVector(A, va); DNAVector(B, vb); double num=0.0, denom=0.0; for(int i=0;i<ArraySize(va);++i) { double d = va[i] - vb[i]; num += d*d; denom += va[i]*va[i]; } double l2 = MathSqrt(num); double scale = MathSqrt(denom) + 1e-9; return l2 / scale; } // Signal generation (rule-based) Signal GenerateSignal(const DNAMetrics &D) { Signal s; s.type = SIGNAL_NONE; s.score = 0.0; s.reason = ""; double cFT = Clamp(D.breakout_follow_through, 0.0, 1.0); double cSmooth = Clamp(D.smoothness_index, 0.0, 1.0); double cNotSpikes = 1.0 - Clamp(D.pct_spikes, 0.0, 1.0); double cRetr38 = Clamp(D.retr_38_freq, 0.0, 1.0); double cRetrGT70 = Clamp(D.retr_gt70_freq, 0.0, 1.0); // weights double wFT = 0.50, wSmooth = 0.25, wNotSpikes = 0.15, wRetr38 = 0.10; double wSellNearLow = 0.30, wSellSpikes = 0.25, wSellSmoothInv = 0.25, wSellRetrGT70 = 0.20; double buyScore = wFT*cFT + wSmooth*cSmooth + wNotSpikes*cNotSpikes + wRetr38*cRetr38; double sellScore = wSellNearLow*Clamp(D.pct_close_near_low,0,1) + wSellSpikes*Clamp(D.pct_spikes,0,1) + wSellSmoothInv*(1.0 - cSmooth) + wSellRetrGT70*cRetrGT70; double atrPenalty = Clamp(D.atr_norm * 10.0, 0.0, 0.5); double buyScorePen = Clamp(buyScore * (1.0 - atrPenalty), 0.0, 1.0); double sellScorePen = Clamp(sellScore * (1.0 - atrPenalty * 0.5), 0.0, 1.0); double minThreshold = InpSignalThreshold; double minGap = InpSignalGap; s.reason = StringFormat("buy_raw=%.3f sell_raw=%.3f buy=%.3f sell=%.3f", buyScore, sellScore, buyScorePen, sellScorePen); if(buyScorePen - sellScorePen >= minGap && buyScorePen >= minThreshold) { s.type = SIGNAL_BUY; s.score = buyScorePen; } else if(sellScorePen - buyScorePen >= minGap && sellScorePen >= minThreshold) { s.type = SIGNAL_SELL; s.score = sellScorePen; } else { s.type = SIGNAL_NONE; s.score = MathMax(buyScorePen, sellScorePen); } return s; } // Notification cooldown void MaybeNotify(const Signal &s, string sym) { if(s.type == SIGNAL_NONE) return; if(TimeCurrent() - gLastSignalTime < 60*15) return; // 15-minute cooldown string text = StringFormat("MarketDNA %s %s signal score=%.2f reason=%s", sym, SignalTypeToString(s.type), s.score, s.reason); SendNotification(text); Alert(text); gLastSignalTime = TimeCurrent(); }
Отображение, сохранение и управление жизненным циклом
Наконец, мы выводим результаты с помощью объектов на графике. DrawPassport(title, D, x, y) собирает прямоугольную панель (ObjectCreate с OBJ_RECTANGLE_LABEL) и набор подписанных строк (OBJ_LABEL), которые показывают все метрики, предупреждения и временные метки; DrawComparison рисует компактный заголовок сходства, когда задан символ для сравнения. Мы сохраняем результаты через WriteCSV(D, sym) и WriteSignalCSV(s, sym) в файлы в каталоге FILE_COMMON (если файл создается впервые, его заголовок записывается автоматически).
В жизненном цикле советника OnInit задает EventSetTimer(сек) (при этом sec >= 5), OnTimer вызывает Recalculate(), а Recalculate() пропускает тяжелые пересчеты, если время последнего закрытого бара не изменилось (проверка через CopyRates(..., 0, 1)). OnDeinit останавливает таймер, а ClearObjects() удаляет UI-объекты с заданным префиксом. Необязательные режимы InpSelfTest и InpDebugRetr позволяют проверить логику свингов и откатов и вывести отладочную информацию.
// Create or update label helper void CreateOrSetLabel(string name, int corner, int x, int y, string text, int fontsize=11, color clr=(color)(-1)) { string obj = Pref()+name; color useclr = (clr == (color)(-1) ? TextColor() : clr); if(ObjectFind(0,obj) == -1) { ObjectCreate(0,obj,OBJ_LABEL,0,0,0); ObjectSetInteger(0,obj,OBJPROP_CORNER,corner); ObjectSetInteger(0,obj,OBJPROP_XDISTANCE,x); ObjectSetInteger(0,obj,OBJPROP_YDISTANCE,y); ObjectSetInteger(0,obj,OBJPROP_FONTSIZE,fontsize); ObjectSetInteger(0,obj,OBJPROP_COLOR,useclr); ObjectSetString(0,obj,OBJPROP_FONT,"Arial"); } ObjectSetString(0,obj,OBJPROP_TEXT,text); } // Minimal DrawPassport example (truncated) void DrawPassport(string title, DNAMetrics &D, int x, int y) { CreateOrSetRect(title+"_bg", InpCorner, x, y, 360, 460, BgColor()); CreateOrSetLabel(title+"_hdr", InpCorner, x+10, y+8, title, 12, Accent()); CreateOrSetLabel(title+"_ATR", InpCorner, x+10, y+32, StringFormat("ATR: %.1f pips (%.3f%%)", D.atr_pips, D.atr_norm*100.0), 10, TextColor()); CreateOrSetLabel(title+"_WB", InpCorner, x+10, y+50, StringFormat("Wick/Body avg: %.2f", D.wick_body_ratio_avg), 10, TextColor()); // ... add more labels for other metrics } // CSV writing (append, creates header when empty) void WriteCSV(DNAMetrics &D, string sym) { if(!InpLogCSV) return; string fname = InpCSVFileName; int handle = FileOpen(fname, FILE_READ|FILE_WRITE|FILE_CSV|FILE_COMMON|FILE_ANSI); if(handle == INVALID_HANDLE) { PrintFormat("Unable to open CSV '%s'", fname); return; } if(FileSize(handle) == 0) FileWrite(handle, "timestamp","symbol","tf","sample_bars","wick_body_avg","pct_close_high","pct_close_low","pct_doji","atr_mean","atr_pips"); FileSeek(handle, 0, SEEK_END); FileWrite(handle, TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS), sym, TFToString(InpTF), D.sample_bars, D.wick_body_ratio_avg, D.pct_close_near_high, D.pct_close_near_low, D.pct_doji, D.atr_mean, D.atr_pips); FileClose(handle); } // Lifecycle hooks (skeleton) int OnInit() { EventSetTimer(MathMax(5, InpRecalcSeconds)); // enforce minimal cadence CreateOrSetLabel("status", InpCorner, InpX, InpY + 380, "Idle: waiting first calculation...", 10, TextColor()); if(InpSelfTest) SelfTest(); Recalculate(); // initial build return INIT_SUCCEEDED; } void OnTimer() { Recalculate(); // optimized to rebuild only on new closed bar } void OnDeinit(const int reason) { EventKillTimer(); ClearObjects(); // cleanup }
Результаты
Когда мы прикрепили советник к графику EURUSD H1, он сразу построил панель Market-DNA и вычислил метрики инструмента по выбранной выборке. Панель сводит профили волатильности, всплесков и откатов, вклад сессий и другие структурные сигналы; на основе этих нормализованных метрик советник вычислил отдельные оценки для BUY и SELL и выдал сигнал BUY, потому что оценка BUY превысила оценку SELL на заданный разрыв и прошла порог. Отметка времени на панели, оценка и диагностическая строка объясняют, почему перевес был на стороне покупателей (высокая доля пробоев с продолжением, низкая частота всплесков, умеренная плавность), поэтому схема не просто показывает отдельную торговую рекомендацию, а документирует сам процесс принятия решения.
Из выборки в 1 200 баров мы получили 171 свинг и 80 откатов (около 44,2% свингов дали измеримый откат). Лондонская сессия занимает наибольшую долю диапазона, поэтому если вы хотите торговать резкими движениями, сосредоточьтесь на лондонских часах. В среднем цикл свинга составляет 9,62 бара, что показывает, насколько регулярно возникают свинги. Самое главное – доля пробоев с продолжением чрезвычайно высока (≈98%).

Аналогичная диаграмма показана для GBPUSD – паре, которая коррелирует с EURUSD. Market-DNA показывает более высокую вариативность по сравнению с EURUSD: зафиксирован 161 свинг и 80 откатов, а средний цикл свинга составляет 11,28 бара. Доля пробоев с продолжением высока (≈94%), а на лондонскую сессию приходится наибольшая доля диапазона (≈46%).
Заключение
Теперь, когда мы прошли весь путь – от идеи и реализации до тестирования с обнадеживающими результатами, – можно заключить, что этот инструмент действительно улавливает уникальный "профиль" каждой пары на основе движения цен. Вы можете экспериментировать с входными параметрами, проводить тестирование на исторических данных или демо-тестирование, чтобы подобрать значения под свою стратегию. Однако помните, что этот советник в первую очередь предназначен для обучения – он прежде всего показывает индивидуальность каждой пары через исторические данные Price Action. Его не следует использовать для реальной торговли на деньги – лучше рассматривать его как вспомогательный инструмент в дополнение к вашим действующим стратегиям.
Читайте другие мои статьи здесь.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/19460
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Тестер стратегий для Python и MetaTrader 5 (Часть 1): Торговый симулятор
Статистический арбитраж на основе коинтегрированных акций (заключительная часть): Анализ данных с помощью специализированной БД
Автоматизация торговых стратегий в MQL5 (Часть 25): Советник для торговли по линиям тренда с аппроксимацией методом наименьших квадратов и динамической генерацией сигналов
Разработка инструментария для анализа Price Action (Часть 39): Автоматизация обнаружения BOS и ChOCh на MQL5
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Снова отличная идея и работа от К. Бенджамина, спасибо и здоровых дней вам и вашим близким.