Создание индикатора ZigZag со статистикой волн в MQL5
Индикатор количественного анализа волн ZigZag: статистика вместо интуиции
Подавляющее большинство трейдеров использует классический ZigZag чисто визуально: посмотрел на график, на глаз прикинул, что волна большая или маленькая, принял решение. Никакой количественной базы под этим нет — ZigZag остаётся картинкой, а не измерительным инструментом.
Индикатор, о котором пойдёт речь в этой статье, решает именно эту задачу: он не просто рисует зигзаг, а параллельно считает статистику по длинам волн — отдельно для восходящих и нисходящих движений. На выходе трейдер получает конкретные числа: текущую длину волны, максимум, минимум, среднее и медиану. Все значения — в пунктах и обновляются на каждом новом баре без перегрузки терминала.
В статье разберу код целиком: как находятся экстремумы в скользящем окне, как конечный автомат собирает их в чередующуюся последовательность и как накапливается статистика. Отдельно покажу отрисовку подписанных пивотов на графике и разберу, где у такого подхода есть слабые места и как их можно закрыть в развитии индикатора.
Почему один зигзаг — это мало
У обычного ZigZag есть системная проблема восприятия: человеческий глаз плохо оценивает абсолютную длину отрезков на графике, особенно при разных масштабах и таймфреймах. Волна в 150 пунктов на EURUSD и на GBPJPY визуально выглядит по-разному из-за разного ценового диапазона на экране. Поэтому трейдер обычно сравнивает её с недавними волнами на том же графике, а не с объективной историей инструмента.
Отдельная проблема — асимметрия движений, которую на глаз почти невозможно заметить за разумное время наблюдения. Если медвежьи волны на инструменте в среднем длиннее бычьих на 20%, эту цифру не увидеть, просто глядя на зигзаг день за днём — нужен именно подсчёт по всей доступной истории. Индикатор в этой статье закрывает оба пробела: он переводит визуальную оценку в число и накапливает статистику по всей видимой на графике истории, а не только по последним нескольким волнам, которые случайно попали в поле зрения.
Почему медиана, а не только среднее
Среднее арифметическое чувствительно к выбросам. Если рынок один раз дал аномально длинный импульс (например, на новостях), среднее по выборке будет заметно искажено. Эффект сохранится надолго, пока новые обычные волны не снизят вклад выброса.
Медиана устойчива к выбросам по определению — она показывает типичную волну, которая реально происходит на инструменте чаще всего. Расхождение между средним и медианой — уже само по себе диагностический сигнал: чем оно больше, тем сильнее в истории присутствуют редкие, но мощные движения. Разберём это на условном числовом примере из восьми бычьих волн одного инструмента:
| № волны | Длина, пункты |
|---|---|
| 1 | 120 |
| 2 | 145 |
| 3 | 98 |
| 4 | 160 |
| 5 | 132 |
| 6 | 1240 |
| 7 | 110 |
| 8 | 155 |
Среднее по этой выборке — 270 пунктов, что выглядит как типичная волна почти в два раза длиннее любой реально произошедшей, кроме одного новостного выброса на волне №6. Медиана по той же выборке — 138,5 пункта, что гораздо честнее отражает то, с чем трейдер реально сталкивается в девяти случаях из десяти. Именно поэтому индикатор считает обе величины параллельно и не подменяет одну другой.
Архитектура: буферы данных и буфер отрисовки
Ключевое архитектурное решение — три буфера при одном отображаемом графическом объекте:
//+------------------------------------------------------------------+ //| Буферы индикатора: один отображаемый + два служебных | //+------------------------------------------------------------------+ #property indicator_buffers 3 #property indicator_plots 1 double ZigZagBuffer[]; // финальные значения ZigZag (отображаемая линия) double HighMapBuffer[]; // обнаруженные хаи (служебный буфер) double LowMapBuffer[]; // обнаруженные лои (служебный буфер)
HighMapBuffer и LowMapBuffer объявлены как INDICATOR_CALCULATIONS — они участвуют в расчёте, но не рисуются и не засоряют DataWindow. Сначала кандидаты на экстремумы записываются в служебные буферы, затем конечный автомат выбирает из них финальную альтернирующую последовательность хай-лоу-хай-лоу. Такое разделение — не украшательство, а прямое следствие того, что промежуточные кандидаты и финальный зигзаг живут в разных системах координат: кандидат может быть отменён backstep-механизмом на несколько баров позже своего появления, а финальная линия должна оставаться неизменной после того, как она отрисована.
Инициализация индикатора связывает буферы и настраивает поведение линии на пустых значениях:
//+------------------------------------------------------------------+ //| Инициализация | //+------------------------------------------------------------------+ int OnInit() { //--- привязка буферов: один рисуемый, два для внутренних расчётов SetIndexBuffer(0, ZigZagBuffer, INDICATOR_DATA); SetIndexBuffer(1, HighMapBuffer, INDICATOR_CALCULATIONS); SetIndexBuffer(2, LowMapBuffer, INDICATOR_CALCULATIONS); //--- нули не должны отображаться как точки линии PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, 0.0); PlotIndexSetInteger(0, PLOT_SHIFT, 0); //--- определяем тестер/оптимизацию: там поканальная отрисовка объектов //--- и комментарий не нужны и замедляют валидатор Маркета g_in_tester = (bool)MQLInfoInteger(MQL_TESTER) || (bool)MQLInfoInteger(MQL_OPTIMIZATION) || (bool)MQLInfoInteger(MQL_VISUAL_MODE); IndicatorSetString(INDICATOR_SHORTNAME, "Quantitative Analysis Indicator"); lastBars = 0; // принудительный полный проход отрисовки return(INIT_SUCCEEDED); }
Обратная сторона инициализации — деинициализация, которая аккуратно убирает за индикатором графический мусор при его удалении с графика или смене таймфрейма:
//+------------------------------------------------------------------+ //| Деинициализация: очистка графика | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { ObjectsDeleteAll(0, OBJ_PREFIX); // удаляем все свои объекты Comment(""); // очищаем комментарий ChartRedraw(); }
Поиск экстремума в скользящем окне
Прежде чем разбирать конечный автомат, стоит остановиться на двух вспомогательных функциях, которые находят индекс максимума и минимума в окне заданной глубины назад от текущего бара. Это не готовые функции терминала, а собственная реализация под нужды индикатора — без использования индексации таймсерий, что важно при работе с массивами, переданными в OnCalculate в порядке non-series (не как таймсерия):
//+------------------------------------------------------------------+ //| Позиция наибольшего значения в окне назад (не таймсерия) | //+------------------------------------------------------------------+ int ZZHighest(const double &array[], int depth, int start) { if(start < 0) return(0); double max = array[start]; int index = start; for(int i = start - 1; i > start - depth && i >= 0; i--) { if(array[i] > max) // найден новый локальный максимум { index = i; max = array[i]; } } return(index); } //+------------------------------------------------------------------+ //| Позиция наименьшего значения в окне назад (не таймсерия) | //+------------------------------------------------------------------+ int ZZLowest(const double &array[], int depth, int start) { if(start < 0) return(0); double min = array[start]; int index = start; for(int i = start - 1; i > start - depth && i >= 0; i--) { if(array[i] < min) // найден новый локальный минимум { index = i; min = array[i]; } } return(index); }
Обе функции линейны по глубине окна InpDepth и вызываются на каждом баре внутри первого проходного цикла, то есть сложность полного пересчёта — O(rates_total × InpDepth). Для типичной глубины 12 и графика в несколько десятков тысяч баров это остаётся быстрой операцией, но при экстремально большом InpDepth на очень длинной истории стоит иметь это в виду.
Конечный автомат поиска экстремумов
Логика поиска пивотов реализована как явный конечный автомат с тремя состояниями:
//+------------------------------------------------------------------+ //| Состояния конечного автомата поиска экстремумов ZigZag | //+------------------------------------------------------------------+ enum EnSearchMode { Extremum = 0, // ищем первый экстремум Bottom = -1, // ищем следующий низ Peak = 1 // ищем следующий верх };
Первый проход по барам детектирует кандидатов на хай и лоу через ZZHighest/ZZLowest — поиск экстремума в скользящем окне InpDepth баров назад. Здесь же работает backstep: если в последних InpBackstep барах уже был зафиксирован менее выгодный экстремум того же типа, он аннулируется — так убираются дребезжащие локальные пики внутри одной волны, которые иначе создавали бы несколько соседних зубцов ZigZag там, где по смыслу должна быть одна волна.
//--- кандидат на низ val = low[ZZLowest(low, InpDepth, shift)]; if(val == last_low) val = 0.0; else { last_low = val; if((low[shift] - val) > InpDeviation * _Point) val = 0.0; else { //--- backstep: сбрасываем более высокие лои рядом for(back = 1; back <= InpBackstep; back++) { res = LowMapBuffer[shift - back]; if((res != 0) && (res > val)) LowMapBuffer[shift - back] = 0.0; } } } if(low[shift] == val) LowMapBuffer[shift] = val; else LowMapBuffer[shift] = 0.0;
Симметричный блок работает для кандидатов на хай, с той же логикой отклонения (InpDeviation) и того же backstep-подавления. Второй проход превращает облако кандидатов в чистую альтернирующую последовательность:
//--- состояние Peak: ищем следующий верх после низа case Peak: //--- более глубокий лоу заменяет текущий if(LowMapBuffer[shift] != 0.0 && LowMapBuffer[shift] < last_low && HighMapBuffer[shift] == 0.0) { ZigZagBuffer[last_low_pos] = 0.0; last_low_pos = shift; last_low = LowMapBuffer[shift]; ZigZagBuffer[shift] = last_low; res = 1; } //--- хай закрывает волну, переключаемся на поиск низа if(HighMapBuffer[shift] != 0.0 && LowMapBuffer[shift] == 0.0) { last_high = HighMapBuffer[shift]; last_high_pos = shift; ZigZagBuffer[shift] = last_high; extreme_search = Bottom; res = 1; } break;
Пока автомат в состоянии Peak (ищем вершину после низа), новый более глубокий лоу заменяет предыдущий — он честнее отражает истинное дно текущей коррекции, — а появление хая переключает состояние на Bottom. Симметрично работает состояние Bottom. Именно это гарантирует главное свойство любого корректного зигзага: хай и лоу строго чередуются, без двух хаёв подряд.
Инкрементальный пересчёт без полной перерисовки
Важный практический момент, который часто упускают в самописных зигзагах — поведение при поступлении нового тика без полной перерисовки истории. Индикатор откатывается назад ровно на ExtRecalc = 3 последних зафиксированных экстремума и восстанавливает состояние автомата из содержимого LowMapBuffer/HighMapBuffer на этой точке:
//+------------------------------------------------------------------+ //| Восстановление состояния автомата при инкрементальном пересчёте | //+------------------------------------------------------------------+ if(prev_calculated > 0) { i = rates_total - 1; while(extreme_counter < ExtRecalc && i > rates_total - 100) { res = ZigZagBuffer[i]; if(res != 0) extreme_counter++; i--; } i++; start = i; //--- восстанавливаем, какой тип экстремума искали последним if(LowMapBuffer[i] != 0) { cur_low = LowMapBuffer[i]; extreme_search = Peak; } else { cur_high = HighMapBuffer[i]; extreme_search = Bottom; } //--- очищаем хвост, который будет пересчитан заново for(i = start + 1; i < rates_total && !IsStopped(); i++) { ZigZagBuffer[i] = 0.0; LowMapBuffer[i] = 0.0; HighMapBuffer[i] = 0.0; } }
Это стандартный для MetaQuotes паттерн инкрементального расчёта в OnCalculate, который не гоняет весь массив на каждом тике — критично для нагруженных графиков и мультивалютных панелей. Полный пересчёт (prev_calculated == 0) происходит только при первом запуске индикатора, смене таймфрейма или подгрузке дополнительной истории, а во всех остальных случаях обрабатывается только хвост из последних нескольких баров.
Структура и накопление статистики волн
Статистика по каждому направлению хранится в отдельной структуре, которая сбрасывается перед каждым полным пересчётом:
//+------------------------------------------------------------------+ //| Структура, накапливающая статистику по одному направлению волн | //+------------------------------------------------------------------+ struct WaveStats { double currentLength; // длина последней завершённой волны, пункты double maxLength; // максимальная длина волны double minLength; // минимальная длина волны double avgLength; // средняя длина волны double medianLength; // медианная длина волны int count; // количество волн }; WaveStats bullishWaves; // статистика бычьих волн (движения вверх) WaveStats bearishWaves; // статистика медвежьих волн (движения вниз)
Для среднего используется формула инкрементального пересчёта без хранения всех значений — классическая скользящая формула, требующая O(1) памяти на одну волну:
//+------------------------------------------------------------------+ //| Обновление статистики одной новой волной | //+------------------------------------------------------------------+ void UpdateWaveStats(WaveStats &stats, double length) { stats.currentLength = length; // последняя волна stats.maxLength = (stats.count == 0) ? length : MathMax(stats.maxLength, length); stats.minLength = (stats.count == 0) ? length : MathMin(stats.minLength, length); stats.avgLength = (stats.avgLength * stats.count + length) / (stats.count + 1); stats.count++; }
Пивоты передаются в эту функцию из общего цикла, который проходит по всей найденной последовательности и относит каждую волну либо к бычьим, либо к медвежьим в зависимости от направления движения между соседними пивотами:
//--- идём по парам соседних пивотов (val[0] старше val[n-1]) for(int i = 1; i < n; i++) { double waveLength = MathAbs(val[i] - val[i - 1]) / _Point; // длина в пунктах if(val[i] > val[i - 1]) // пивот выше предыдущего -> движение вверх { UpdateWaveStats(bullishWaves, waveLength); int sz = ArraySize(bullLen); ArrayResize(bullLen, sz + 1); bullLen[sz] = waveLength; } else // пивот ниже -> движение вниз { UpdateWaveStats(bearishWaves, waveLength); int sz = ArraySize(bearLen); ArrayResize(bearLen, sz + 1); bearLen[sz] = waveLength; } }
Для медианы такой инкрементальный трюк невозможен — медиану нельзя обновить без знания всего упорядоченного набора, поэтому массивы bullLen[]/bearLen[] накапливаются полностью и сортируются через ArraySort только один раз на пересчёт статистики, а не на каждый бар:
//+------------------------------------------------------------------+ //| Медиана массива длин волн | //+------------------------------------------------------------------+ double CalculateMedian(double &array[]) { int size = ArraySize(array); if(size == 0) return(0); ArraySort(array); //--- сортировка по возрастанию if(size % 2 == 0) return((array[size / 2 - 1] + array[size / 2]) / 2.0); return(array[size / 2]); }
Это осознанный компромисс: точность важнее наносекунд, а пересчёт происходит не на каждом тике, а только на новом баре — проверка этого условия происходит ещё до сбора пивотов, чтобы не тратить ресурсы впустую при внутрибарных тиках:
//--- статистика и отрисовка: только на новом баре, чтобы не грузить терминал if(rates_total == lastBars && prev_calculated != 0) return(rates_total); lastBars = rates_total;
Вывод статистики в комментарий графика
Собранная статистика выводится в стандартный комментарий графика — минималистичное решение, не требующее создания панели или дополнительных графических объектов:
//+------------------------------------------------------------------+ //| Печать статистики в комментарий графика | //+------------------------------------------------------------------+ void DisplayStats() { string info = "=== Количественный анализ волн ZigZag (пункты) ===\n"; info += "Бычьи волны (" + IntegerToString(bullishWaves.count) + "):\n"; info += StringFormat(" Текущая: %.1f\n", bullishWaves.currentLength); info += StringFormat(" Максимум: %.1f\n", bullishWaves.maxLength); info += StringFormat(" Минимум: %.1f\n", bullishWaves.minLength); info += StringFormat(" Среднее: %.1f\n", bullishWaves.avgLength); info += StringFormat(" Медиана: %.1f\n\n", bullishWaves.medianLength); info += "Медвежьи волны (" + IntegerToString(bearishWaves.count) + "):\n"; info += StringFormat(" Текущая: %.1f\n", bearishWaves.currentLength); info += StringFormat(" Максимум: %.1f\n", bearishWaves.maxLength); info += StringFormat(" Минимум: %.1f\n", bearishWaves.minLength); info += StringFormat(" Среднее: %.1f\n", bearishWaves.avgLength); info += StringFormat(" Медиана: %.1f", bearishWaves.medianLength); Comment(info); }
Такой формат вывода удобен для быстрой визуальной проверки при разработке и отладке, но у него есть очевидное ограничение — значения недоступны программно другим индикаторам и советникам через iCustom. Этот момент разберу отдельно в разделе про ограничения.
Защита производительности в тестере и оптимизации
//--- определяем режим работы: тестер, оптимизация или визуальный режим g_in_tester = (bool)MQLInfoInteger(MQL_TESTER) || (bool)MQLInfoInteger(MQL_OPTIMIZATION) || (bool)MQLInfoInteger(MQL_VISUAL_MODE); ... //--- в тестере нужна только линия ZigZag: пропускаем статистику, комментарий и объекты if(g_in_tester) return(rates_total);
При оптимизации в составе советника отрисовка объектов и вызов Comment() на каждом баре резко замедляют прогон. Это легко превращает 10 минут расчёта в часы. Индикатор явно отключает весь визуальный слой при работе в тестере, оставляя только расчёт самого буфера ZigZag, который может понадобиться советнику через iCustom. Это ровно та деталь, которую валидатор Маркета проверяет одной из первых у любого индикатора с графическими объектами.
Визуализация: подписанные пивоты
Помимо линии зигзага, индикатор рисует на каждом пивоте точку нужного цвета и подпись с длиной именно этой волны в пунктах:
//+------------------------------------------------------------------+ //| Отрисовка точек пивотов и подписей длины волн | //+------------------------------------------------------------------+ void DrawPivots(const datetime &time[], const int &idx[], const double &val[], int n) { int total = ArraySize(time); for(int i = 0; i < n; i++) { int barPos = idx[i]; if(barPos < 0 || barPos >= total) continue; datetime t = time[barPos]; //--- цвет пивота: лоу (после него движение вверх) зелёный, хай красный bool isLow; if(i == 0) isLow = (n > 1 && val[1] > val[0]); // первый пивот — по следующему else isLow = (val[i] < val[i - 1]); // ниже предыдущего -> лоу color dotColor = isLow ? InpBullColor : InpBearColor; //--- точка пивота string dotName = OBJ_PREFIX + "dot_" + IntegerToString(idx[i]); ObjectCreate(0, dotName, OBJ_ARROW, 0, t, val[i]); ObjectSetInteger(0, dotName, OBJPROP_ARROWCODE, 159); // крупная точка ObjectSetInteger(0, dotName, OBJPROP_COLOR, dotColor); ObjectSetInteger(0, dotName, OBJPROP_WIDTH, InpDotSize); ObjectSetInteger(0, dotName, OBJPROP_ANCHOR, ANCHOR_CENTER); ObjectSetInteger(0, dotName, OBJPROP_BACK, false); //--- подпись с длиной завершённой волны на пивоте if(InpShowLabels && i > 0) { double waveLength = MathAbs(val[i] - val[i - 1]) / _Point; bool isBull = (val[i] > val[i - 1]); string txtName = OBJ_PREFIX + "txt_" + IntegerToString(idx[i]); ObjectCreate(0, txtName, OBJ_TEXT, 0, t, val[i]); ObjectSetString(0, txtName, OBJPROP_TEXT, StringFormat("%.0f", waveLength)); ObjectSetInteger(0, txtName, OBJPROP_COLOR, isBull ? InpBullColor : InpBearColor); ObjectSetInteger(0, txtName, OBJPROP_FONTSIZE, 8); //--- хай подписывается сверху, лоу снизу ObjectSetInteger(0, txtName, OBJPROP_ANCHOR, isBull ? ANCHOR_LOWER : ANCHOR_UPPER); } } }
Подпись для восходящей волны ставится снизу, для нисходящей — сверху, что визуально повторяет направление движения и уменьшает наложение подписей соседних пивотов. Объекты получают уникальные имена на основе индекса бара. Это позволяет перерисовывать только изменившийся хвост и не трогать старые пивоты — но для этого вместо полного ObjectsDeleteAll нужна частичная очистка.

