English Deutsch 日本語
preview
Разработка инструментария для анализа Price Action (Часть 40): ДНК-профиль рынка

Разработка инструментария для анализа Price Action (Часть 40): ДНК-профиль рынка

MetaTrader 5Примеры |
334 2
Christian Benjamin
Christian Benjamin

Содержание



Введение

В биологии дезоксирибонуклеиновая кислота (ДНК) – это молекула, которая кодирует генетический план, уникальный для каждого организма. ДНК определяет биологическую идентичность и передается из поколения в поколение. Однажды я наблюдал случай, когда мужчина отрицал отцовство, пока тест ДНК, проведенный после рождения ребенка, не доказал, что он действительно отец. Можно спросить, как это связано с анализом динамики цены: связь здесь в идее устойчивого, узнаваемого отпечатка.

В ходе исследования я обнаружил, что каждая валютная пара имеет собственный характерный профиль динамики цены. Некоторые пары могут вести себя похоже под воздействием общих факторов (например, 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

    Прикрепленные файлы |
    Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
    Mustafa Nail Sertoglu
    Mustafa Nail Sertoglu | 18 сент. 2025 в 07:35
    Снова прекрасная идея и работа от К. Бенджамина, спасибо и удачных дней вам и вашим близким.
    Christian Benjamin
    Christian Benjamin | 18 сент. 2025 в 08:45
    Mustafa Nail Sertoglu #:
    Снова отличная идея и работа от К. Бенджамина, спасибо и здоровых дней вам и вашим близким.
    Это удовольствие для меня.
    Тестер стратегий для Python и MetaTrader 5 (Часть 1): Торговый симулятор Тестер стратегий для Python и MetaTrader 5 (Часть 1): Торговый симулятор
    Модуль MetaTrader 5 для Python, предоставляет удобный способ открывать сделки в приложении MetaTrader 5 с помощью Python, но у него есть серьезная проблема: в нем нет возможностей тестера стратегий, присутствующих в приложении MetaTrader 5. В этой серии статей мы создадим фреймворк для бэктестинга ваших торговых стратегий в средах Python.
    Статистический арбитраж на основе коинтегрированных акций (заключительная часть): Анализ данных с помощью специализированной БД Статистический арбитраж на основе коинтегрированных акций (заключительная часть): Анализ данных с помощью специализированной БД
    В статье рассказывается, как объединить SQLite (OLTP) с DuckDB (OLAP) для обработки данных статистического арбитража. Колоночный движок DuckDB, оператор ASOF JOIN и встроенные функции для работы с массивами ускоряют выполнение основных задач, таких как сопоставление котировок со сделками и RWEC, при этом зафиксировано увеличение скорости от 2 до 23 раз по сравнению с SQLite при работе с большими массивами данных. Вы получаете более простые запросы и более быструю аналитику, при этом исполнение операций по-прежнему осуществляется в SQLite.
    Автоматизация торговых стратегий в MQL5 (Часть 25): Советник для торговли по линиям тренда с аппроксимацией методом наименьших квадратов и динамической генерацией сигналов Автоматизация торговых стратегий в MQL5 (Часть 25): Советник для торговли по линиям тренда с аппроксимацией методом наименьших квадратов и динамической генерацией сигналов
    В данной статье мы разрабатываем программу для торговли по линиям тренда, которая использует аппроксимацию методом наименьших квадратов (least squares fit) для определения линий поддержки и сопротивления, генерируя динамические сигналы на покупку и продажу при касании ценой этих линий и открывая позиции по полученным сигналам.
    Разработка инструментария для анализа Price Action (Часть 39): Автоматизация обнаружения BOS и ChOCh на MQL5 Разработка инструментария для анализа Price Action (Часть 39): Автоматизация обнаружения BOS и ChOCh на MQL5
    В этой статье представлена Fractal Reaction System – компактная система на MQL5, которая преобразует фрактальные опорные точки в сигналы рыночной структуры, пригодные для практического применения. Используя логику закрытых баров, чтобы избежать перерисовки, советник предупреждает о смене характера (ChOCh) и подтверждает пробои структуры (BOS), рисует сохраняемые графические объекты, а также ведет журнал и выдает алерты по каждому подтвержденному событию (на десктопных и мобильных устройствах, в том числе со звуком). Ниже разберем проектирование алгоритма, примечания по реализации, результаты тестирования и полный код советника, чтобы вы могли самостоятельно скомпилировать, протестировать и запустить данный инструмент.