English Deutsch 日本語
preview
Разработка инструментария для анализа Price Action (Часть 41): Создание советника для статистического анализа ценовых уровней на MQL5

Разработка инструментария для анализа Price Action (Часть 41): Создание советника для статистического анализа ценовых уровней на MQL5

MetaTrader 5Торговые системы |
223 0
Christian Benjamin
Christian Benjamin

Содержание



Введение

Статистика всегда занимала центральное место в финансовом анализе, поскольку превращает шумные рыночные данные в измеримые и сопоставимые величины. В этой части цикла о Price Action мы применяем те же статистические инструменты непосредственно к свечам: вместо того чтобы рассматривать каждый бар как отдельную единицу информации, мы сводим множество баров к воспроизводимым ценовым уровням и характеристикам распределения, что позволяет интерпретировать недавнее поведение рынка.

Каждую свечу можно охарактеризовать типичной ценой, которая здесь определяется как арифметическое среднее трех ее основных компонентов:

Typical Price

Поэтому использовать TP для расчета среднего, медианы, моды и процентильных уровней принципиально важно: мода выделяет ценовой кластер, в котором рынок проводит больше всего времени, и часто соответствует практическим уровням поддержки или сопротивления; медиана показывает устойчивый центр распределения и выявляет направленные сдвиги, когда цена ее пересекает; среднее и рассчитываемая на его основе z-оценка задают точку равновесия, чувствительную к крупным движениям, и полезны для сигналов с поправкой на волатильность; наконец, процентили (P25/P75) задают границы средних 50% динамики цены (IQR) и помогают отличать плотную консолидацию от широкого разброса. Итак, статистика на основе TP дает опорные уровни, статистически значимые и практически полезные для анализа внутридневной цены.

В этой статье мы покажем, как эти метрики превращаются в практические сигналы, удобные для работы на графике: они становятся горизонтальными опорными линиями (среднее, медиана, уровни P25/P75, уровни моды), основой для порогов, масштабируемых по ATR, которые отличают пробои от разворотов, и базой для сигнального механизма на основе z-оценки, выявляющего необычно экстремальную динамику цены. В предлагаемой реализации (советник KDE Level Sentinel) акцент сделан на воспроизводимости и удобстве использования: снимки фиксируют опорные уровни для последующего мониторинга, метки остаются стабильными и не перекрываются, а сигналы отображаются точными стрелками на графике, отмечающими точную цену срабатывания.

Далее разберем математическую основу каждой метрики, детали реализации на MQL5 и то, как интерпретировать сигналы советника, чтобы перейти от необработанных свечных данных к ясным и проверяемым торговым гипотезам.


Логика стратегии

Как уже отмечалось, мы применяем статистические методы к динамике цены, поэтому для всех статистических расчетов используем типичную цену. Типичная цена (TP) рассчитывается как сумма цены максимума, цены минимума и цены закрытия бара, деленная на три; она уравновешивает торговый диапазон с уровнем закрытия, сглаживает отдельные всплески и дает более устойчивый ряд, чем одна только цена закрытия. Учитывая внутрибарные экстремумы без использования цены открытия, TP дает более содержательные входные данные для статистики распределения – среднего, медианы и оценок ядерной плотности, что повышает стабильность и качество сигнала в последующих моделях. По сравнению с альтернативами, такими как Close, HL/2 или OHLC4, TP занимает разумную середину: этот показатель учитывает и диапазон, и направление, оставаясь компактным и устойчивым рядом данных, хорошо подходящим для статистического анализа Price Action.

Ниже приведены статистические показатели, которые мы получаем из типичной цены (TP). Каждая метрика отражает отдельный аспект поведения цены во времени – от центральной тенденции и разброса до частоты и структуры. 

Метрики, полученные из TP:
  • Среднее (среднее арифметическое)
  • Медиана
  • Мода
  • Стандартное отклонение
  • Дисперсия
  • Диапазон (разница между максимумом и минимумом)
  • Асимметрия и эксцесс (дополнительно)

Давайте разберем их по отдельности, чтобы увидеть, как необработанные свечные данные превращаются в значимые ценовые уровни и сигналы для анализа Price Action.

1. Среднее (среднее арифметическое)

Среднее показывает центральное значение всех типичных цен в выборке. Хотя оно чувствительно к резким всплескам, среднее дает надежное представление о том, возле какого уровня цена в среднем находится. Например, если человек за три месяца получает 1 000, 1 200 и 1 100 долларов, средняя зарплата составляет (1000+1200+1100)/3=1100, что отражает общий уровень дохода. Аналогичным образом, в торговле среднее значение TP указывает на средний уровень цены, вокруг которого колеблется рынок.

Mean

Реализация на MQL5:

double Mean(const double &values[])
{
   double sum = 0.0;
   for(int i=0; i<ArraySize(values); i++)
      sum += values[i];
   return sum / ArraySize(values);
}

2. Медиана

Медиана – это значение, находящееся в середине упорядоченного набора типичных цен. В отличие от среднего, она не подвержена влиянию экстремальных максимумов или минимумов, что делает ее надежной мерой центральной тенденции. Например, при оценках 50, 55, 60, 95 и 100 медиана равна 60, что отражает реальную середину результатов. В торговле медианное значение TP показывает устойчивый центр динамики цены без искажений из-за необычных всплесков.

Median

Реализация на MQL5:

double Median(double &values[])
{
   ArraySort(values, WHOLE_ARRAY, 0, MODE_ASCEND);
   int size = ArraySize(values);
   if(size % 2 == 0)
      return (values[size/2 - 1] + values[size/2]) / 2.0;
   else
      return values[size/2];
}

3. Мода

Мода определяет наиболее часто встречающееся значение в наборе данных и показывает естественную кластеризацию. Например, в группе, где размеры обуви равны 7, 8, 8, 9, 8, 7, 10, 8, 9 и 7, наиболее распространенный размер – 8, то есть мода. Аналогичным образом, в торговле мода TP указывает на ценовые уровни, на которых рынок проводит больше всего времени, и часто совпадает с сильными зонами поддержки или сопротивления.

Mode

Реализация на MQL5:

double Mode(const double &values[])
{
   double mode = values[0];
   int maxCount = 0;

   for(int i=0; i<ArraySize(values); i++)
   {
      int count = 0;
      for(int j=0; j<ArraySize(values); j++)
      {
         if(values[j] == values[i]) count++;
      }
      if(count > maxCount)
      {
         maxCount = count;
         mode = values[i];
      }
   }
   return mode;
}

4. Стандартное отклонение

Стандартное отклонение показывает, насколько значения отклоняются от среднего, отражая степень изменчивости. Рассмотрим двух человек с одинаковым средним количеством шагов в день: один проходит 7 900, 8 000 и 8 100 шагов, а другой – 2 000, 15 000 и 5 000. У обоих среднее значение около 8 000, но у второго разброс значительно больше. В торговле стандартное отклонение TP позволяет отличить спокойные и стабильные рынки от волатильных и нестабильных.

Standard Deviation

Реализация на MQL5:

double StandardDeviation(const double &values[])
{
   double mean = Mean(values);
   double sum = 0.0;
   for(int i=0; i<ArraySize(values); i++)
      sum += MathPow(values[i] - mean, 2);
   return MathSqrt(sum / ArraySize(values));
}

5. Дисперсия

Дисперсия – это квадрат стандартного отклонения, который количественно характеризует разброс значений вокруг среднего в квадратных единицах. Хотя дисперсия менее интуитивна, чем стандартное отклонение, она усиливает крупные отклонения и дает последовательную основу для сравнения волатильности между инструментами. В контексте TP дисперсия показывает, насколько широко разбросаны ценовые уровни, и дает еще один взгляд на стабильность рынка.

Variance

Реализация на MQL5:

double Variance(const double &values[])
{
   double mean = Mean(values);
   double sum = 0.0;
   for(int i=0; i<ArraySize(values); i++)
      sum += MathPow(values[i] - mean, 2);
   return sum / ArraySize(values);
}

6. Диапазон

Диапазон показывает разницу между максимальным и минимальным значениями в наборе данных. Например, если недельная температура колеблется между 20°C и 35°C, диапазон составляет 15°C. В трейдинге диапазон TP показывает ширину движения рынка и помогает быстро отличать узкие консолидации от широких колебаний.

Range

Реализация на MQL5:

double Range(const double &values[])
{
   double minVal = values[ArrayMinimum(values)];
   double maxVal = values[ArrayMaximum(values)];
   return maxVal - minVal;
}

7. Асимметрия или эксцесс

Асимметрия показывает, насколько распределение несимметрично.

double Skewness(const double &values[])
{
   int n = ArraySize(values);
   double mean = Mean(values);
   double sd   = StandardDeviation(values);

   double sum = 0.0;
   for(int i=0; i<n; i++)
      sum += MathPow((values[i] - mean)/sd, 3);

   return (double)n / ((n-1)*(n-2)) * sum;
}

В компании, где большинство сотрудников зарабатывают 3 000 долларов, а генеральный директор – 50 000 долларов, средняя зарплата смещается вверх, создавая положительную асимметрию. Аналогичным образом, асимметрия в TP показывает, были ли цены сильнее смещены к верхним или нижним экстремумам, и сигнализирует о направленных дисбалансах в структуре рынка.

Skewness

Эксцесс характеризует "тяжесть хвостов" распределения, то есть вероятность экстремальных значений. На шоссе, если большинство автомобилей движутся со скоростью от 60 до 70 км/ч, эксцесс низкий. Если автомобили обычно движутся в этом диапазоне, но иногда ползут со скоростью 20 км/ч или разгоняются до 150 км/ч, эксцесс высокий.

double Kurtosis(const double &values[])
{
   int n = ArraySize(values);
   double mean = Mean(values);
   double sd   = StandardDeviation(values);

   double sum = 0.0;
   for(int i=0; i<n; i++)
      sum += MathPow((values[i] - mean)/sd, 4);

   return ((double)n*(n+1) / ((n-1)*(n-2)*(n-3))) * sum
          - (3.0*MathPow(n-1,2) / ((n-2)*(n-3)));
}

В торговле высокий эксцесс TP указывает на рынки, которые большую часть времени спокойны, но склонны к внезапным резким движениям.

Kurtosis

8. Процентили (P25 и P75)

Процентили делят набор данных на ранжированные позиции, позволяя понять, где именно значения находятся внутри распределения. 25-й процентиль (P25) обозначает точку, ниже которой находятся 25% типичных цен, а 75-й процентиль (P75) – уровень, ниже которого находятся 75% цен. Вместе эти два значения образуют межквартильный размах (IQR), который охватывает средние 50% всех наблюдений.

double p25 = Percentile(values, 0.25);
double p75 = Percentile(values, 0.75);

Print("P25 = ", DoubleToString(p25, _Digits), 
      " | P75 = ", DoubleToString(p75, _Digits));

В контексте трейдинга, P25 выделяет "нижний кластер" ценовой активности и часто показывает, где покупатели стабильно проявляют активность, тогда как P75 выделяет "верхний кластер", где обычно доминируют продавцы. В совокупности эти границы дают более точное представление о рыночной концентрации, чем одно только среднее или одна только медиана. Узкий межквартильный размах (IQR) указывает на консолидацию, тогда как широкий IQR свидетельствует о большем рыночном разбросе.

Логика генерации сигнала основана на z-оценке для последней типичной цены, вычисляемой как (TP - mean) / stddev в пределах текущего статистического окна. Когда это стандартизированное отклонение выходит за пределы настроенного порога входа (ZScoreSignalEnter), формируется сигнал на покупку, если цена находится достаточно ниже среднего (отрицательная z-оценка), или сигнал на продажу, если она значительно выше (положительная z-оценка). Сигналы подтверждаются только при включенных настройках AllowLongSignals или AllowShortSignals. После перехода в состояние сигнала оно сохраняется, пока z-оценка не вернется в пределы заданной зоны выхода (ZScoreSignalExit); только после этого сигнал снимается и при необходимости формируется алерт о развороте. Каждый переход сигнала приводит к вызову функции EmitAlertWithArrow, которая рисует на графике направленную стрелку и, в зависимости от пользовательских настроек, может также выдавать всплывающее оповещение, звуковой сигнал или push-уведомление.


Разбор кода

В этом разделе рассматриваются детали реализации KDE Level Sentinel.mq5 с акцентом на архитектуру, поток данных, основные алгоритмы и важные вспомогательные процедуры для графического оформления и мониторинга уровней. Материал организован так, чтобы читателю было проще сопоставить концептуальную схему с соответствующими разделами кода и параметрами конфигурации.

Конфигурация и инициализация

Все настраиваемые пользователем параметры объявлены в верхней части исходного кода как входные параметры. К ним относятся окно анализа (Lookback), опция исключения текущего формирующегося бара, настройки KDE и гистограммы (ModeBins, KDEGridPoints, KDEBandwidthFactor), пороги z-оценки для сигналов, параметры снимков и мониторинга (AutoSnapshotLevels, MonitorBars, TouchTolerancePips, BreakoutPips, ReversalPips, UseATRforThresholds), а также интервалы обновления интерфейса и очистки (TimerIntervalSeconds, CleanupIntervalSeconds). Этот раздел служит панелью управления советника: изменение любого входного параметра меняет его статистическую конфигурацию и поведение при мониторинге без правки кода.

// ---------- user inputs (control panel) ----------
input int    Lookback               = 1000;
input bool   ExcludeCurrent         = true;
input bool   UseWeightedByVol       = true;
input int    ModeBins               = 30;
input int    KDEGridPoints          = 100;
input double KDEBandwidthFactor     = 1.0;
input bool   DrawHistogramOnChart   = false;
input int    RefreshEveryXTicks     = 1;
input double ZScoreSignalEnter      = 2.0;
input double ZScoreSignalExit       = 0.8;
input bool   AutoSnapshotLevels     = true;
input int    MonitorBars            = 20;
input double TouchTolerancePips     = 3.0;
input bool   UseATRforThresholds    = true;
input double ATRMultiplier          = 0.5;
input int    ATRperiod              = 14;
input int    TimerIntervalSeconds   = 60;
input int    CleanupIntervalSeconds = 3600;

// ---------- OnInit (build names, cleanup, placeholders, start timer) ----------
int OnInit()
  {
   S_base = StringFormat("CSTATS_%s_%d", _Symbol, (int)TF);
   S_mean = S_base + "_MEAN";
   S_p25  = S_base + "_P25";
   // remove leftovers from previous runs
   RemoveExistingEAObjects();
   // create panel + placeholder HLINEs
   CreatePanel();
   CreateHLine(S_mean, 0.0, clrBlack, 2);
   CreateHLine(S_p25,  0.0, clrTeal,  1);
   // optionally clear previous snapshot
   if(ClearSnapshotOnStart) ClearSnapshot();
   // start periodic timer for housekeeping
   EventSetTimer(TimerIntervalSeconds);
   return(INIT_SUCCEEDED);
  }

Во время OnInit формируются единые префиксы имен объектов и глобальных переменных через S_base = StringFormat("CSTATS_%s_%d", _Symbol, (int)TF). Такая детерминированная схема именования защищает несколько экземпляров на разных графиках от случайных конфликтов и централизует очистку. Далее инициализация удаляет оставшиеся объекты от предыдущих экземпляров, создает компактную угловую панель (сводные метки), размещает горизонтальные линии-заглушки для каждой ключевой статистики (среднее, ±SD, P25/P75, медиана, обе моды), при необходимости очищает предыдущие снимки и запускает периодический таймер для служебных задач.

Сбор данных и основной цикл

Основные вычисления выполняются в OnTick; частота их запуска ограничивается параметром RefreshEveryXTicks, чтобы избежать чрезмерной нагрузки на ЦП при частых тиках. Процедура копирует бары Lookback с настроенного таймфрейма в rates[] через CopyRates, используя start = ExcludeCurrent ? 1 : 0, чтобы при необходимости исключить формирующуюся свечу. На основе rates[] формируются массивы типичных цен vals[] = (high + low + close) / 3.0 и тиковых объемов vols[] (если включено взвешивание по объему).

void OnTick()
  {
   tick_count++;
   if(tick_count < RefreshEveryXTicks) return;
   tick_count = 0;

   int start = ExcludeCurrent ? 1 : 0;
   int needed = Lookback;
   if(Bars(_Symbol, TF) - start < needed) return;

   MqlRates rates[];
   int copied = CopyRates(_Symbol, TF, start, needed, rates);
   if(copied <= 0) { Print("CopyRates failed: ", GetLastError()); return; }

   double vals[], vols[];
   ArrayResize(vals, copied);
   ArrayResize(vols, copied);
   for(int i = 0; i < copied; i++)
     {
      vals[i] = (rates[i].high + rates[i].low + rates[i].close) / 3.0; // typical price
      vols[i] = (double)rates[i].tick_volume;
     }

   // pass vals/vols into statistics routines...
  }

Сразу после этого выполняются статистические расчеты: арифметическое среднее, при необходимости среднее, взвешенное по объему, выборочные дисперсия и стандартное отклонение, медиана, 25-й и 75-й процентили, мода по бинам (ModeBinned) и мода на основе KDE (ModeKDE). Для параметра сглаживания KDE используется правило, близкое к правилу Сильвермана: h = 1.06 * sd * n^-0.2, дополнительно масштабируемое коэффициентом KDEBandwidthFactor; затем плотность оценивается на равномерной сетке из KDEGridPoints, и возвращается точка сетки с максимальной оцененной плотностью. Для последней типичной цены (latest) вычисляется z-оценка по формуле (latest - mean) / stddev, на основе которой формируются простые сигналы входа и выхода.
Вычисленные статистические значения отображаются на графике в виде горизонтальных линий и текстовых меток, а также экспортируются в виде глобальных переменных с ключами, начинающимися с S_base, чтобы другие скрипты и индикаторы могли считывать их в совместимом формате.

Снимки и мониторинг опорных уровней

Когда включен AutoSnapshotLevels, фиксируется один снимок: сохраняются текущие оценки уровней (среднее, среднее ± стандартное отклонение, P25, P75, медиана, мода) и создается массив refLevels[], состоящий из структур RefLevel. Каждый RefLevel содержит поля name, price, touched, touchTime, monitorLeft, highest, lowest, result (0 означает "неизвестно", 1 – пробой, -1 – разворот, 2 – отсутствие продолжения) и resolvedTime. Линии HLINE для снимков именуются по шаблону S_base + "_REF_" + name и либо обновляют канонические объекты TXT (если каноническая метка уже существует), либо используют отдельные метки для REF.

// take snapshot (single-shot)
void SnapshotReferenceLevels(double mean_val, double p25, double p75, double median_val, double mode_b, double mode_k)
  {
   snapshot_mean = mean_val;
   snapshot_p25  = p25;
   snapshot_p75  = p75;
   snapshot_median= median_val;
   // build refLevels
   ArrayResize(refLevels, 6);
   refLevels[0].name = "MEAN"; refLevels[0].price = snapshot_mean; refLevels[0].touched=false; refLevels[0].result=0;
   // ... fill others ...
   refSnapshotTaken = true;
   snapshotTakenTime = TimeCurrent();
  }

// monitor reference levels (called from OnTick)
void MonitorReferenceLevels(const MqlRates &rates[], int copied)
  {
   if(!refSnapshotTaken || copied <= 0) return;
   double barHigh = rates[0].high;
   double barLow  = rates[0].low;
   double barClose= rates[0].close;
   double pipPoints = pipToPointMultiplier();
   double touchTol = TouchTolerancePips * pipPoints;
   // compute thresholds (fixed or ATR-scaled)
   double breakoutThreshold = BreakoutPips * pipPoints;
   if(UseATRforThresholds)
     {
      int hATR = iATR(_Symbol, TF, ATRperiod);
      double atrBuf[];
      CopyBuffer(hATR,0,0,1,atrBuf);
      IndicatorRelease(hATR);
      breakoutThreshold = atrBuf[0] * ATRMultiplier;
     }

   for(int i=0;i<ArraySize(refLevels);i++)
     {
      RefLevel L = refLevels[i];
      if(L.result != 0) continue;
      if(!L.touched)
        {
         if(barHigh >= L.price - touchTol && barLow <= L.price + touchTol)
           {
            L.touched = true;
            L.touchTime = rates[0].time;
            L.monitorLeft = MonitorBars;
            L.highest = barHigh; L.lowest = barLow;
            refLevels[i] = L;
           }
        }
      else
        {
         // update highest/lowest and evaluate breakout/reversal
         if(barHigh > L.highest) L.highest = barHigh;
         if(barLow  < L.lowest)  L.lowest  = barLow;
         bool breakout = (L.highest >= L.price + breakoutThreshold);
         bool reversal = (L.lowest  <= L.price - breakoutThreshold);
         if(breakout && !reversal) { L.result = 1; L.resolvedTime = rates[0].time; DrawOutcome(L, true); }
         else if(reversal && !breakout) { L.result = -1; L.resolvedTime = rates[0].time; DrawOutcome(L, false); }
         else { if(--L.monitorLeft <= 0) { L.result = 2; L.resolvedTime = rates[0].time; DrawOutcome(L, false); } }
         refLevels[i] = L;
        }
     }
  }

Мониторинг реализован в MonitorReferenceLevels. Для каждого опорного уровня, который еще не получил итоговой оценки, максимум, минимум и цена закрытия последнего бара сравниваются с допуском касания (TouchTolerancePips, который переводится в ценовые единицы функцией pipToPointMultiplier()). При касании monitorLeft принимает значение MonitorBars, а исходные максимум и минимум фиксируются. В течение окна мониторинга отслеживаются максимальная и минимальная цены, а также оцениваются условия пробоя и разворота. Пороги задаются либо фиксированными значениями в пипсах, либо вычисляются на основе ATR, если UseATRforThresholds включен; ATR получают через iATR + CopyBuffer и масштабируют с помощью ATRMultiplier, чтобы получить адаптивные пороги. Поддерживается также необязательное подтверждение по закрытию бара (UseCloseForConfirm). Результат фиксируется либо сразу после подтверждения, либо в конце окна мониторинга – путем сравнения наблюдаемых экстремумов с порогами; затем он записывается и отображается через DrawOutcome.

Визуальные элементы и размещение меток

Приоритет отдан стабильным и не перекрывающимся аннотациям на графике. Для статистики и опорных уровней снимка горизонтальные линии создаются через CreateHLine; если объект уже существует, он обновляется, а затем для него устанавливается временная метка в метаданных. Текстовые метки обрабатываются функцией CreateOrUpdateLineText, которая:

void CreateOrUpdateLineText(string name, datetime t, double price, string text)
  {
   long chart_id = ChartID();
   int x0 = 0, y0 = 0;
   bool ok = ChartTimePriceToXY(chart_id, 0, t, price, x0, y0);
   int fontSize = 10;
   int pixelThresh = MathMax(18, fontSize * 2);

   // collect used Y positions from existing OBJ_TEXT objects
   int usedYPositions[];
   ArrayResize(usedYPositions,0);
   int total = ObjectsTotal(0);
   for(int oi=0; oi<total; oi++)
     {
      string oname = ObjectName(0, oi);
      if(oname == name) continue;
      if(ObjectGetInteger(0, oname, OBJPROP_TYPE) != OBJ_TEXT) continue;
      long ot = (long)ObjectGetInteger(0, oname, OBJPROP_TIME);
      double op = ObjectGetDouble(0, oname, OBJPROP_PRICE);
      int xp=0, yp=0;
      if(ChartTimePriceToXY(chart_id,0,(datetime)ot,op,xp,yp))
        {
         ArrayResize(usedYPositions, ArraySize(usedYPositions)+1);
         usedYPositions[ArraySize(usedYPositions)-1] = yp;
        }
     }

   // attempt to find free Y slot
   int chosenY = y0;
   if(!IsYFree(chosenY, usedYPositions, pixelThresh))
     {
      int step = pixelThresh;
      bool found = false;
      for(int s=1; s<=20 && !found; s++)
        {
         int yUp = y0 - s*step;
         if(yUp >= 0 && IsYFree(yUp, usedYPositions, pixelThresh)) { chosenY = yUp; found = true; break; }
         int yDn = y0 + s*step;
         if(IsYFree(yDn, usedYPositions, pixelThresh)) { chosenY = yDn; found = true; break; }
        }
     }

   // convert chosen XY back to time/price; fallback if needed
   datetime tt = t; double pp = price;
   if(!ChartXYToTimePrice(chart_id, 0, x0, chosenY, tt, pp))
     { // fallback: nudge price slightly
      double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
      int slotDelta = (chosenY - y0) / pixelThresh;
      pp = price + slotDelta * pixelThresh * point;
     }

   // create or update the OBJ_TEXT
   KeepSingleTextLabel(name);
   if(ObjectFind(0, name) >= 0)
     {
      ObjectSetInteger(0, name, OBJPROP_TIME, (long)tt);
      ObjectSetDouble(0, name, OBJPROP_PRICE, pp);
      ObjectSetString(0, name, OBJPROP_TEXT, text);
     }
   else
     {
      ObjectCreate(0, name, OBJ_TEXT, 0, tt, pp);
      ObjectSetString(0, name, OBJPROP_TEXT, text);
     }
   SetObjTimestamp(name);
  }

Пытается сопоставить целевые time+price с экранными координатами через ChartTimePriceToXY.

Собирает Y-координаты существующих объектов OBJ_TEXT (и канонических TXT-меток статистики), чтобы новые метки не пересекались.

Выполняет поиск вверх и вниз с шагом в пикселях (он определяется размером шрифта), чтобы найти свободную позицию (IsYFree), после чего преобразует выбранный XY-слот обратно в time+price через ChartXYToTimePrice. Если преобразование не удается, используется надежный резервный механизм: положение корректируется приблизительно, чтобы избежать сбоев.

Обеспечивает наличие одного объекта _TXT на каждую HLINE с помощью KeepSingleTextLabel.

Этот подход дает читаемые метки с минимальным дрожанием при прокрутке графика или изменении его размера. Сигнальные стрелки и маркеры результатов создаются функцией DrawArrowAt, которая также рисует тонкую и точную HLINE на цене стрелки, чтобы сохранить точное выравнивание.

Гистограмма и KDE

DrawHistogram вычисляет частотную гистограмму типичных цен по ModeBins, рисует линии HLINE в центрах интервалов с шириной, пропорциональной частоте, и создает компактные метки с количеством наблюдений. ModeBinned вычисляет быструю оценку моды, возвращая центр наиболее заполненного интервала. ModeKDE дает более сглаженную оценку моды, выполняя "наивную" оценку ядерной плотности по заданной пользователем сетке; ее вычислительная сложность составляет O(n × gridPts), поэтому параметр нужно подбирать с учетом выбранного Lookback и внутрибарной частоты обновлений.

// histogram drawing (bin counts => HLINE widths)
void DrawHistogram(const double &arr[], int n, int bins, int maxWidth)
  {
   double minv = ArrayMin(arr, n);
   double maxv = ArrayMax(arr, n);
   double binw = (maxv - minv) / bins;
   int counts[]; ArrayResize(counts, bins); ArrayInitialize(counts,0);
   for(int i=0;i<n;i++)
     {
      int b = (int)MathFloor((arr[i]-minv)/binw);
      if(b < 0) b=0; if(b >= bins) b=bins-1;
      counts[b]++;
     }
   // draw HLINE per bin with width proportional to counts[b]
  }

// KDE-based modal estimate
double ModeKDE(const double &a[], int n, int gridPts, double bwFactor)
  {
   double mn = ArrayMin(a,n), mx = ArrayMax(a,n);
   double sd = MathSqrt(Variance(a, n, false));
   double h = 1.06 * sd * MathPow((double)n, -0.2);
   if(h <= 0) h = (mx - mn) / 20.0;
   h *= bwFactor;
   double bestX = mn, bestD = -1.0;
   const double SQRT2PI = 2.5066282746310002;
   for(int g=0; g<gridPts; g++)
     {
      double x = mn + (double)g/(gridPts-1) * (mx - mn);
      double s = 0.0;
      for(int i=0;i<n;i++)
        {
         double u = (x - a[i]) / h;
         s += MathExp(-0.5 * u * u);
        }
      double dens = s / (n * h * SQRT2PI);
      if(dens > bestD) { bestD = dens; bestX = x; }
     }
   return(bestX);
  }

Метаданные и управление жизненным циклом

Каждый созданный объект графика связывается с временной меткой в метаданных, которая хранится в глобальной переменной с ключом S_base + "_META_" + objectName через SetObjTimestamp. Это позволяет функции RemoveOldObjects (вызываемой в OnTimer) просматривать список потенциальных объектов и удалять те, чей возраст превышает CleanupIntervalSeconds, чтобы не засорять график при длительной работе. RemoveExistingEAObjects и CleanupAllMetaGlobals обеспечивают управляемые инициализацию и завершение работы, удаляя ранее созданные объекты и их метаданные.

// store timestamp meta
void SetObjTimestamp(string name)
  {
   string g = S_base + "_META_" + name;
   GlobalVariableSet(g, (double)TimeCurrent());
  }

// read meta timestamp
datetime GetObjTimestamp(string name)
  {
   string g = S_base + "_META_" + name;
   if(GlobalVariableCheck(g)) return (datetime)GlobalVariableGet(g);
   return 0;
  }

// remove objects older than ageSec
void RemoveOldObjects(int ageSec)
  {
   datetime now = TimeCurrent();
   string candidates[] = { S_mean, S_mean + "_TXT", S_p25, S_p25 + "_TXT", S_panel /* ... */ };
   for(int j=0;j<ArraySize(candidates);j++)
     {
      string nm = candidates[j];
      datetime ts = GetObjTimestamp(nm);
      if(ts == 0) continue;
      if((int)(now - ts) >= ageSec)
        {
         if(ObjectFind(0, nm) >= 0) ObjectDelete(0, nm);
         string g = S_base + "_META_" + nm;
         if(GlobalVariableCheck(g)) GlobalVariableDel(g);
        }
     }
  }

Алерты и обработка сигналов

Сигналы на основе z-оценки оцениваются на каждом тике с учетом настроек AllowLongSignals / AllowShortSignals. Порог входа задается параметром ZScoreSignalEnter, а порог выхода – параметром ZScoreSignalExit. При смене состояния сигнала функция EmitAlertWithArrow рисует стрелку на графике и при необходимости запускает платформенные алерты (Alert()), звуки (PlaySound()) и push-уведомления (SendNotification()), одновременно удаляя стрелки противоположных сигналов, чтобы снизить визуальный шум на графике.

// z-score signal logic (called from OnTick after stats computed)
int newSig = currentSignal;
if(zscore >= ZScoreSignalEnter && AllowLongSignals) newSig = 1;
else if(zscore <= -ZScoreSignalEnter && AllowShortSignals) newSig = -1;
else if(currentSignal == 1 && zscore < ZScoreSignalExit) newSig = 0;
else if(currentSignal == -1 && zscore > -ZScoreSignalExit) newSig = 0;

if(newSig != currentSignal)
  {
   if(newSig == 1)
     EmitAlertWithArrow("CSTATS LONG " + _Symbol + " z=" + DoubleToString(zscore,3), t_now, latest, true, S_arrow_long);
   else if(newSig == -1)
     EmitAlertWithArrow("CSTATS SHORT " + _Symbol + " z=" + DoubleToString(zscore,3), t_now, latest, false, S_arrow_short);
   else // clear arrows on exit
     {
      if(ObjectFind(0, S_arrow_long) >= 0) ObjectDelete(0, S_arrow_long);
      if(ObjectFind(0, S_arrow_short) >= 0) ObjectDelete(0, S_arrow_short);
     }
   currentSignal = newSig;
  }

// emit alert helper
void EmitAlertWithArrow(string message, datetime when, double price, bool isBuy, string arrowName)
  {
   DrawArrowAt(arrowName, when, price, isBuy);
   if(SendAlertOnSignal) Alert(message);
   if(PlaySoundOnSignal) PlaySound(SoundFileOnSignal);
   if(SendPushOnSignal) SendNotification(message);
  }

Вспомогательные статистические функции и численные нюансы

Код реализует стандартную статистику на основе массивов: среднее, взвешенное среднее, выборочную дисперсию, медиану (включая усреднение для четного числа элементов) и процентиль с линейной интерполяцией – с учетом семантики массивов MQL5. Вспомогательные процедуры при необходимости вычисляют асимметрию и эксцесс для дальнейшего расширения. На практике разрешение KDE (KDEGridPoints) и полосу пропускания (KDEBandwidthFactor) нужно подбирать как компромисс между сглаживанием, точностью и нагрузкой на CPU. Создание и освобождение хэндла ATR выполняется при каждой оценке; при высокочастотном выполнении рекомендуется повторно использовать хэндлы индикатора или вычислять ATR только на новом баре, чтобы снизить накладные расходы.

double Mean(const double &a[], int n)
  {
   if(n<=0) return 0.0;
   double s=0.0;
   for(int i=0;i<n;i++) s += a[i];
   return s / n;
  }

double WeightedMean(const double &a[], const double &w[], int n)
  {
   if(n<=0) return 0.0;
   double sw=0.0, s=0.0;
   for(int i=0;i<n;i++) { s += a[i] * w[i]; sw += w[i]; }
   if(sw == 0.0) return Mean(a, n);
   return s / sw;
  }

double Variance(const double &a[], int n, bool sample)
  {
   if(n <= 1) return 0.0;
   double mu = Mean(a,n), s = 0.0;
   for(int i=0;i<n;i++) { double d = a[i] - mu; s += d * d; }
   return sample ? s / (n-1) : s / n;
  }

double Median(const double &a[], int n)
  {
   if(n <= 0) return 0.0;
   double tmp[]; ArrayResize(tmp, n); ArrayCopy(tmp, a); ArraySort(tmp);
   if((n % 2) == 1) return tmp[n/2];
   return (tmp[n/2 - 1] + tmp[n/2]) / 2.0;
  }

double Percentile(const double &a[], int n, double q)
  {
   if(n <= 0) return 0.0;
   double tmp[]; ArrayResize(tmp, n); ArrayCopy(tmp, a); ArraySort(tmp);
   if(q <= 0.0) return tmp[0];
   if(q >= 1.0) return tmp[n-1];
   double idx = q * (n - 1);
   int i0 = (int)MathFloor(idx); double frac = idx - i0;
   if(i0 + 1 < n) return tmp[i0] * (1.0 - frac) + tmp[i0+1] * frac;
   return tmp[i0];
  }


Результаты

В этом разделе рассматривается работа советника на графике. Очень важно тщательно протестировать систему как в тестировании на исторических данных, так и на демо-счете, прежде чем использовать ее с реальным капиталом, поскольку сигналы в реальном времени могут подтолкнуть даже дисциплинированных трейдеров к преждевременным сделкам. Советник выводит компактную панель метрик и рисует горизонтальные опорные линии для каждой рассчитанной статистики; каждая линия сопровождается текстовой меткой, поясняющей ее смысл (среднее, медиана, моды, P25/P75 и т.д.). Эти визуальные подсказки позволяют легко оценить, как цена взаимодействует со статистически значимыми уровнями, и проверить поведение советника в исторических и форвардных тестах.

На рисунке ниже показаны статистическая панель советника и соответствующие горизонтальные уровни, нанесенные на Step Index (M5). Среднее и взвешенное среднее сходятся на уровне 8114,8, задавая устойчивую центральную точку равновесия. Стандартное отклонение (15,6) указывает на умеренную волатильность в текущей выборке, тогда как медиана (8113,3) остается близкой к среднему, что отражает симметричное распределение цен. И дискретная мода (8112,4), и мода, оцененная по KDE (8113,2), находятся чуть ниже среднего значения и обозначают плотную торговую зону, которая выступает естественной областью поддержки/сопротивления. Процентили (P25 = 8105,7, P75 = 8121,6) охватывают межквартильный размах в 16 пунктов, тем самым задавая средние 50% активности и выделяя зону консолидации, в которой цена колеблется чаще всего. Наконец, z-оценка (2,676) показывает, что цена поднялась более чем на два стандартных отклонения выше среднего, что указывает на временную перерастянутость и увеличивает вероятность возврата к среднему.

В совокупности эти результаты показывают, как статистические уровни, полученные из типичной цены, могут служить практическими опорными уровнями. Отображение советника на графике позволяет трейдерам визуально оценить, как рынок реагирует на эти зоны, отвергает их или уходит далеко за их пределы, что помогает опираться на объективно измеряемую структуру рынка, а не на субъективные оценки.


Заключение

Советник рассчитывает опорные уровни распределения на основе типичной цены (TP = (H+L+C)/3) и выводит их прямо на график: подписанные горизонтальные линии (среднее, взвешенное среднее, медиана, моды (по бинам и по KDE), P25/P75), компактную панель метрик и визуальные сигналы (стрелки, маркеры касаний и результатов). Он также экспортирует глобальные переменные для программного использования и включает логику мониторинга, которая фиксирует касания и классифицирует результаты как пробой, разворот или отсутствие продолжения.

Эти уровни переводят кластеризацию, центральную тенденцию и разброс в объективные опорные точки на графике, которые можно быстро интерпретировать по графику. Их можно рассматривать как статистическую карту недавнего поведения цены. Используйте их как опору для принятия решений: сначала подтверждайте, как цена взаимодействует с этими зонами, и только потом действуйте; выводы советника рассматривайте как контекст, а не как команду на автоматическое исполнение сделки. В следующих проектах мы рассмотрим более продвинутые статистические методы и ансамблевые техники, чтобы повысить стабильность уровней и прогностическую силу модели.

Читайте другие мои статьи здесь.

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/19589

Прикрепленные файлы |
Архитектура системы машинного обучения в MetaTrader 5 (Часть 4): Скрытый изъян пайплайна финансового ML — одновременность меток Архитектура системы машинного обучения в MetaTrader 5 (Часть 4): Скрытый изъян пайплайна финансового ML — одновременность меток
Узнайте, как исправить критический изъян в финансовом машинном обучении, который приводит к переобученным моделям и плохой работе в реальной торговле, — одновременность меток. При использовании метода тройного барьера (triple-barrier) обучающие метки перекрываются во времени, нарушая базовое предположение IID большинства ML-алгоритмов (алгоритмов машинного обучения). В статье показано практическое решение через взвешивание наблюдений: как измерять временное перекрытие торговых сигналов, рассчитывать взвешивание наблюдений с учётом уникальной информации и применять эти веса в scikit-learn для построения более устойчивых классификаторов. Освоение этих техник поможет сделать торговые модели более устойчивыми, надёжными и прибыльными.
Двумерные копулы в MQL5 (Часть 1): Реализация гауссовой копулы и t-копулы Стьюдента для моделирования зависимостей Двумерные копулы в MQL5 (Часть 1): Реализация гауссовой копулы и t-копулы Стьюдента для моделирования зависимостей
Это первая часть серии статей, посвящённых реализации двумерных копул в MQL5. В статье представлен код, реализующий гауссову копулу и t-копулу Стьюдента. Также рассматриваются основы статистических копул и связанные с ними темы. Код основан на Python-пакете ArbitrageLab от Hudson and Thames.
Кодекс рыночных состояний в MQL5 (Часть 1): Побитовое обучение на примере Nvidia Кодекс рыночных состояний в MQL5 (Часть 1): Побитовое обучение на примере Nvidia
Мы начинаем новую серию статей, которая развивает наши предыдущие наработки, изложенные в серии о MQL5 Wizard, и продвигает их дальше по мере усиления нашего подхода к системной торговле и тестированию стратегий. В этой новой серии мы сосредоточимся на советниках, запрограммированных на удержание только одного типа позиций — преимущественно длинных. Сосредоточение на одном направлении торговли может упростить анализ, снизить сложность стратегии и дать важные наблюдения, особенно при работе с активами за пределами Forex. Поэтому в этой серии мы исследуем, эффективен ли такой подход для акций и других невалютных активов, где long-only-системы часто хорошо согласуются с подходом smart money и стратегиями институциональных участников.
Автоматизация торговых стратегий в MQL5 (Часть 25): Советник для торговли по линиям тренда с аппроксимацией методом наименьших квадратов и динамической генерацией сигналов Автоматизация торговых стратегий в MQL5 (Часть 25): Советник для торговли по линиям тренда с аппроксимацией методом наименьших квадратов и динамической генерацией сигналов
В данной статье мы разрабатываем программу для торговли по линиям тренда, которая использует аппроксимацию методом наименьших квадратов (least squares fit) для определения линий поддержки и сопротивления, генерируя динамические сигналы на покупку и продажу при касании ценой этих линий и открывая позиции по полученным сигналам.