Вот так выглядит график. А в Print мы видим статистику:

Практическое применение статистики
| Ситуация | Что показывает статистика | Практический вывод |
|---|---|---|
| Медиана 180 пт, среднее 340 пт | На инструменте есть редкие крупные импульсы | Стоп ставить у медианы, тейк частично держать под хвостовые движения |
| Среднее растёт, медиана стабильна | Появились редкие выбросы, а не общий рост волатильности | Не пересматривать базовые параметры риска по всей истории |
| Медиана медвежьих волн больше медианы бычьих | Снижения на инструменте в среднем резче роста | Асимметричные стопы/тейки для длинных и коротких позиций |
| Текущая волна заметно длиннее медианы | Движение уже вышло за типичный диапазон | Повышенная осторожность против входа по тренду на истощении импульса |
| Минимум и медиана близки, максимум сильно выше | Распределение волн сильно скошено вправо | Управление риском по процентилям, а не по среднему отклонению |
Входные параметры
| Параметр | Роль |
|---|---|
| InpDepth | Глубина окна поиска экстремума в барах. Больше — более крупные волны, меньше шума от локальных зубцов |
| InpDeviation | Минимальное отклонение в пунктах, чтобы кандидат считался значимым экстремумом, а не шумом внутри окна |
| InpBackstep | Число баров, в пределах которых менее выгодный экстремум того же типа аннулируется |
| InpShowLabels | Включает подписи длины волны на каждом пивоте |
| InpDotSize | Размер точки пивота на графике |
| InpBullColor / InpBearColor | Цвет пивотов и подписей для бычьих (лоу) и медвежьих (хай) точек соответственно |
Честно про ограничения
- Статистика считается по всей истории на графике целиком, без разбивки по временным окнам. Режим волатильности год назад и текущий режим смешиваются в одном среднем и одной медиане, что может маскировать реальные изменения поведения инструмента.
- Значения статистики доступны только через Comment(), то есть недоступны программно другим индикаторам и советникам. Для использования в автоматической торговле их нужно выводить через дополнительные скрытые буферы и читать через iCustom.
- Backstep и Deviation — эвристики, а не параметры, выведенные из статистики самого инструмента. Их разумные значения могут заметно отличаться между инструментами с разной типичной волатильностью, и подбор без отдельной валидации на out-of-sample-периоде рискует переобучением на конкретном участке истории.
- Отрисовка полностью очищает и заново создаёт все объекты на каждом новом баре (ObjectsDeleteAll перед DrawPivots), что на очень длинной истории с большим количеством пивотов может создавать заметную, хоть и разовую, нагрузку на каждый новый бар.
- Индикатор не разделяет статистику по фазам рынка (тренд/флэт), хотя типичная длина волны в трендовой и флэтовой фазе у большинства инструментов заметно различается — это прямое направление для доработки, а не признак ошибки в текущей реализации.
Куда расширять
Скользящее окно вместо всей истории. Добавить входной параметр количества последних волн, по которым считается статистика, чтобы отделить текущий режим волатильности от долгосрочного профиля инструмента.
Программный доступ к статистике. Вывести среднее, медиану, максимум и минимум по каждому направлению в дополнительные скрытые буферы INDICATOR_CALCULATIONS, чтобы советники могли читать их через iCustom без парсинга текста комментария.
Разделение по фазам рынка. Добавить фильтр по ADX или собственному классификатору тренд/флэт, чтобы считать отдельную статистику волн для каждого режима — это существенно точнее отражает то, чего ждать от следующей волны в текущих условиях.
Процентили вместо только медианы. Поскольку распределение длин волн часто скошено (см. раздел 2), добавление 25-го и 75-го процентилей дало бы более полную картину разброса, чем пара медиана/среднее — особенно полезно для калибровки диапазонов стопов и тейков.
Заключение
Этот индикатор — наглядный пример того, что количественный анализ не обязательно означает нейросети или сложную статистику. Достаточно взять привычный визуальный инструмент и добавить к нему явный подсчёт среднего, медианы, минимума и максимума по естественным сегментам движения цены — и получить работающий количественный слой поверх интуитивного индикатора, которым трейдеры и так уже пользуются каждый день.
Статистику нужно учитывать с поправкой на ограничения: она агрегирует всю историю без разделения по режимам и сейчас доступна только через комментарий графика. Параметры (depth, deviation, backstep) следует проверять на конкретном инструменте, а не переносить значения по умолчанию без вопросов.
| Название файла | Описание файла |
|---|---|
| Quantitative Analysis Indicator.mq5 | Индикатор ZigZag с расчётом статистики длины волн (среднее, медиана, максимум, минимум) отдельно для бычьих и медвежьих движений |
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Встраивание торговой дисциплины в код (Часть 6): Построение единого фреймворка дисциплины на MQL5
Создание пользовательских индикаторов в MQL5 (Часть 11): Улучшение футпринт-графика с помощью слоёв рыночной структуры и ордер-флоу
Автоматизация торговых стратегий в MQL5 (Часть 32): Создание системы распознавания гармонического паттерна "5 Drives" с использованием Price Action
Кодирование свечных моделей (Часть 1): Алфавитная система для выявления сигналов
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования