Торговые инструменты на MQL5 (Часть 35): Расширение слоя рисования на холсте — каналы, вилы, инструменты Ганна и Фибоначчи
Введение
В предыдущей статье (Часть 34) мы заменили встроенные графические объекты MetaTrader 5 интерактивным слоем рисования на холсте и добавили в него небольшой набор базовых инструментов для рисования линий. Архитектурная основа выстроена аккуратно, но пока отсутствует аналитический инструментарий, необходимый для серьезного анализа графиков: каналы для построения трендовых огибающих, вилы для анализа структуры ценовых колебаний, инструменты Ганна для углового анализа и инструменты Фибоначчи для анализа коррекций, расширений, временных проекций и дуг. Эта статья призвана восполнить этот пробел.
Мы расширяем слой рисования на холсте, добавляя пятнадцать новых аналитических инструментов, сгруппированных в семь четких категорий: каналы (параллельный канал, равноудаленный канал, регрессионный канал), вилы (стандартные вилы, вилы Шиффа, модифицированные вилы Шиффа), инструменты Ганна (веер, сетка, коробка), инструменты Фибоначчи (уровни, расширение, временная проекция, дуги, веер, канал), а также действие "Удалить всё". Эта статья предназначена для разработчиков на языке MetaQuotes Language 5 (MQL5) среднего и продвинутого уровня, которые уже освоили цепочку наследования, созданную нами в предыдущей части. Мы рассмотрим следующие темы:
- От линий, заданных двумя точками, к аналитическим инструментам с несколькими опорными точками
- Реализация каналов и вил
- Реализация инструментов Ганна и Фибоначчи
- Интеграция новых инструментов в движок рисования
- Визуализация
- Заключение
В итоге слой рисования на холсте будет содержать полный набор аналитических инструментов рисования с несколькими опорными точками: пользователь сможет размещать их, наводить на них курсор, выбирать, изменять их форму и удалять по той же схеме взаимодействия, что и базовые инструменты для рисования линий.
От линий, заданных двумя точками, к аналитическим инструментам с несколькими опорными точками
Все инструменты для рисования линий, представленные в предыдущей части, устроены по общему принципу. Они задаются не более чем двумя опорными точками, каждый их маркер расположен непосредственно в одной из этих точек, а геометрия, которую отрисовывает движок, напрямую определяется опорными точками, без промежуточных вычисляемых точек. Это работает для линий тренда, лучей, прямоугольников и других базовых инструментов, но перестаёт работать, как только мы пытаемся смоделировать канал, вилы, веер Ганна или любой инструмент Фибоначчи — им нужны дополнительные опорные точки, вычисляемые маркеры, отдельные массивы уровней для каждого инструмента или геометрия, вычисляемая на основе рыночных данных при каждой перерисовке.
Новые инструменты подразделяются на три архитектурные категории. Некоторые из них размещаются тремя щелчками, поскольку для задания их геометрии нужны три опорные точки: первые два щелчка задают базовую линию, а третий — смещение, начало проекции или вершину. Другие вычисляют положение маркеров на основе существующих маркеров, чтобы сохранить геометрические инварианты: четвёртая вершина параллельного канала вычисляется по остальным трём, конечные точки регрессионного канала определяются по результатам аппроксимации методом наименьших квадратов по барам во временном окне, а медианная линия вил начинается в вычисляемой точке, положение которой различается у вил Эндрюса, вил Шиффа и модифицированных вил Шиффа. Инструменты Фибоначчи добавляют третью особенность: каждый из них отображает семейство линий или дуг, задаваемых коэффициентами; эти коэффициенты, цвета и флаги видимости хранятся в самом нарисованном объекте в виде массива уровней, который движок считывает при каждой перерисовке. Эта архитектура была разработана по образцу стандартной системы веб-графиков MQL5, в которой используются инструменты с заливкой, в отличие от инструментов терминала, состоящих преимущественно из линий и не поддерживающих регулировку непрозрачности. См. иллюстрацию ниже.

Чтобы избежать переработки движка, мы расширяем цепочку наследования. Класс инструментов линий становится базовым для класса инструментов каналов. Последний, в свою очередь, становится базовым для класса инструментов Фибоначчи, который становится новым родительским классом реестра инструментов и, в конечном итоге, движка рисования. Каждый слой добавляет процедуры рисования, функции проверки попадания и общие вспомогательные функции, которые повторно используются вышележащими слоями. К ним относятся вычисление конечных точек регрессии, отсечение луча по границам холста, построчная заливка четырёхугольников, отрисовка дуг со сглаживанием. В структуру объекта отрисовки добавляются новые группы полей для хранения свойств отдельных инструментов. В движке появляется система сохранения настроек инструмента, благодаря которой настройки сохраняются при последовательных размещениях одного и того же инструмента. Конвейер динамического предварительного просмотра получает возможность отображать два промежуточных состояния для предпросмотра инструментов, задаваемых тремя щелчками мыши. Вот наглядное представление наших целей.

Теперь, когда концептуальная схема готова, мы можем приступить к реализации.
Реализация инструментов "Канал" и "Вилы"
Объявление класса инструментов каналов
Мы начинаем реализацию с объявления "CChannelTools" — нового слоя, который наследуется от класса инструментов линий, представленного в предыдущей части. Этот уровень задаёт интерфейс новых аналитических инструментов. Он объявляет все общедоступные процедуры рисования и функции проверки попадания, которые движок рисования будет вызывать при отрисовке каналов, вил и инструментов Ганна или при взаимодействии с ними.
//+------------------------------------------------------------------+ //| ToolsPalette_Channels.mqh | //| Copyright 2026, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #ifndef TOOLS_PALETTE_CHANNELS_MQH #define TOOLS_PALETTE_CHANNELS_MQH //--- Include the base line-tools layer that CChannelTools extends #include "ToolsPalette_Lines.mqh" //+------------------------------------------------------------------+ //| Channel tools library: parallel / regression / stddev | //+------------------------------------------------------------------+ class CChannelTools : public CLineTools { protected: public: //--- Compute regression line endpoints for the bar range [t1, t2] bool ComputeRegressionEndpoints(datetime t1, datetime t2, double &leftP, double &rightP); // leftP/rightP correspond to stored t1/t2 (unsorted) //--- Draw the parallel channel onto the given canvas void DrawParallelChannelOn(CCanvas &canvas, int x1, int y1, int x2, int y2, int x3, int y3, color objColor, bool selected, bool hovered, int lineWidth = 2, int lineOpacity = 100, int lineStyle = 0, color midColor = clrNONE, int midOpacity = 80, int midStyle = 1, color fillColor = clrNONE, int fillOpacity = 30, bool midVisible = true, int midWidth = 2, double midOffset = 0.5); //--- Draw the regression channel onto the given canvas void DrawRegressionChannelOn(CCanvas &canvas, long chartId, datetime t1, datetime t2, color objColor, bool selected, bool hovered, int lineWidth = 2, int lineOpacity = 100, int lineStyle = 0, color fillColor = clrNONE, int fillOpacity = 30, color fillColor2 = clrNONE, int fillOpacity2 = 30, color textColor = clrNONE, int textOpacity = 100, bool centerVisible = true, color centerColor = clrNONE, int centerOpacity = 100, int centerWidth = 2, int centerStyle = 1, bool upperBandVisible = true, double upperBandSigma = 2.0, bool lowerBandVisible = true, double lowerBandSigma = 2.0, bool pearsonVisible = true); //--- Draw the std deviation channel onto the given canvas void DrawStdDevChannelOn(CCanvas &canvas, long chartId, datetime t1, datetime t2, color objColor, bool selected, bool hovered, int lineWidth = 2, int lineOpacity = 100, int lineStyle = 0, color fillColor = clrNONE, int fillOpacity = 30, bool centerVisible = true, color centerColor = clrNONE, int centerOpacity = 100, int centerWidth = 2, int centerStyle = 1, bool upperBandVisible = true, double upperBandSigma = 2.0, bool lowerBandVisible = true, double lowerBandSigma = 2.0); //--- Hit test the parallel channel body (2 solid lines + dashed center) bool HitTestParallelChannel(int mx, int my, int x1, int y1, int x2, int y2, int x3, int y3, int threshold); //--- Hit test the regression channel body (recomputes regression to find line positions) bool HitTestRegressionChannel(int mx, int my, long chartId, datetime t1, datetime t2, int threshold); //--- Hit test the std deviation channel body (delegates to regression hit test) bool HitTestStdDevChannel(int mx, int my, long chartId, datetime t1, datetime t2, int threshold); //--- Draw Andrew's Pitchfork onto the given canvas void DrawAndrewsPitchforkOn(CCanvas &canvas, int x1, int y1, int x2, int y2, int x3, int y3, color objColor, bool selected, bool hovered, bool medianVisible, color medianColor, int medianWidth, int medianStyle, bool outerVisible, color outerColor, int outerWidth, int outerStyle, bool innerVisible, color innerColor, int innerWidth, int innerStyle, int lineOpacity, int fillOpacity); //--- Hit test Andrew's Pitchfork body (tests all 6 drawn lines) bool HitTestAndrewsPitchfork(int mx, int my, int canvasW, int canvasH, int x1, int y1, int x2, int y2, int x3, int y3, int threshold); //--- Draw Schiff Pitchfork onto the given canvas void DrawSchiffPitchforkOn(CCanvas &canvas, int x1, int y1, int x2, int y2, int x3, int y3, color objColor, bool selected, bool hovered, bool medianVisible, color medianColor, int medianWidth, int medianStyle, bool outerVisible, color outerColor, int outerWidth, int outerStyle, bool innerVisible, color innerColor, int innerWidth, int innerStyle, int lineOpacity, int fillOpacity); //--- Hit test Schiff Pitchfork body bool HitTestSchiffPitchfork(int mx, int my, int canvasW, int canvasH, int x1, int y1, int x2, int y2, int x3, int y3, int threshold); //--- Draw Modified Schiff Pitchfork onto the given canvas void DrawModSchiffPitchforkOn(CCanvas &canvas, int x1, int y1, int x2, int y2, int x3, int y3, color objColor, bool selected, bool hovered, bool medianVisible, color medianColor, int medianWidth, int medianStyle, bool outerVisible, color outerColor, int outerWidth, int outerStyle, bool innerVisible, color innerColor, int innerWidth, int innerStyle, int lineOpacity, int fillOpacity); //--- Hit test Modified Schiff Pitchfork body bool HitTestModSchiffPitchfork(int mx, int my, int canvasW, int canvasH, int x1, int y1, int x2, int y2, int x3, int y3, int threshold); //--- Draw Gann Line: single ray from P1 through P2, extended to the right canvas edge void DrawGannLineOn(CCanvas &canvas, int x1, int y1, int x2, int y2, color objColor, bool selected, bool hovered, int lineOpacity, int lineWidth, int lineStyle); //--- Hit test Gann Line body (distance from cursor to extended ray) bool HitTestGannLine(int mx, int my, int canvasW, int canvasH, int x1, int y1, int x2, int y2, int threshold); //--- Draw Gann Fan: 9 rays from P1 at canonical Gann ratios void DrawGannFanOn(CCanvas &canvas, int x1, int y1, int x2, int y2, color objColor, bool selected, bool hovered, const double &lvlRatio[], const color &lvlColor[], const int &lvlOpacity[], const int &lvlWidth[], const int &lvlStyle[], const bool &lvlVisible[], int fillOpacity); //--- Hit test Gann Fan (returns true if cursor is near any of the 9 rays) bool HitTestGannFan(int mx, int my, int canvasW, int canvasH, int x1, int y1, int x2, int y2, int threshold); //--- Draw Gann Box: 7 ratios drive both horizontal price-fib and vertical time-fib lines void DrawGannBoxOn(CCanvas &canvas, int x1, int y1, int x2, int y2, color objColor, bool selected, bool hovered, const double &lvlRatio[], const color &lvlColor[], const int &lvlOpacity[], const int &lvlWidth[], const int &lvlStyle[], const bool &lvlVisible[], int fillOpacity); //--- Hit test Gann Box (returns true if cursor is near any edge or interior Fib grid line) bool HitTestGannBox(int mx, int my, int x1, int y1, int x2, int y2, int threshold); };
Для начала мы обеспечиваем защиту заголовочного файла от повторного включения с помощью макроса TOOLS_PALETTE_CHANNELS_MQH и подключаем заголовочный файл линейных инструментов, который предоставляет вспомогательные функции для работы с холстом, средство отрисовки маркеров и процедуры рисования штриховых линий, повторно используемые новыми инструментами. Сам класс наследуется от класса линейных инструментов, поэтому все методы, которые мы добавили в предыдущей части (рисование линий со сглаживанием, построчная заливка прямоугольников, отрисовка маркеров с ореолами при наведении курсора), доступны для каналов и вил без повторного объявления.
Первым идет объявление функции ComputeRegressionEndpoints, которая принимает два значения типа datetime, задающие начало и конец временного окна, и возвращает цены в левой и правой конечных точках линии регрессии, построенной методом наименьших квадратов (OLS) по ценам закрытия всех баров в этом окне. Мы будем использовать эту функцию как при размещении (для привязки опорных точек регрессионного канала к конечным точкам линии регрессии), так и при каждой перерисовке (чтобы центральная линия оставалась привязанной к текущей динамике цены по мере появления новых баров). Две возвращаемые цены соответствуют сохраненным значениям даты и времени в их исходном порядке, поэтому вызывающему коду не требуется их сортировать.
Затем мы объявляем три процедуры рисования каналов: "DrawParallelChannelOn", "DrawRegressionChannelOn" и "DrawStdDevChannelOn". Каждая из них принимает целевой холст, опорные точки (три или две в зависимости от варианта), состояние объекта (выделен ли он и наведен ли на него курсор) и параметры стиля для каждого компонента. Процедура рисования параллельного канала принимает цвет, непрозрачность, стиль и видимость средней линии отдельно от свойств границ канала, поскольку пользователи обычно хотят, чтобы средняя линия визуально отличалась от границ. Процедура рисования регрессионного канала предоставляет параметры центральной линии, видимости и ширины обеих полос стандартного отклонения, цвета подписи коэффициента корреляции Пирсона R, а также второй цвет заливки, чтобы пользователи могли окрашивать верхнюю и нижнюю половины канала по-разному. Канал стандартного отклонения представляет собой упрощенный вариант с одним цветом заливки и без подписи коэффициента корреляции Пирсона.
Три функции проверки попадания — "HitTestParallelChannel", "HitTestRegressionChannel" и "HitTestStdDevChannel" — соответствуют процедурам рисования. Функции проверки попадания для регрессионных каналов принимают идентификатор графика, а не предварительно вычисленные пиксельные координаты, поскольку им необходимо заново вычислить регрессию внутри функции, чтобы определить текущее положение линий.
Далее идут три процедуры рисования вил — "DrawAndrewsPitchforkOn", "DrawSchiffPitchforkOn" и "DrawModSchiffPitchforkOn" — с соответствующими функциями проверки попадания. Каждая процедура рисования вил принимает три опорные точки, параметры видимости каждой линии, цвет, ширину и стиль медианной, внешних и внутренних линий, а также общую непрозрачность и непрозрачность заливки. Эти три варианта имеют одинаковую сигнатуру параметров, поскольку единственное различие между ними заключается в положении начала медианной линии: у вил Эндрюса — в P1, у вил Шиффа — во времени P1 и на уровне цены середины отрезка P2–P3, а у модифицированных вил Шиффа — в геометрической середине отрезка P1–P2. Логика, зависящая от конкретного варианта, реализована внутри каждой процедуры рисования, а не в месте вызова.
Завершает список объявлений раздел инструментов Ганна. "DrawGannLineOn" — самая простая процедура: две опорные точки и один луч, продолженный до правого края холста. Функции "DrawGannFanOn" и "DrawGannBoxOn" принимают одни и те же шесть массивов параметров уровней — коэффициентов, цветов, непрозрачности, толщины линий, стилей и флагов видимости, — поэтому движок может напрямую передавать сохраненную в объекте конфигурацию уровней без ее распаковки. Соответствующие функции проверки попадания — "HitTestGannLine", "HitTestGannFan" и "HitTestGannBox" — дополняют общедоступный интерфейс. Теперь, когда протокол объявлен, можно перейти к реализациям. Поскольку для всех этих категорий объектов используется один и тот же подход, а основу мы заложили в предыдущей части, остановимся на наиболее важных и существенных разделах. Начнем с реализации регрессии по методу наименьших квадратов (OLS), поскольку она включает полосы стандартного отклонения, а остальные объекты каналов используют тот же подход. Давайте сначала определим вспомогательные функции.
Общие вспомогательные функции каналов и вычисление конечных точек регрессии
Определим четыре общие вспомогательные функции, которые вызываются всеми рассматриваемыми в этой части каналами, вилами, инструментами Ганна и Фибоначчи. Первые три отвечают за отрисовку на уровне пикселей — альфа-композитинг, построчную заливку многоугольников и рисование штриховых линий, — а четвертая вычисляет конечные точки регрессии по методу наименьших квадратов (OLS), используемые регрессионными каналами и каналами стандартного отклонения.
//+------------------------------------------------------------------+ //| Blend a single pixel onto the canvas with alpha compositing | //+------------------------------------------------------------------+ #ifndef CHANNEL_BLEND_PIXEL_SET_DEFINED #define CHANNEL_BLEND_PIXEL_SET_DEFINED void ChannelBlendPixelSet(CCanvas &canvas, int px, int py, uint srcArgb) { int cW = canvas.Width(), cH = canvas.Height(); //--- Reject out-of-bounds pixels immediately if(px < 0 || px >= cW || py < 0 || py >= cH) return; //--- Extract source alpha from the top byte uchar sA = (uchar)((srcArgb >> 24) & 0xFF); //--- Skip fully transparent pixels if(sA == 0) return; //--- Fully opaque: write directly without compositing math if(sA == 255) { canvas.PixelSet(px, py, srcArgb); return; } //--- Read the existing destination pixel for blending uint dst = canvas.PixelGet(px, py); uchar dA = (uchar)((dst >> 24) & 0xFF); //--- Normalize alpha values to [0, 1] range double a = sA / 255.0; double da = dA / 255.0; //--- Compute the combined output alpha (porter-duff over) double oa = a + da * (1.0 - a); if(oa <= 0.0) return; //--- Normalize source RGB channels double sR = ((srcArgb >> 16) & 0xFF) / 255.0; double sG = ((srcArgb >> 8) & 0xFF) / 255.0; double sB = ( srcArgb & 0xFF) / 255.0; //--- Normalize destination RGB channels double dR = ((dst >> 16) & 0xFF) / 255.0; double dG = ((dst >> 8) & 0xFF) / 255.0; double dB = ( dst & 0xFF) / 255.0; //--- Assemble the composited ARGB output pixel uint outPix = ((uint)(uchar)(oa * 255.0 + 0.5) << 24) | ((uint)(uchar)((sR*a + dR*da*(1.0-a)) / oa * 255.0 + 0.5) << 16) | ((uint)(uchar)((sG*a + dG*da*(1.0-a)) / oa * 255.0 + 0.5) << 8) | (uint)(uchar)((sB*a + dB*da*(1.0-a)) / oa * 255.0 + 0.5); canvas.PixelSet(px, py, outPix); } #endif //+------------------------------------------------------------------+ //| Scanline fill a 4-vertex quad with flat alpha-composited color | //+------------------------------------------------------------------+ #ifndef CHANNEL_FILL_QUAD_DEFINED #define CHANNEL_FILL_QUAD_DEFINED void ChannelFillQuad(CCanvas &canvas, int x0, int y0, int x1, int y1, int x2, int y2, int x3, int y3, uint fillArgb) { //--- Pack vertices into arrays for scanline traversal int pxs[4]; int pys[4]; pxs[0] = x0; pys[0] = y0; pxs[1] = x1; pys[1] = y1; pxs[2] = x2; pys[2] = y2; pxs[3] = x3; pys[3] = y3; //--- Find the vertical extents of the quad int ymin = pys[0], ymax = pys[0]; for(int k = 1; k < 4; k++) { if(pys[k] < ymin) ymin = pys[k]; if(pys[k] > ymax) ymax = pys[k]; } int cW = canvas.Width(), cH = canvas.Height(); //--- Clamp vertical range to canvas bounds if(ymin < 0) ymin = 0; if(ymax >= cH) ymax = cH - 1; //--- Scanline rasterization: one horizontal span per row for(int yy = ymin; yy <= ymax; yy++) { double xs[4]; int nx = 0; //--- Intersect each edge of the quad with the current scanline for(int e = 0; e < 4; e++) { int e2 = (e + 1) & 3; double ay = (double)pys[e], by = (double)pys[e2]; //--- Skip edges that don't cross this scanline row if((ay > yy) == (by > yy)) continue; double ax = (double)pxs[e], bx = (double)pxs[e2]; double t = ((double)yy - ay) / (by - ay); xs[nx++] = ax + t * (bx - ax); } if(nx < 2) continue; //--- Sort intersection X values ascending for span fill for(int a = 0; a < nx - 1; a++) for(int b = a + 1; b < nx; b++) if(xs[b] < xs[a]) { double tmp = xs[a]; xs[a] = xs[b]; xs[b] = tmp; } //--- Fill each horizontal span between paired intersections for(int p = 0; p + 1 < nx; p += 2) { int xL = (int)MathCeil(xs[p]); int xR = (int)MathFloor(xs[p + 1]); //--- Clamp span to canvas horizontal bounds if(xL < 0) xL = 0; if(xR >= cW) xR = cW - 1; for(int xx = xL; xx <= xR; xx++) ChannelBlendPixelSet(canvas, xx, yy, fillArgb); } } } #endif //+------------------------------------------------------------------+ //| Draw a 1px dashed line (4-on 4-off) between two canvas points | //+------------------------------------------------------------------+ void ChannelDrawDashedLine(CCanvas &canvas, int x1, int y1, int x2, int y2, uint argb) { //--- Compute direction and total pixel length of the segment double dxl = x2 - x1, dyl = y2 - y1; double ll = MathSqrt(dxl * dxl + dyl * dyl); //--- Skip degenerate zero-length segments if(ll <= 1.0) return; int steps = (int)ll; int cW = canvas.Width(), cH = canvas.Height(); //--- Walk along the segment and paint only the "on" dash pixels for(int s = 0; s < steps; s++) { //--- 4-on, 4-off: skip every second 4-pixel run if((s / 4) % 2 != 0) continue; int px = x1 + (int)(dxl * s / steps); int py = y1 + (int)(dyl * s / steps); //--- Paint only pixels that land inside the canvas if(px >= 0 && px < cW && py >= 0 && py < cH) ChannelBlendPixelSet(canvas, px, py, argb); } } //+------------------------------------------------------------------+ //| Compute regression endpoints over bar closes in [t1, t2] | //+------------------------------------------------------------------+ bool CChannelTools::ComputeRegressionEndpoints(datetime t1, datetime t2, double &leftP, double &rightP) { //--- Identify the chronologically earlier and later datetimes datetime tL = (t1 < t2) ? t1 : t2; datetime tR = (t1 < t2) ? t2 : t1; //--- Map datetimes to bar indices (nearest bar, no strict mode) int barR = iBarShift(_Symbol, _Period, tR, false); int barL = iBarShift(_Symbol, _Period, tL, false); //--- Ensure barL is the higher (older) bar index if(barL < barR) { int tmp = barL; barL = barR; barR = tmp; } int nBars = barL - barR + 1; //--- Reject ranges too small for a valid regression if(nBars < 2) return false; //--- Accumulate regression sums over all bars in range double Sx = 0, Sy = 0, Sxx = 0, Sxy = 0; for(int i = 0; i < nBars; i++) { int shift = barL - i; double cls = iClose(_Symbol, _Period, shift); //--- Skip bars with no valid close price if(cls == 0.0) continue; double x = (double)i; Sx += x; Sy += cls; Sxx += x*x; Sxy += x*cls; } double dn = (double)nBars; double denom = dn*Sxx - Sx*Sx; //--- Reject degenerate case where X-variance is effectively zero if(MathAbs(denom) < 1e-12) return false; //--- Compute slope and intercept of the OLS regression line double slope = (dn*Sxy - Sx*Sy) / denom; double intercept = (Sy - slope*Sx) / dn; //--- Evaluate the line at the leftmost and rightmost bar positions leftP = intercept; rightP = slope * (double)(nBars - 1) + intercept; //--- Swap outputs if caller passed t1 > t2 (preserve stored order) if(t1 > t2) { double tmp = leftP; leftP = rightP; rightP = tmp; } return true; }
Начнём с "ChannelBlendPixelSet" — примитива альфа-композитинга, который позволяет полупрозрачной заливке аккуратно накладываться поверх всего, что уже нарисовано на холсте. При простейшей записи пикселей существующий пиксель заменяется новым, что дает неверный результат при отрисовке полупрозрачной полосы поверх другой полупрозрачной полосы: нижняя полоса стирается. Альфа-композитинг устраняет эту проблему, объединяя исходный пиксель с пикселем назначения с учетом их альфа-значений по формуле Портера — Даффа "источник поверх назначения". В тривиальных случаях — при полной прозрачности или полной непрозрачности пикселя — мы сразу выходим из функции и выполняем полный расчет только при частичной непрозрачности: нормализуем оба альфа-значения в диапазон от нуля до единицы, вычисляем результирующее альфа-значение и пропорционально смешиваем каждый канал RGB.
Далее функция "ChannelFillQuad" заполняет внутреннюю область выпуклого четырехугольника однородным цветом с применением альфа-композитинга, используя метод построчной растеризации. Вместо того чтобы проверять принадлежность каждого пикселя многоугольнику (что вычислительно затратно), мы обрабатываем многоугольник по одной горизонтальной строке: с помощью линейной интерполяции находим пересечения его ребер с этой строкой, сортируем их по возрастанию координаты и заполняем горизонтальные интервалы между парами пересечений с помощью примитива смешивания. Это стандартный алгоритм программной заливки многоугольников; заливка корректно смешивается с любым содержимым под ней, поскольку каждый пиксель обрабатывается функцией "ChannelBlendPixelSet".
Третья вспомогательная функция, "ChannelDrawDashedLine", рисует между двумя точками холста штриховую линию толщиной в один пиксель по схеме "четыре пикселя рисуем, четыре пропускаем": она проходит отрезок по одному пикселю, пропуская каждую вторую группу из четырех пикселей. Определения "ChannelBlendPixelSet" и "ChannelFillQuad" снабжены защитой от повторного включения ("CHANNEL_BLEND_PIXEL_SET_DEFINED" и "CHANNEL_FILL_QUAD_DEFINED"), поскольку заголовочный файл линий также определяет эти функции для отрисовки прямоугольников; такая защита позволяет включать файлы в любом порядке без ошибки повторного определения. Мы решили определить их в файле каналов, чтобы обеспечить простоту расширения на случай, если в будущем нам понадобится более сложный алгоритм.
Наконец, "ComputeRegressionEndpoints" — это вспомогательная функция расчета регрессии по методу наименьших квадратов (OLS), на которой основано построение как регрессионного канала, так и канала стандартного отклонения. Мы определяем более ранний и более поздний моменты времени, сопоставляем их с индексами баров с помощью функции iBarShift и проходим по барам, накапливая стандартные суммы для метода наименьших квадратов (OLS) — сумму X, сумму Y, сумму квадратов X и сумму произведений X и Y; цены закрытия получаем с помощью функции iClose. На основе этих сумм мы вычисляем угловой коэффициент и свободный член прямой наилучшего приближения, рассчитываем ее значения в позициях крайнего левого и крайнего правого баров, чтобы получить цены в конечных точках, и меняем результаты местами, если вызывающий код передал моменты времени в обратном порядке, чтобы результаты соответствовали сохраненному порядку времени. С помощью этих четырех вспомогательных функций можно реализовать сами функции отрисовки каналов. Начнем с регрессионного канала.
Отрисовка регрессионного канала
Регрессионный канал отображает центральную линию, построенную методом наименьших квадратов (OLS) по ценам закрытия баров в выбранном временном окне, две полосы стандартного отклонения вокруг нее, полупрозрачную заливку между центральной линией и каждой полосой, а также подпись с коэффициентом корреляции Пирсона R, показывающую, насколько хорошо динамика цены соответствует этой линии.
//+------------------------------------------------------------------+ //| Draw a regression channel onto the canvas | //+------------------------------------------------------------------+ void CChannelTools::DrawRegressionChannelOn(CCanvas &canvas, long chartId, datetime t1, datetime t2, color objColor, bool selected, bool hovered, int lineWidth = 2, int lineOpacity = 100, int lineStyle = 0, color fillColor = clrNONE, int fillOpacity = 30, color fillColor2 = clrNONE, int fillOpacity2 = 30, color textColor = clrNONE, int textOpacity = 100, bool centerVisible = true, color centerColor = clrNONE, int centerOpacity = 100, int centerWidth = 2, int centerStyle = 1, bool upperBandVisible = true, double upperBandSigma = 2.0, bool lowerBandVisible = true, double lowerBandSigma = 2.0, bool pearsonVisible = true) { //--- Clamp line width to the supported [1, 4] range if(lineWidth < 1) lineWidth = 1; if(lineWidth > 4) lineWidth = 4; //--- Clamp center-line width to the supported [1, 4] range if(centerWidth < 1) centerWidth = 1; if(centerWidth > 4) centerWidth = 4; //--- Clamp line style to the supported [0, 3] range if(lineStyle < 0) lineStyle = 0; if(lineStyle > 3) lineStyle = 3; //--- Clamp center style to the supported [0, 3] range if(centerStyle < 0) centerStyle = 0; if(centerStyle > 3) centerStyle = 3; //--- Prevent sigma values collapsing to zero if(upperBandSigma < 0.1) upperBandSigma = 0.1; if(lowerBandSigma < 0.1) lowerBandSigma = 0.1; //--- Resolve clrNONE to the appropriate default colors per part color lineCol = objColor; color centerCol = (centerColor == clrNONE) ? objColor : centerColor; color fillUp = (fillColor == clrNONE) ? clrDodgerBlue : fillColor; color fillDn = (fillColor2 == clrNONE) ? clrCrimson : fillColor2; color labelCol = (textColor == clrNONE) ? objColor : textColor; //--- Build ARGB values for band boundary lines and fills const uint lineArgb = ColorWithPercentOpacity(lineCol, lineOpacity); const uint upperFillArgb = ColorWithPercentOpacity(fillUp, fillOpacity); const uint lowerFillArgb = ColorWithPercentOpacity(fillDn, fillOpacity2); const int thick = lineWidth; //--- Determine the chronologically left and right datetimes datetime tL = (t1 < t2) ? t1 : t2; datetime tR = (t1 < t2) ? t2 : t1; //--- Map datetimes to bar indices int barR = iBarShift(_Symbol, _Period, tR, false); int barL = iBarShift(_Symbol, _Period, tL, false); //--- Ensure barL is the older (higher-index) bar if(barL < barR) { int tmp = barL; barL = barR; barR = tmp; } int nBars = barL - barR + 1; //--- Need at least 2 bars for a valid regression if(nBars < 2) return; //--- Accumulate OLS sums including Syy for Pearson R double Sx = 0, Sy = 0, Sxx = 0, Syy = 0, Sxy = 0; for(int i = 0; i < nBars; i++) { int shift = barL - i; double cls = iClose(_Symbol, _Period, shift); //--- Skip bars with invalid close price if(cls == 0.0) continue; double x = (double)i; Sx += x; Sy += cls; Sxx += x*x; Syy += cls*cls; Sxy += x*cls; } double dn = (double)nBars; double denom = dn * Sxx - Sx * Sx; //--- Reject degenerate X-variance case if(MathAbs(denom) < 1e-12) return; //--- Compute OLS slope and intercept double slope = (dn * Sxy - Sx * Sy) / denom; double intercept = (Sy - slope * Sx) / dn; //--- Compute the Y-variance denominator needed for Pearson R double denomY = dn * Syy - Sy * Sy; double pearsonR = 0.0; //--- Compute Pearson R only when both denominators are well-defined if(denomY > 1e-12 && denom > 1e-12) pearsonR = (dn * Sxy - Sx * Sy) / MathSqrt(denom * denomY); //--- Accumulate sum of squared residuals for sigma double sumSq = 0.0; for(int i = 0; i < nBars; i++) { int shift = barL - i; double cls = iClose(_Symbol, _Period, shift); //--- Skip invalid bars in residual pass if(cls == 0.0) continue; double pred = slope * (double)i + intercept; double r = cls - pred; sumSq += r * r; } //--- Compute residual standard deviation double sigma = MathSqrt(sumSq / dn); //--- Convert bar indices back to chart datetimes datetime leftTime = (datetime)iTime(_Symbol, _Period, barL); datetime rightTime = (datetime)iTime(_Symbol, _Period, barR); //--- Compute the center, upper, and lower band prices at both ends double leftCP = intercept; double rightCP = slope * (double)(nBars - 1) + intercept; double leftUP = leftCP + upperBandSigma * sigma; double rightUP = rightCP + upperBandSigma * sigma; double leftLP = leftCP - lowerBandSigma * sigma; double rightLP = rightCP - lowerBandSigma * sigma; //--- Map all price/time coordinates to canvas pixel positions int xL_p=0, yLc=0, xR_p=0, yRc=0, xtmp=0; int yLu=0, yRu=0, yLl=0, yRl=0; ChartTimePriceToXY(chartId, 0, leftTime, leftCP, xL_p, yLc); ChartTimePriceToXY(chartId, 0, rightTime, rightCP, xR_p, yRc); ChartTimePriceToXY(chartId, 0, leftTime, leftUP, xtmp, yLu); ChartTimePriceToXY(chartId, 0, rightTime, rightUP, xtmp, yRu); ChartTimePriceToXY(chartId, 0, leftTime, leftLP, xtmp, yLl); ChartTimePriceToXY(chartId, 0, rightTime, rightLP, xtmp, yRl); //--- Fill the upper band region between the center and upper boundary if(upperBandVisible) ChannelFillQuad(canvas, xL_p, yLu, xR_p, yRu, xR_p, yRc, xL_p, yLc, upperFillArgb); //--- Fill the lower band region between center and lower boundary if(lowerBandVisible) ChannelFillQuad(canvas, xL_p, yLc, xR_p, yRc, xR_p, yRl, xL_p, yLl, lowerFillArgb); //--- Draw upper and lower band boundary lines gated by visibility if(upperBandVisible || lowerBandVisible) { if(lineStyle == 0) { if(upperBandVisible) DrawThickLine(canvas, xL_p, yLu, xR_p, yRu, thick, lineArgb); if(lowerBandVisible) DrawThickLine(canvas, xL_p, yLl, xR_p, yRl, thick, lineArgb); } else { int pat[]; const int n = BuildLineStylePattern(lineStyle, thick, pat); if(n > 0) { if(upperBandVisible) WidgetDashedLineAA(canvas, xL_p, yLu, xR_p, yRu, thick, lineArgb, pat); if(lowerBandVisible) WidgetDashedLineAA(canvas, xL_p, yLl, xR_p, yRl, thick, lineArgb, pat); } else { //--- Fall back to solid lines if pattern build fails if(upperBandVisible) DrawThickLine(canvas, xL_p, yLu, xR_p, yRu, thick, lineArgb); if(lowerBandVisible) DrawThickLine(canvas, xL_p, yLl, xR_p, yRl, thick, lineArgb); } } } //--- Draw the center regression line with its own style/width/color if(centerVisible) { const uint centerArgb = ColorWithPercentOpacity(centerCol, centerOpacity); if(centerStyle == 0) { DrawThickLine(canvas, xL_p, yLc, xR_p, yRc, centerWidth, centerArgb); } else { int cpat[]; const int cn = BuildLineStylePattern(centerStyle, centerWidth, cpat); if(cn > 0) WidgetDashedLineAA(canvas, xL_p, yLc, xR_p, yRc, centerWidth, centerArgb, cpat); else DrawThickLine(canvas, xL_p, yLc, xR_p, yRc, centerWidth, centerArgb); } } //--- Render the Pearson R label only when visibility is enabled if(pearsonVisible) { int fontPxSize = 9; string fontName = "Arial"; string rText = DoubleToString(pearsonR, 17); //--- Set font for the upcoming TextGetSize / TextOut calls TextSetFont(fontName, -(fontPxSize * 10)); uint twU = 0, thU = 0; TextGetSize(rText, twU, thU); int tw = (int)twU, th = (int)thU; if(tw > 0 && th > 0) { uint textArgb = ColorWithPercentOpacity(labelCol, textOpacity); //--- First pass: render on black background to extract ink coverage uint bufB[]; ArrayResize(bufB, tw * th); ArrayFill(bufB, 0, tw * th, 0xFF000000); TextOut(rText, 0, 0, TA_LEFT | TA_TOP, bufB, tw, th, textArgb, COLOR_FORMAT_ARGB_NORMALIZE); //--- Second pass: render on white background to extract ink coverage uint bufW[]; ArrayResize(bufW, tw * th); ArrayFill(bufW, 0, tw * th, 0xFFFFFFFF); TextOut(rText, 0, 0, TA_LEFT | TA_TOP, bufW, tw, th, textArgb, COLOR_FORMAT_ARGB_NORMALIZE); //--- Position the label below the lower band's left endpoint int txtX = xL_p - tw / 2; int txtY = yLl + 4; int cW_t = canvas.Width(); int cH_t = canvas.Height(); //--- Clamp label to stay inside the canvas bounds if(txtX < 0) txtX = 0; if(txtX + tw >= cW_t) txtX = cW_t - tw - 1; if(txtY + th >= cH_t) txtY = cH_t - th - 1; //--- Extract source RGB from the label color uchar srcR = (uchar)((labelCol) & 0xFF); uchar srcG = (uchar)((labelCol >> 8) & 0xFF); uchar srcB = (uchar)((labelCol >> 16) & 0xFF); //--- Composite each glyph pixel onto the canvas using alpha from ink coverage for(int py = 0; py < th; py++) { for(int px = 0; px < tw; px++) { int i = py * tw + px; //--- Derive per-pixel alpha from the black/white render difference int dR = (int)((bufW[i] >> 16) & 0xFF) - (int)((bufB[i] >> 16) & 0xFF); int dG = (int)((bufW[i] >> 8) & 0xFF) - (int)((bufB[i] >> 8) & 0xFF); int dB = (int)( bufW[i] & 0xFF) - (int)( bufB[i] & 0xFF); int a = 255 - (dR + dG + dB) / 3; //--- Skip transparent pixels if(a <= 0) continue; if(a > 255) a = 255; int dstX = txtX + px, dstY = txtY + py; //--- Reject pixels that land outside the canvas if(dstX < 0 || dstX >= cW_t || dstY < 0 || dstY >= cH_t) continue; uint existing = canvas.PixelGet(dstX, dstY); //--- Blend label pixel over the existing canvas pixel double sA = (double)a / 255.0; double dA = ((existing >> 24) & 0xFF) / 255.0; double oA = sA + dA * (1.0 - sA); if(oA <= 0.0) continue; double sR = srcR / 255.0, sG = srcG / 255.0, sB = srcB / 255.0; double dR_ = ((existing >> 16) & 0xFF) / 255.0; double dG_ = ((existing >> 8) & 0xFF) / 255.0; double dB_ = ( existing & 0xFF) / 255.0; uint outPix = ((uint)(uchar)(oA * 255.0 + 0.5) << 24) | ((uint)(uchar)((sR*sA + dR_*dA*(1.0-sA)) / oA * 255.0 + 0.5) << 16) | ((uint)(uchar)((sG*sA + dG_*dA*(1.0-sA)) / oA * 255.0 + 0.5) << 8) | (uint)(uchar)((sB*sA + dB_*dA*(1.0-sA)) / oA * 255.0 + 0.5); canvas.PixelSet(dstX, dstY, outPix); } } } } //--- Draw 2 handles at center-line endpoints when selected or hovered if(selected || hovered) { if(m_hideHandleIdx != 0) DrawHandleOnCanvas(canvas, xL_p, yLc, selected, lineCol, m_haloHandleIdx == 0); if(m_hideHandleIdx != 1) DrawHandleOnCanvas(canvas, xR_p, yRc, selected, lineCol, m_haloHandleIdx == 1); } }
Сначала мы ограничиваем толщину граничных линий и центральной линии поддерживаемым диапазоном от 1 до 4, их стили — диапазоном от 0 до 3, а значения сигмы — снизу величиной 0,1, чтобы ширина полос никогда не становилась нулевой. Затем мы заменяем входные значения цвета clrNONE значениями по умолчанию для соответствующих элементов: для центральной линии используется цвет объекта, для верхней заливки — DodgerBlue, для нижней — Crimson, а для подписи с коэффициентом корреляции Пирсона — цвет объекта; после этого с помощью "ColorWithPercentOpacity" формируем значения ARGB для границ полос и заливок.
Сама регрессия рассчитывается по классическим формулам метода наименьших квадратов, как и в "ComputeRegressionEndpoints", но с дополнительным накоплением суммы квадратов Y, чтобы можно было также вычислить коэффициент корреляции Пирсона. С помощью функции iBarShift мы определяем индексы более раннего и более позднего баров, накапливаем четыре суммы (X, Y, X², XY), а также сумму Y² по всем допустимым значениям цен закрытия, затем вычисляем угловой коэффициент и свободный член. Коэффициент корреляции Пирсона R рассчитывается по стандартной формуле: из произведения числа баров на сумму произведений XY вычитается произведение суммы X на сумму Y, а результат делится на квадратный корень из произведения знаменателей, связанных с дисперсиями X и Y. На втором этапе выполняется повторный проход по барам для накопления суммы квадратов остатков между каждой ценой закрытия и прогнозируемой ценой, на основе чего вычисляется сигма как стандартное отклонение остатков. Затем для обеих конечных точек рассчитываются цены центральной линии и границ верхней и нижней полос: вычисляется значение прямой, к которому прибавляется или из которого вычитается сигма, умноженная на заданный пользователем множитель сигмы для соответствующей полосы.
Для преобразования каждой пары "время/цена" в пиксельные координаты холста используется функция ChartTimePriceToXY, а верхнюю и нижнюю полосы мы заливаем с помощью "ChannelFillQuad", чтобы каждая полоса корректно смешивалась с любым фоном под ней. Две граничные линии полос рисуются с помощью "DrawThickLine" для сплошного стиля или "WidgetDashedLineAA" для штриховых стилей, шаблон которых формируется с помощью "BuildLineStylePattern". Центральная линия рисуется по той же логике, но с независимо задаваемыми стилем, толщиной и непрозрачностью, поэтому пользователи могут сделать ее штриховой, даже если границы полос сплошные.
Для подписи с коэффициентом корреляции Пирсона R используется двухпроходная отрисовка на черном и белом фоне, позволяющая извлечь альфа-канал глифов. Мы выводим текст в чёрный и белый буфер, предварительно измерив его с помощью функций TextSetFont и TextGetSize. Для каждого пикселя мы вычисляем значение альфа-канала по разности двух результатов отрисовки: там, где глиф полностью непрозрачен, значения в обоих буферах совпадают; там, где глиф отсутствует, черный фон остается черным, а белый — белым; там, где пиксель лишь частично покрыт глифом, разность значений цветовых каналов точно показывает, в какой степени просвечивает фон. Значение альфа-канала каждого пикселя равно 255 минус средняя по цветовым каналам разность между результатами отрисовки на белом и чёрном фоне. Затем мы накладываем каждый пиксель глифа на холст с использованием режима наложения "over" по алгоритму Портера-Даффа. Наконец, когда канал выбран или на него наведён курсор, мы рисуем два маркера на концах центральной линии с помощью "DrawHandleOnCanvas", учитывая индексы скрытия и ореола, чтобы маркеры корректно отображались при перетаскивании или наведении курсора. Теперь мы можем определить логику проверки попадания для этого канала, поскольку для остальных каналов используется тот же подход.
Проверка попадания для регрессионного канала и канала стандартного отклонения
Проверка попадания для регрессионного канала требует пересчёта регрессии в каждом кадре при перемещении курсора, поскольку положения линий зависят от текущей динамики цены, а не от сохранённых опорных точек. Мы определяем "HitTestRegressionChannel" именно для этой цели, а "HitTestStdDevChannel" использует ту же логику, поскольку оба инструмента имеют одинаковую геометрию, состоящую из трёх линий.
//+------------------------------------------------------------------+ //| Hit test the regression channel body (recomputes regression) | //+------------------------------------------------------------------+ bool CChannelTools::HitTestRegressionChannel(int mx, int my, long chartId, datetime t1, datetime t2, int threshold) { //--- Identify chronologically left and right datetimes datetime tL = (t1 < t2) ? t1 : t2; datetime tR = (t1 < t2) ? t2 : t1; //--- Map datetimes to bar indices int barR = iBarShift(_Symbol, _Period, tR, false); int barL = iBarShift(_Symbol, _Period, tL, false); //--- Ensure barL holds the older bar if(barL < barR) { int tmp = barL; barL = barR; barR = tmp; } int nBars = barL - barR + 1; //--- Abort if too few bars for a regression if(nBars < 2) return false; //--- Accumulate OLS sums for slope and intercept double Sx = 0, Sy = 0, Sxx = 0, Sxy = 0; for(int i = 0; i < nBars; i++) { int shift = barL - i; double cls = iClose(_Symbol, _Period, shift); //--- Skip bars with no valid close if(cls == 0.0) continue; double x = (double)i; Sx += x; Sy += cls; Sxx += x*x; Sxy += x*cls; } double dn = (double)nBars; double denom = dn*Sxx - Sx*Sx; //--- Reject degenerate X-variance if(MathAbs(denom) < 1e-12) return false; //--- Compute slope and intercept double slope = (dn*Sxy - Sx*Sy) / denom; double intercept = (Sy - slope*Sx) / dn; //--- Compute residual sigma double sumSq = 0.0; for(int i = 0; i < nBars; i++) { int shift = barL - i; double cls = iClose(_Symbol, _Period, shift); //--- Skip invalid bars if(cls == 0.0) continue; double pred = slope*(double)i + intercept; double r = cls - pred; sumSq += r*r; } double sigma = MathSqrt(sumSq / dn); //--- Recover bar datetimes for coordinate mapping datetime leftTime = (datetime)iTime(_Symbol, _Period, barL); datetime rightTime = (datetime)iTime(_Symbol, _Period, barR); double leftCP = intercept; double rightCP = slope*(double)(nBars-1) + intercept; //--- Map all six band endpoints to pixel coordinates int xL_p=0, yLc=0, xR_p=0, yRc=0, xt=0, yLu=0, yRu=0, yLl=0, yRl=0; ChartTimePriceToXY(chartId, 0, leftTime, leftCP, xL_p, yLc); ChartTimePriceToXY(chartId, 0, rightTime, rightCP, xR_p, yRc); ChartTimePriceToXY(chartId, 0, leftTime, leftCP + 2.0*sigma, xt, yLu); ChartTimePriceToXY(chartId, 0, rightTime, rightCP + 2.0*sigma, xt, yRu); ChartTimePriceToXY(chartId, 0, leftTime, leftCP - 2.0*sigma, xt, yLl); ChartTimePriceToXY(chartId, 0, rightTime, rightCP - 2.0*sigma, xt, yRl); //--- Test all 3 lines: upper band, lower band, center regression if(PointToSegmentDistance(mx, my, xL_p, yLu, xR_p, yRu) <= threshold) return true; if(PointToSegmentDistance(mx, my, xL_p, yLl, xR_p, yRl) <= threshold) return true; if(PointToSegmentDistance(mx, my, xL_p, yLc, xR_p, yRc) <= threshold) return true; return false; } //+------------------------------------------------------------------+ //| Hit test the std deviation channel body | //+------------------------------------------------------------------+ bool CChannelTools::HitTestStdDevChannel(int mx, int my, long chartId, datetime t1, datetime t2, int threshold) { //--- Std dev channel shares identical 3-line geometry with regression; delegate return HitTestRegressionChannel(mx, my, chartId, t1, t2, threshold); }
Здесь мы сначала повторяем те же подготовительные вычисления для регрессии, что и в процедуре рисования: определяем индексы более старого и более нового баров с помощью "iBarShift", накапливаем суммы для метода наименьших квадратов (X, Y, X², XY), исключаем вырожденный случай и вычисляем угловой коэффициент и свободный член. При втором проходе по барам накапливается сумма квадратов остатков, что позволяет вычислить сигму. Затем на обоих концах диапазона баров вычисляются цены конечных точек центральной линии и границ верхней и нижней полос; для границ полос используется фиксированное смещение на две сигмы.
С помощью функции ChartTimePriceToXY мы преобразуем все шесть конечных точек в координаты пикселей и проверяем положение курсора относительно каждой из трёх линий — верхней полосы, нижней полосы и центральной линии регрессии — с помощью унаследованной вспомогательной функции "PointToSegmentDistance". При попадании на любую из трёх линий возвращается true. "HitTestStdDevChannel" — однострочная функция, которая делегирует проверку функции проверки попадания для регрессионного канала, поскольку геометрия полос у обоих инструментов одинакова. После интеграции этой логики в движок рисования получаем следующий результат.

Завершив этот этап, мы полностью реализуем объект "вилы Эндрюса" и применим тот же подход к остальным разновидностям вил.
Рисование вил Эндрюса и проверка попадания
Вилы Эндрюса отображаются в виде шести линий: основания между P2 и P3, медианной линии в виде луча из P1 через середину отрезка P2–P3, двух внешних линий, параллельных медианной и начинающихся в P2 и P3, и двух внутренних линий, расположенных посередине между каждой внешней линией и медианной. Мы определяем функцию "DrawAndrewsPitchforkOn" для рисования всех шести линий и полупрозрачной заливки полос между ними, а функцию "HitTestAndrewsPitchfork" — для проверки попадания курсора на каждую нарисованную линию.
//+------------------------------------------------------------------+ //| Extend a pitchfork ray to the right (or nearest) canvas edge | //+------------------------------------------------------------------+ void PitchforkExtendToRight(int canvasW, int canvasH, int x0, int y0, double dx, double dy, int &ex, int &ey) { //--- Positive dx: intersect the right canvas edge if(dx > 0.5) { double t = ((double)(canvasW - 1) - (double)x0) / dx; ex = canvasW - 1; ey = (int)MathRound((double)y0 + dy * t); } else if(dx < -0.5) { //--- Negative dx: intersect the left canvas edge instead double t = ((double)0 - (double)x0) / dx; ex = 0; ey = (int)MathRound((double)y0 + dy * t); } else { //--- Near-vertical line: extend to the top or bottom edge based on dy sign if(dy >= 0) { ex = x0; ey = canvasH - 1; } else { ex = x0; ey = 0; } } } //+------------------------------------------------------------------+ //| Draw Andrew's Pitchfork onto the canvas | //+------------------------------------------------------------------+ void CChannelTools::DrawAndrewsPitchforkOn(CCanvas &canvas, int x1, int y1, int x2, int y2, int x3, int y3, color objColor, bool selected, bool hovered, bool medianVisible, color medianColor, int medianWidth, int medianStyle, bool outerVisible, color outerColor, int outerWidth, int outerStyle, bool innerVisible, color innerColor, int innerWidth, int innerStyle, int lineOpacity, int fillOpacity) { const int cW = canvas.Width(); const int cH = canvas.Height(); //--- Build per-group ARGBs honoring the shared user opacity const uint medArgb = ColorWithPercentOpacity(medianColor, lineOpacity); const uint outArgb = ColorWithPercentOpacity(outerColor, lineOpacity); const uint innArgb = ColorWithPercentOpacity(innerColor, lineOpacity); //--- Compute the midpoint of P2-P3 through which the median passes const int mx = (x2 + x3) / 2; const int my = (y2 + y3) / 2; //--- Build the median direction vector from P1 toward the midpoint const double mdx = (double)(mx - x1); const double mdy = (double)(my - y1); const double mlen = MathSqrt(mdx * mdx + mdy * mdy); //--- Reject degenerate pivot where P1 equals the midpoint if(mlen < 1.0) return; //--- Extend each of the three primary lines to the right canvas edge int medEndX = 0, medEndY = 0; int upEndX = 0, upEndY = 0; int loEndX = 0, loEndY = 0; PitchforkExtendToRight(cW, cH, mx, my, mdx, mdy, medEndX, medEndY); PitchforkExtendToRight(cW, cH, x2, y2, mdx, mdy, upEndX, upEndY); PitchforkExtendToRight(cW, cH, x3, y3, mdx, mdy, loEndX, loEndY); //--- Compute inner (green) anchor points at 50% between each outer and the midpoint const int gUpStartX = (x2 + mx) / 2, gUpStartY = (y2 + my) / 2; const int gLoStartX = (x3 + mx) / 2, gLoStartY = (y3 + my) / 2; //--- Compute inner extension endpoints midway between outer ends and median end const int gUpEndX = (upEndX + medEndX) / 2, gUpEndY = (upEndY + medEndY) / 2; const int gLoEndX = (loEndX + medEndX) / 2, gLoEndY = (loEndY + medEndY) / 2; //--- Build fill ARGBs for the outer and inner band regions const uint outerFill = ColorWithPercentOpacity(outerColor, fillOpacity); const uint innerFill = ColorWithPercentOpacity(innerColor, fillOpacity); //--- Fill outer bands only when both outer and inner groups are visible if(outerVisible && innerVisible) { ChannelFillQuad(canvas, x2, y2, upEndX, upEndY, gUpEndX, gUpEndY, gUpStartX, gUpStartY, outerFill); ChannelFillQuad(canvas, gLoStartX, gLoStartY, gLoEndX, gLoEndY, loEndX, loEndY, x3, y3, outerFill); } //--- Fill inner (green) band when inner group is visible if(innerVisible) { ChannelFillQuad(canvas, gUpStartX, gUpStartY, gUpEndX, gUpEndY, gLoEndX, gLoEndY, gLoStartX, gLoStartY, innerFill); } //--- Stroke the median group: base line P2-P3 plus median ray P1->medEnd if(medianVisible) { const int thM = (medianWidth > 0) ? medianWidth : 2; if(medianStyle == 0) { DrawThickLine(canvas, x2, y2, x3, y3, thM, medArgb); DrawThickLine(canvas, x1, y1, medEndX, medEndY, thM, medArgb); } else { int pat[]; const int pn = BuildLineStylePattern(medianStyle, thM, pat); if(pn > 0) { WidgetDashedLineAA(canvas, x2, y2, x3, y3, thM, medArgb, pat); WidgetDashedLineAA(canvas, x1, y1, medEndX, medEndY, thM, medArgb, pat); } else { DrawThickLine(canvas, x2, y2, x3, y3, thM, medArgb); DrawThickLine(canvas, x1, y1, medEndX, medEndY, thM, medArgb); } } } //--- Stroke the outer (blue) parallels from P2 and P3 if(outerVisible) { const int thO = (outerWidth > 0) ? outerWidth : 2; if(outerStyle == 0) { DrawThickLine(canvas, x2, y2, upEndX, upEndY, thO, outArgb); DrawThickLine(canvas, x3, y3, loEndX, loEndY, thO, outArgb); } else { int pat[]; const int pn = BuildLineStylePattern(outerStyle, thO, pat); if(pn > 0) { WidgetDashedLineAA(canvas, x2, y2, upEndX, upEndY, thO, outArgb, pat); WidgetDashedLineAA(canvas, x3, y3, loEndX, loEndY, thO, outArgb, pat); } else { DrawThickLine(canvas, x2, y2, upEndX, upEndY, thO, outArgb); DrawThickLine(canvas, x3, y3, loEndX, loEndY, thO, outArgb); } } } //--- Stroke the inner (green) parallels at 50% between each outer and median if(innerVisible) { const int thI = (innerWidth > 0) ? innerWidth : 2; if(innerStyle == 0) { DrawThickLine(canvas, gUpStartX, gUpStartY, gUpEndX, gUpEndY, thI, innArgb); DrawThickLine(canvas, gLoStartX, gLoStartY, gLoEndX, gLoEndY, thI, innArgb); } else { int pat[]; const int pn = BuildLineStylePattern(innerStyle, thI, pat); if(pn > 0) { WidgetDashedLineAA(canvas, gUpStartX, gUpStartY, gUpEndX, gUpEndY, thI, innArgb, pat); WidgetDashedLineAA(canvas, gLoStartX, gLoStartY, gLoEndX, gLoEndY, thI, innArgb, pat); } else { DrawThickLine(canvas, gUpStartX, gUpStartY, gUpEndX, gUpEndY, thI, innArgb); DrawThickLine(canvas, gLoStartX, gLoStartY, gLoEndX, gLoEndY, thI, innArgb); } } } //--- Draw 3 handles at the user-defined anchor points when selected or hovered if(selected || hovered) { color handleColor = (hovered || selected) ? clrDodgerBlue : objColor; if(m_hideHandleIdx != 0) DrawHandleOnCanvas(canvas, x1, y1, selected, handleColor, m_haloHandleIdx == 0); if(m_hideHandleIdx != 1) DrawHandleOnCanvas(canvas, x2, y2, selected, handleColor, m_haloHandleIdx == 1); if(m_hideHandleIdx != 2) DrawHandleOnCanvas(canvas, x3, y3, selected, handleColor, m_haloHandleIdx == 2); } } //+------------------------------------------------------------------+ //| Hit test Andrew's Pitchfork body (all 6 drawn lines) | //+------------------------------------------------------------------+ bool CChannelTools::HitTestAndrewsPitchfork(int mx_h, int my_h, int canvasW, int canvasH, int x1, int y1, int x2, int y2, int x3, int y3, int threshold) { //--- Reconstruct the midpoint of P2-P3 int mpx = (x2 + x3) / 2; int mpy = (y2 + y3) / 2; double mdx = (double)(mpx - x1); double mdy = (double)(mpy - y1); double mlen = MathSqrt(mdx * mdx + mdy * mdy); //--- Reject degenerate pivot if(mlen < 1.0) return false; //--- Extend the three primary rays to the canvas edge int medEndX = 0, medEndY = 0; int upEndX = 0, upEndY = 0; int loEndX = 0, loEndY = 0; PitchforkExtendToRight(canvasW, canvasH, mpx, mpy, mdx, mdy, medEndX, medEndY); PitchforkExtendToRight(canvasW, canvasH, x2, y2, mdx, mdy, upEndX, upEndY); PitchforkExtendToRight(canvasW, canvasH, x3, y3, mdx, mdy, loEndX, loEndY); //--- Reconstruct the inner (green) start and end points int gUpStartX = (x2 + mpx) / 2, gUpStartY = (y2 + mpy) / 2; int gLoStartX = (x3 + mpx) / 2, gLoStartY = (y3 + mpy) / 2; int gUpEndX = (upEndX + medEndX) / 2, gUpEndY = (upEndY + medEndY) / 2; int gLoEndX = (loEndX + medEndX) / 2, gLoEndY = (loEndY + medEndY) / 2; //--- Test each of the 6 lines in order: base, median, outer up, outer lo, inner up, inner lo if(PointToSegmentDistance(mx_h, my_h, x2, y2, x3, y3) <= threshold) return true; if(PointToSegmentDistance(mx_h, my_h, x1, y1, medEndX, medEndY) <= threshold) return true; if(PointToSegmentDistance(mx_h, my_h, x2, y2, upEndX, upEndY) <= threshold) return true; if(PointToSegmentDistance(mx_h, my_h, x3, y3, loEndX, loEndY) <= threshold) return true; if(PointToSegmentDistance(mx_h, my_h, gUpStartX, gUpStartY, gUpEndX, gUpEndY) <= threshold) return true; if(PointToSegmentDistance(mx_h, my_h, gLoStartX, gLoStartY, gLoEndX, gLoEndY) <= threshold) return true; return false; }
Сначала мы определяем "PitchforkExtendToRight" — небольшую вспомогательную функцию, не являющуюся методом класса, которая продлевает луч из начальной точки в заданном направлении до первого пересечения с границей холста. Если X-компонента вектора направления положительна и достаточно велика по модулю, мы находим пересечение с правым краем; если отрицательна — с левым; в противном случае выбираем верхний или нижний край в зависимости от знака Y-компоненты. Благодаря этому каждая линия вил выглядит как луч, уходящий за край графика.
В функции "DrawAndrewsPitchforkOn" мы формируем значения ARGB для каждой группы с помощью "ColorWithPercentOpacity", чтобы медианная, внешние и внутренние линии сохраняли собственные цвета при общей непрозрачности, заданной пользователем. Мы вычисляем середину отрезка P2–P3, определяем вектор направления медианной линии от P1 к этой середине и исключаем вырожденный случай, когда P1 точно совпадает с ней. Конечные точки трех основных лучей (медианной, верхней и нижней линий) вычисляются путем их продления из соответствующих начальных точек в направлении медианной линии, а опорные точки внутренних линий размещаются посередине между каждой внешней линией и медианной — как в начале, так и на краю холста.
Затем с помощью "ChannelFillQuad" выполняется полупрозрачная заливка полос: внешние полосы занимают области между каждой внешней линией и соответствующей внутренней, а внутренняя полоса — область между двумя внутренними линиями. Каждая заливка выполняется только при установленном флаге видимости, поэтому пользователи могут отключать любую группу полос независимо от остальных. Затем отрисовываются шесть линий: для сплошных используется "DrawThickLine", а для штриховых — "WidgetDashedLineAA" с шаблоном, сформированным функцией "BuildLineStylePattern". Наконец, когда вилы выделены или на них наведен курсор, мы рисуем три маркера в заданных пользователем опорных точках P1, P2 и P3 с помощью функции "DrawHandleOnCanvas".
"HitTestAndrewsPitchfork" воссоздает те же шесть линий с использованием идентичной геометрии и поочередно проверяет положение курсора относительно каждой из них с помощью унаследованной вспомогательной функции "PointToSegmentDistance". Проверка выполняется в следующем порядке: основание, медианная линия, верхняя внешняя, нижняя внешняя, верхняя внутренняя и нижняя внутренняя линии; попадание на любую из этих шести линий сразу приводит к возврату true. Мы используем тот же подход для определения остальных объектов. В результате получается следующее.

Для более наглядного сравнения мы добавили встроенный инструмент терминала "Вилы Эндрюса". Мы видим, что эта же структура сохраняется. В будущем мы добавим окно настроек, в котором можно будет легко изменять значения по умолчанию и добавлять новые уровни, точно так же, как это делается в терминале. Успешно завершив этот этап, мы перейдем к реализации инструментов Ганна, которой посвящен следующий раздел.
Реализация инструментов Ганна и Фибоначчи
Отрисовка веера Ганна и проверка попадания
Веер Ганна отображает девять лучей, исходящих из точки P1, с каноническими отношениями цены ко времени (8/1, 4/1, 3/1, 2/1, 1/1, 1/2, 1/3, 1/4, 1/8), полупрозрачными клиновидными заливками между соседними видимыми лучами, метками коэффициентов, привязанными к перекрестию P2, и двумя маркерами в опорных точках. Мы также вводим две свободные вспомогательные функции — "GannDrawLabel" для отрисовки меток и "GannExtendRay" для отсечения лучей по границам холста, — которые повторно используются в остальных инструментах Ганна и Фибоначчи.
//+------------------------------------------------------------------+ //| Draw a text label with 2-pass black/white alpha extraction | //+------------------------------------------------------------------+ void GannDrawLabel(CCanvas &canvas, const string text, int anchorX, int anchorY, color textColor, int fontPxSize = 8) { //--- Skip empty strings immediately if(StringLen(text) == 0) return; //--- Set the font for the upcoming size query and render calls TextSetFont("Arial", -(fontPxSize * 10)); uint twU = 0, thU = 0; TextGetSize(text, twU, thU); int tw = (int)twU, th = (int)thU; //--- Skip if the font engine returned a zero-size bounding box if(tw <= 0 || th <= 0) return; uint textArgb = ColorToARGB(textColor, 255); //--- First pass: render on black to record ink coverage per pixel uint bufB[]; ArrayResize(bufB, tw * th); ArrayFill(bufB, 0, tw * th, 0xFF000000); TextOut(text, 0, 0, TA_LEFT | TA_TOP, bufB, tw, th, textArgb, COLOR_FORMAT_ARGB_NORMALIZE); //--- Second pass: render on white to record ink coverage per pixel uint bufW[]; ArrayResize(bufW, tw * th); ArrayFill(bufW, 0, tw * th, 0xFFFFFFFF); TextOut(text, 0, 0, TA_LEFT | TA_TOP, bufW, tw, th, textArgb, COLOR_FORMAT_ARGB_NORMALIZE); int txtX = anchorX, txtY = anchorY; int cW = canvas.Width(), cH = canvas.Height(); //--- Skip labels whose bounding box falls entirely outside the canvas if(txtX + tw < 0 || txtX >= cW) return; if(txtY + th < 0 || txtY >= cH) return; //--- Extract source RGB from the caller-supplied text color uchar srcR = (uchar)((textColor) & 0xFF); uchar srcG = (uchar)((textColor >> 8) & 0xFF); uchar srcB = (uchar)((textColor >> 16) & 0xFF); //--- Composite each glyph pixel onto the canvas for(int py = 0; py < th; py++) { for(int px = 0; px < tw; px++) { int i = py * tw + px; //--- Derive per-pixel alpha from the black/white render difference int dR = (int)((bufW[i] >> 16) & 0xFF) - (int)((bufB[i] >> 16) & 0xFF); int dG = (int)((bufW[i] >> 8) & 0xFF) - (int)((bufB[i] >> 8) & 0xFF); int dB = (int)( bufW[i] & 0xFF) - (int)( bufB[i] & 0xFF); int a = 255 - (dR + dG + dB) / 3; //--- Skip transparent pixels if(a <= 0) continue; if(a > 255) a = 255; int dstX = txtX + px, dstY = txtY + py; //--- Reject pixels outside the canvas if(dstX < 0 || dstX >= cW || dstY < 0 || dstY >= cH) continue; uint existing = canvas.PixelGet(dstX, dstY); //--- Blend the label pixel over the canvas background double sA = (double)a / 255.0; double dA = ((existing >> 24) & 0xFF) / 255.0; double oA = sA + dA * (1.0 - sA); if(oA <= 0.0) continue; double sRf = srcR / 255.0, sGf = srcG / 255.0, sBf = srcB / 255.0; double dRf = ((existing >> 16) & 0xFF) / 255.0; double dGf = ((existing >> 8) & 0xFF) / 255.0; double dBf = ( existing & 0xFF) / 255.0; uint outPix = ((uint)(uchar)(oA * 255.0 + 0.5) << 24) | ((uint)(uchar)((sRf*sA + dRf*dA*(1.0-sA)) / oA * 255.0 + 0.5) << 16) | ((uint)(uchar)((sGf*sA + dGf*dA*(1.0-sA)) / oA * 255.0 + 0.5) << 8) | (uint)(uchar)((sBf*sA + dBf*dA*(1.0-sA)) / oA * 255.0 + 0.5); canvas.PixelSet(dstX, dstY, outPix); } } } //+------------------------------------------------------------------+ //| Extend a ray from (x0, y0) in direction (dx, dy) to canvas edge | //+------------------------------------------------------------------+ void GannExtendRay(int canvasW, int canvasH, int x0, int y0, double dx, double dy, int &outX, int &outY) { double tMax = 1e18; //--- Test intersection with the right edge if(dx > 1e-9) { double t = ((double)(canvasW - 1) - (double)x0) / dx; if(t < tMax) tMax = t; } //--- Test intersection with the bottom edge if(dy > 1e-9) { double t = ((double)(canvasH - 1) - (double)y0) / dy; if(t < tMax) tMax = t; } //--- Test intersection with the top edge if(dy < -1e-9) { double t = (0.0 - (double)y0) / dy; if(t < tMax) tMax = t; } //--- Clamp to zero if no valid forward intersection was found if(tMax < 0 || tMax > 1e17) tMax = 0; //--- Write the resulting canvas-edge endpoint outX = (int)MathRound((double)x0 + dx * tMax); outY = (int)MathRound((double)y0 + dy * tMax); } //+------------------------------------------------------------------+ //| Draw Gann Fan: 9 rays from P1 at canonical Gann ratios | //+------------------------------------------------------------------+ void CChannelTools::DrawGannFanOn(CCanvas &canvas, int x1, int y1, int x2, int y2, color objColor, bool selected, bool hovered, const double &lvlRatio[], const color &lvlColor[], const int &lvlOpacity[], const int &lvlWidth[], const int &lvlStyle[], const bool &lvlVisible[], int fillOpacity) { const int cW = canvas.Width(), cH = canvas.Height(); //--- Derive the unit scale from the P1->P2 vector; dxUnit is the time unit const double dxUnit = (double)(x2 - x1); const double dyUnit = (double)(y2 - y1); //--- Reject a degenerate zero-horizontal extent if(MathAbs(dxUnit) < 1e-6) return; const int nLev = ArraySize(lvlRatio); //--- Nothing to draw if no levels are defined if(nLev <= 0) return; //--- Pre-compute each ray's canvas-edge endpoint const int MAX_GFAN = 32; int endX[]; ArrayResize(endX, MAX_GFAN); int endY[]; ArrayResize(endY, MAX_GFAN); for(int i = 0; i < nLev; i++) { const double dxR = dxUnit; const double dyR = dyUnit * lvlRatio[i]; int eX = x2, eY = y2; GannExtendRay(cW, cH, x1, y1, dxR, dyR, eX, eY); endX[i] = eX; endY[i] = eY; } //--- Draw translucent wedge fills between adjacent visible rays int cornerPts[4][2] = { {cW - 1, 0}, {cW - 1, cH - 1}, {0, cH - 1}, {0, 0} }; int prevVis = -1; for(int i = 0; i < nLev; i++) { if(!lvlVisible[i]) continue; //--- Need at least two visible rays to form a wedge if(prevVis < 0) { prevVis = i; continue; } const uint fillArgb = ColorWithPercentOpacity(lvlColor[i], fillOpacity); const int eaX = endX[prevVis], eaY = endY[prevVis]; const int ebX = endX[i], ebY = endY[i]; //--- Determine which canvas edge each ray endpoint sits on int edgeA = -1, edgeB = -1; if(eaY <= 0) edgeA = 0; else if(eaX >= cW - 1) edgeA = 1; else if(eaY >= cH - 1) edgeA = 2; else edgeA = 3; if(ebY <= 0) edgeB = 0; else if(ebX >= cW - 1) edgeB = 1; else if(ebY >= cH - 1) edgeB = 2; else edgeB = 3; //--- Build the wedge polygon from P1, the two ray endpoints, and any corner vertices int polyX[8], polyY[8]; int nPts = 0; polyX[nPts] = x1; polyY[nPts] = y1; nPts++; polyX[nPts] = eaX; polyY[nPts] = eaY; nPts++; //--- Determine winding direction to walk corners in the correct order const double aDx = (double)(eaX - x1), aDy = (double)(eaY - y1); const double bDx = (double)(ebX - x1), bDy = (double)(ebY - y1); const double cross = aDx * bDy - aDy * bDx; const int step = (cross >= 0.0) ? 1 : 3; //--- Walk canvas corners from edgeA to edgeB in the correct winding if(edgeA != edgeB) { int e = edgeA; int guard = 0; while(e != edgeB && guard < 4) { int cIdx = (step == 1) ? e : ((e + 3) % 4); polyX[nPts] = cornerPts[cIdx][0]; polyY[nPts] = cornerPts[cIdx][1]; nPts++; e = (e + step) % 4; guard++; } } polyX[nPts] = ebX; polyY[nPts] = ebY; nPts++; //--- Scanline rasterize the polygon with even-odd rule int ymin = polyY[0], ymax = polyY[0]; for(int k = 1; k < nPts; k++) { if(polyY[k] < ymin) ymin = polyY[k]; if(polyY[k] > ymax) ymax = polyY[k]; } //--- Clamp vertical range to canvas if(ymin < 0) ymin = 0; if(ymax >= cH) ymax = cH - 1; for(int yy = ymin; yy <= ymax; yy++) { double xs[16]; int nx = 0; //--- Intersect each polygon edge with the current scanline for(int e = 0; e < nPts; e++) { int e2 = (e + 1) % nPts; double ay = (double)polyY[e], by_ = (double)polyY[e2]; if((ay > yy) == (by_ > yy)) continue; double ax = (double)polyX[e], bx_ = (double)polyX[e2]; double tt = ((double)yy - ay) / (by_ - ay); if(nx < 16) xs[nx++] = ax + tt * (bx_ - ax); } if(nx < 2) continue; //--- Sort intersection X values for span fill for(int a = 0; a < nx - 1; a++) for(int b = a + 1; b < nx; b++) if(xs[b] < xs[a]) { double tmp = xs[a]; xs[a] = xs[b]; xs[b] = tmp; } //--- Fill each span between paired intersections for(int p = 0; p + 1 < nx; p += 2) { int sxL = (int)MathCeil(xs[p]); int sxR = (int)MathFloor(xs[p + 1]); if(sxL < 0) sxL = 0; if(sxR >= cW) sxR = cW - 1; for(int xx = sxL; xx <= sxR; xx++) ChannelBlendPixelSet(canvas, xx, yy, fillArgb); } } prevVis = i; } //--- Draw the rays on top of the fills, gated by visibility for(int i = 0; i < nLev; i++) { if(!lvlVisible[i]) continue; const color lCol = lvlColor[i]; const uint lArgb = ColorWithPercentOpacity(lCol, lvlOpacity[i]); const int thick = (lvlWidth[i] > 0) ? lvlWidth[i] : 2; const int style = lvlStyle[i]; if(style == 0) DrawThickLine(canvas, x1, y1, endX[i], endY[i], thick, lArgb); else { int pat[]; const int pn = BuildLineStylePattern(style, thick, pat); if(pn > 0) WidgetDashedLineAA(canvas, x1, y1, endX[i], endY[i], thick, lArgb, pat); else DrawThickLine(canvas, x1, y1, endX[i], endY[i], thick, lArgb); } } //--- Draw ratio labels at the crosshair through P2 per visible level for(int i = 0; i < nLev; i++) { if(!lvlVisible[i]) continue; const double r = lvlRatio[i]; const double dxR = dxUnit; const double dyR = dyUnit * r; int labX = x2, labY = y2; //--- Anchor the label on the horizontal or vertical P2 crosshair per ratio if(MathAbs(r - 1.0) < 0.001) { //--- r == 1.0 (1/1): label sits at P2 itself labX = x2 + 4; labY = y2 - 10; } else if(r > 1.0) { //--- Steep rays (r > 1): intersect horizontal line y=y2 if(MathAbs(dyR) > 1e-9) { double tt = (double)(y2 - y1) / dyR; labX = (int)MathRound((double)x1 + dxR * tt); labY = y2; } labX += 2; labY -= 12; } else { //--- Shallow rays (r < 1): intersect vertical line x=x2 if(MathAbs(dxR) > 1e-9) { double tt = (double)(x2 - x1) / dxR; labX = x2; labY = (int)MathRound((double)y1 + dyR * tt); } //--- Offset anchor so text box is vertically centered on the line const int gannLabelHalfH = 5; labX += 4; labY -= gannLabelHalfH; } //--- Build the label string: canonical Gann ratios get "p/t" format string lbl = ""; const double tol = 0.005; if(MathAbs(r - 8.0) < tol) lbl = "8/1"; else if(MathAbs(r - 4.0) < tol) lbl = "4/1"; else if(MathAbs(r - 3.0) < tol) lbl = "3/1"; else if(MathAbs(r - 2.0) < tol) lbl = "2/1"; else if(MathAbs(r - 1.0) < tol) lbl = "1/1"; else if(MathAbs(r - 0.5) < tol) lbl = "1/2"; else if(MathAbs(r - (1.0/3.0)) < tol) lbl = "1/3"; else if(MathAbs(r - 0.25) < tol) lbl = "1/4"; else if(MathAbs(r - 0.125) < tol) lbl = "1/8"; else { //--- Non-canonical ratio: format as decimal and strip trailing zeros lbl = DoubleToString(r, 3); while(StringLen(lbl) > 1 && StringSubstr(lbl, StringLen(lbl) - 1, 1) == "0") lbl = StringSubstr(lbl, 0, StringLen(lbl) - 1); //--- Strip a trailing decimal point left after zero removal if(StringLen(lbl) > 0 && StringSubstr(lbl, StringLen(lbl) - 1, 1) == ".") lbl = StringSubstr(lbl, 0, StringLen(lbl) - 1); } GannDrawLabel(canvas, lbl, labX, labY, lvlColor[i], 8); } //--- Draw 2 handles at P1 and P2 when selected or hovered if(selected || hovered) { if(m_hideHandleIdx != 0) DrawHandleOnCanvas(canvas, x1, y1, selected, objColor, m_haloHandleIdx == 0); if(m_hideHandleIdx != 1) DrawHandleOnCanvas(canvas, x2, y2, selected, objColor, m_haloHandleIdx == 1); } } //+------------------------------------------------------------------+ //| Hit test Gann Fan (cursor near any of the 9 rays) | //+------------------------------------------------------------------+ bool CChannelTools::HitTestGannFan(int mx, int my, int canvasW, int canvasH, int x1, int y1, int x2, int y2, int threshold) { //--- Derive the unit scale from P1->P2 double dxUnit = (double)(x2 - x1); double dyUnit = (double)(y2 - y1); //--- Reject zero horizontal extent if(MathAbs(dxUnit) < 1e-6) return false; //--- Canonical Gann price-per-time and time-per-price ratios double ratios_p[9] = {8, 4, 3, 2, 1, 1, 1, 1, 1}; double ratios_t[9] = {1, 1, 1, 1, 1, 2, 3, 4, 8}; //--- Test cursor distance to each of the 9 extended rays for(int i = 0; i < 9; i++) { double dxR = dxUnit * ratios_t[i]; double dyR = dyUnit * ratios_p[i]; int endX = 0, endY = 0; GannExtendRay(canvasW, canvasH, x1, y1, dxR, dyR, endX, endY); if(PointToSegmentDistance(mx, my, x1, y1, endX, endY) <= threshold) return true; } return false; }
Сначала мы определяем "GannDrawLabel", используя тот же двухпроходный метод извлечения альфа-канала на чёрном и белом фоне, который применялся для метки коэффициента корреляции Пирсона R, поскольку API отрисовки текста MQL5 по-прежнему не поддерживает прозрачный фон. Мы определяем размер текста с помощью TextGetSize, дважды отрисовываем его в буферах в памяти с помощью TextOut (один раз на чёрном фоне, другой — на белом) и вычисляем значение альфа-канала каждого пикселя по разности двух изображений, после чего накладываем каждый пиксель глифа на холст по стандартной формуле Портера — Даффа для операции over.
Далее функция "GannExtendRay" продлевает луч из исходной точки в заданном направлении до первой пересекаемой границы холста. Мы вычисляем параметр пересечения с правой, нижней и верхней границами (выбирая те, к которым направлен вектор), выбираем наименьший допустимый параметр и записываем обратно координаты конечной точки, ограниченные границами холста. Эту вспомогательную функцию используют все инструменты на основе лучей из семейств Ганна и Фибоначчи для отсечения лучей по границам холста.
Внутри функции "DrawGannFanOn" мы определяем единичные масштабы времени и цены по вектору от P1 к P2 и исключаем вырожденный случай с нулевой горизонтальной составляющей. Для каждого уровня мы заранее вычисляем конечную точку на границе холста, продлевая луч из P1 с единичным масштабом времени и единичным масштабом цены, умноженным на коэффициент уровня. Затем выполняются полупрозрачные клиновидные заливки: для каждой пары соседних видимых лучей мы строим многоугольник из точки P1, конечных точек двух лучей и всех углов холста, лежащих между этими конечными точками в направлении обхода. Знак, определяющий направление обхода, вычисляется по двумерному векторному произведению двух векторов лучей; в зависимости от знака мы обходим углы в прямом или обратном порядке, чтобы многоугольник оставался выпуклым. Затем выполняется построчная растеризация многоугольника с использованием унаследованного примитива смешивания, благодаря чему заливки корректно накладываются на всё, что находится под ними.
Линии лучей рисуются поверх заливок с помощью функций "DrawThickLine" или "WidgetDashedLineAA" в зависимости от стиля уровня, причем отображение каждой из них управляется соответствующим флагом видимости. Затем выводятся метки, положение которых зависит от коэффициента: у крутых лучей с коэффициентом больше 1,0 метки располагаются на горизонтальной линии перекрестия, проходящей через P2; у пологих лучей с коэффициентом меньше 1,0 — на вертикальной линии перекрестия, проходящей через P2; метка луча 1/1 располагается непосредственно в P2. Строка метки формируется путём сопоставления коэффициента с каноническими значениями Ганна в пределах небольшого допуска и подстановки записи "p/t" (8/1, 4/1 и т. д.); для неканонических коэффициентов используется десятичная запись с удалением конечных нулей. Наконец, когда веер выбран или на него наведён курсор, в точках P1 и P2 отображаются два маркера.
"HitTestGannFan" восстанавливает девять канонических коэффициентов Ганна в виде параллельных массивов цены и времени, продлевает каждый луч до границы холста с помощью той же вспомогательной функции и проверяет попадание курсора на каждый продлённый луч с помощью унаследованной вспомогательной функции "PointToSegmentDistance". При попадании курсора на любой из девяти лучей возвращается true. В результате мы получаем следующее.

Всё получилось. В реализации сетки Ганна используется та же логика. Теперь перейдем к реализации инструментов Фибоначчи. Логика остаётся схожей, но мы вынесем её в отдельный файл, чтобы при необходимости расширения в будущем знать, где она находится.
Объявление класса инструментов Фибоначчи
Переходим от инструментов Ганна к инструментам Фибоначчи, объявляя класс "CFibonacciTools" — следующее звено в цепочке наследования. Этот класс наследуется от "CChannelTools", поэтому все вспомогательные функции для каналов и лучей Ганна, а также пиксельные примитивы, созданные к этому моменту, становятся доступны шести инструментам Фибоначчи без повторного объявления.
//+------------------------------------------------------------------+ //| ToolsPalette_Fibonacci.mqh | //| Copyright 2026, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #ifndef TOOLS_PALETTE_FIBONACCI_MQH #define TOOLS_PALETTE_FIBONACCI_MQH //--- Include the channel tools layer that CFibonacciTools extends #include "ToolsPalette_Channels.mqh" //+------------------------------------------------------------------+ //| Fibonacci tool library: retracement, expansion, channel, | //| time zone, fan, and arcs | //+------------------------------------------------------------------+ class CFibonacciTools : public CChannelTools { protected: color FibColorForLevel(double lvl) const; // Return canonical color for a Fibonacci ratio level void FiboDrawLabel(CCanvas &canvas, const string text, int anchorX, int anchorY, color textColor, int fontPxSize = 8); // Wrap GannDrawLabel for Fib label rendering //--- Trace an anti-aliased parametric circular arc at (cx, cy) with given radius void FiboDrawArc(CCanvas &canvas, int cx, int cy, double radius, double startAngle, double endAngle, uint argb, int thickness); public: //--- Draw Fib Retracement horizontal ratio lines between two price anchors void DrawFibRetracementOn(CCanvas &canvas, int x1, int y1, int x2, int y2, color objColor, bool selected, bool hovered, const double &lvlRatio[], const color &lvlColor[], const int &lvlOpacity[], const int &lvlWidth[], const int &lvlStyle[], const bool &lvlVisible[]); //--- Hit test Fib Retracement body bool HitTestFibRetracement(int mx, int my, int canvasW, int x1, int y1, int x2, int y2, int threshold); //--- Draw Fib Expansion projected extensions from 3-click anchors void DrawFibExpansionOn(CCanvas &canvas, int x1, int y1, int x2, int y2, int x3, int y3, color objColor, bool selected, bool hovered, const double &lvlRatio[], const color &lvlColor[], const int &lvlOpacity[], const int &lvlWidth[], const int &lvlStyle[], const bool &lvlVisible[]); //--- Hit test Fib Expansion body bool HitTestFibExpansion(int mx, int my, int canvasW, int x1, int y1, int x2, int y2, int x3, int y3, int threshold); //--- Draw Fib Channel parallel ratio lines from 3-click anchors void DrawFibChannelOn(CCanvas &canvas, int x1, int y1, int x2, int y2, int x3, int y3, color objColor, bool selected, bool hovered, const double &lvlRatio[], const color &lvlColor[], const int &lvlOpacity[], const int &lvlWidth[], const int &lvlStyle[], const bool &lvlVisible[]); //--- Hit test Fib Channel body bool HitTestFibChannel(int mx, int my, int canvasW, int x1, int y1, int x2, int y2, int x3, int y3, int threshold); //--- Draw Fib Time Zone vertical lines at Fibonacci multiples of the unit interval void DrawFibTimeZoneOn(CCanvas &canvas, int x1, int y1, int x2, int y2, color objColor, bool selected, bool hovered, const double &lvlRatio[], const color &lvlColor[], const int &lvlOpacity[], const int &lvlWidth[], const int &lvlStyle[], const bool &lvlVisible[]); //--- Hit test Fib Time Zone body bool HitTestFibTimeZone(int mx, int my, int canvasH, int x1, int y1, int x2, int y2, int threshold); //--- Draw Fib Speed Resistance Fan price and time ray families void DrawFibFanOn(CCanvas &canvas, int x1, int y1, int x2, int y2, color objColor, bool selected, bool hovered, const double &lvlRatio[], const color &lvlColor[], const int &lvlOpacity[], const int &lvlWidth[], const int &lvlStyle[], const bool &lvlVisible[]); //--- Hit test Fib Fan body bool HitTestFibFan(int mx, int my, int canvasW, int canvasH, int x1, int y1, int x2, int y2, int threshold); //--- Draw Fib Speed Resistance Arcs concentric semicircles void DrawFibArcsOn(CCanvas &canvas, int x1, int y1, int x2, int y2, color objColor, bool selected, bool hovered, const double &lvlRatio[], const color &lvlColor[], const int &lvlOpacity[], const int &lvlWidth[], const int &lvlStyle[], const bool &lvlVisible[]); //--- Hit test Fib Arcs body bool HitTestFibArcs(int mx, int my, int x1, int y1, int x2, int y2, int threshold); };
Для защиты заголовочного файла от повторного включения используем макрос "TOOLS_PALETTE_FIBONACCI_MQH" и подключаем заголовочный файл каналов, чтобы унаследовать весь доступный интерфейс. Сначала объявляются три защищённых вспомогательных метода: "FibColorForLevel" возвращает стандартный цвет для коэффициента Фибоначчи (золотой для 0,5, морской зелёный для 0,618 и так далее), "FiboDrawLabel" служит простой обёрткой над "GannDrawLabel", чтобы код в местах вызова оставался читаемым, а "FiboDrawArc" строит параметрическую дугу со сглаживанием по заданным центру и радиусу — этот метод нужен именно для инструмента "Дуги скоростного сопротивления Фибоначчи".
В открытом интерфейсе объявлены шесть процедур рисования инструментов Фибоначчи и соответствующие им функции проверки попадания. "DrawFibRetracementOn" и "DrawFibTimeZoneOn" — инструменты, задаваемые двумя щелчками, а "DrawFibExpansionOn" и "DrawFibChannelOn" — тремя: им нужна третья опорная точка. "DrawFibFanOn" и "DrawFibArcsOn" завершают семейство двумя вариантами инструментов скоростного сопротивления. Каждая процедура рисования принимает одни и те же шесть массивов параметров уровней (коэффициентов, цветов, непрозрачности, толщины линий, стилей и флагов видимости), поэтому движок рисования может напрямую передавать сохранённую в объекте конфигурацию уровней без её распаковки. Каждая функция проверки попадания принимает необходимые ей размеры холста (ширину, высоту или оба размера), чтобы при вычислении расстояния до курсора отсекать лучи и дуги по границам холста. Объявив интерфейс, переходим к реализации методов. Мы реализуем логику инструмента "Уровни Фибоначчи", а для остальных инструментов используем тот же подход.
Вспомогательные методы Фибоначчи и уровни Фибоначчи
Реализуем три вспомогательных метода Фибоначчи — подбор цвета, обёртку для вывода подписей и построение параметрических дуг, — а затем сам инструмент "Уровни Фибоначчи". Инструмент "Уровни Фибоначчи" отображает между двумя ценовыми опорными точками горизонтальные линии, соответствующие коэффициентам, с полупрозрачной заливкой полос между соседними уровнями.
//+------------------------------------------------------------------+ //| Return the canonical color for a recognized Fibonacci ratio | //+------------------------------------------------------------------+ color CFibonacciTools::FibColorForLevel(double lvl) const { //--- Use a small epsilon for floating-point safe comparison double eps = 1e-4; if(MathAbs(lvl - 0.0) < eps) return clrGray; if(MathAbs(lvl - 0.236) < eps) return clrCrimson; if(MathAbs(lvl - 0.382) < eps) return clrOrange; if(MathAbs(lvl - 0.5) < eps) return clrGoldenrod; if(MathAbs(lvl - 0.618) < eps) return clrSeaGreen; if(MathAbs(lvl - 0.786) < eps) return clrDarkCyan; if(MathAbs(lvl - 1.0) < eps) return clrGray; if(MathAbs(lvl - 1.618) < eps) return clrDodgerBlue; if(MathAbs(lvl - 2.618) < eps) return clrMediumOrchid; if(MathAbs(lvl - 3.618) < eps) return clrBlueViolet; if(MathAbs(lvl - 4.236) < eps) return clrCrimson; if(MathAbs(lvl - 4.618) < eps) return clrDeepPink; //--- Unrecognized ratio: fall back to the project default color return clrDodgerBlue; } //+------------------------------------------------------------------+ //| Wrap GannDrawLabel for Fib-context label rendering | //+------------------------------------------------------------------+ void CFibonacciTools::FiboDrawLabel(CCanvas &canvas, const string text, int anchorX, int anchorY, color textColor, int fontPxSize) { GannDrawLabel(canvas, text, anchorX, anchorY, textColor, fontPxSize); } //+------------------------------------------------------------------+ //| Trace an anti-aliased parametric arc from startAngle to endAngle | //+------------------------------------------------------------------+ void CFibonacciTools::FiboDrawArc(CCanvas &canvas, int cx, int cy, double radius, double startAngle, double endAngle, uint argb, int thickness) { //--- Skip degenerate radii if(radius < 0.5) return; int cW = canvas.Width(), cH = canvas.Height(); //--- Compute step count so each step is roughly 1 pixel of arc length double arcLen = MathAbs(endAngle - startAngle) * radius; int steps = (int)MathCeil(arcLen); if(steps < 8) steps = 8; if(steps > 4000) steps = 4000; double da = (endAngle - startAngle) / (double)steps; //--- 2px thickness uses two offset passes; 1px uses only the centerline int passes = (thickness >= 2) ? 2 : 1; for(int s = 0; s <= steps; s++) { double a = startAngle + da * (double)s; double xCenter = (double)cx + radius * MathCos(a); double yCenter = (double)cy - radius * MathSin(a); // screen Y flip for(int p = 0; p < passes; p++) { double off = (passes == 1) ? 0.0 : (p == 0 ? -0.5 : 0.5); double xf = xCenter + off * MathCos(a); double yf = yCenter + off * (-MathSin(a)); int ix = (int)MathFloor(xf), iy = (int)MathFloor(yf); double dx = xf - ix, dy = yf - iy; //--- 4-pixel bilinear coverage for anti-aliasing double covs[4] = { (1.0-dx)*(1.0-dy), dx*(1.0-dy), (1.0-dx)*dy, dx*dy }; int pxs[4] = { ix, ix+1, ix, ix+1 }; int pys[4] = { iy, iy, iy+1, iy+1 }; for(int k = 0; k < 4; k++) { int px = pxs[k], py = pys[k]; if(px < 0 || px >= cW || py < 0 || py >= cH) continue; double cov = covs[k]; if(cov <= 0.0) continue; //--- Extract source channels and apply coverage-weighted alpha uchar srcA = (uchar)((argb >> 24) & 0xFF); uchar srcR = (uchar)((argb >> 16) & 0xFF); uchar srcG = (uchar)((argb >> 8) & 0xFF); uchar srcB = (uchar)( argb & 0xFF); double effA = (double)srcA / 255.0 * cov; uint existing = canvas.PixelGet(px, py); double dA = ((existing >> 24) & 0xFF) / 255.0; double oA = effA + dA * (1.0 - effA); if(oA <= 0.0) continue; //--- Composite over the existing pixel double dRf = ((existing >> 16) & 0xFF) / 255.0; double dGf = ((existing >> 8) & 0xFF) / 255.0; double dBf = ( existing & 0xFF) / 255.0; double sRf = srcR / 255.0, sGf = srcG / 255.0, sBf = srcB / 255.0; uint outPix = ((uint)(uchar)(oA * 255.0 + 0.5) << 24) | ((uint)(uchar)((sRf*effA + dRf*dA*(1.0-effA)) / oA * 255.0 + 0.5) << 16) | ((uint)(uchar)((sGf*effA + dGf*dA*(1.0-effA)) / oA * 255.0 + 0.5) << 8) | (uint)(uchar)((sBf*effA + dBf*dA*(1.0-effA)) / oA * 255.0 + 0.5); canvas.PixelSet(px, py, outPix); } } } } //+------------------------------------------------------------------+ //| Draw Fib Retracement horizontal ratio lines between two anchors | //+------------------------------------------------------------------+ void CFibonacciTools::DrawFibRetracementOn(CCanvas &canvas, int x1, int y1, int x2, int y2, color objColor, bool selected, bool hovered, const double &lvlRatio[], const color &lvlColor[], const int &lvlOpacity[], const int &lvlWidth[], const int &lvlStyle[], const bool &lvlVisible[]) { const int nLev = ArraySize(lvlRatio); if(nLev <= 0) return; //--- Lines span strictly between P1 and P2 on the X axis const int xL = MathMin(x1, x2); const int xR = MathMax(x1, x2); //--- Fill translucent bands between adjacent visible levels const uchar bandAlpha = 77; int prevVisIdx = -1; for(int i = 0; i < nLev; i++) { if(!lvlVisible[i]) continue; if(prevVisIdx >= 0) { const int ly1 = y1 + (int)MathRound((double)(y2 - y1) * lvlRatio[prevVisIdx]); const int ly2 = y1 + (int)MathRound((double)(y2 - y1) * lvlRatio[i]); const uint fArgb = ColorToARGB(lvlColor[i], bandAlpha); const int yTop = MathMin(ly1, ly2); const int yBot = MathMax(ly1, ly2); ChannelFillQuad(canvas, xL, yTop, xR, yTop, xR, yBot, xL, yBot, fArgb); } prevVisIdx = i; } //--- Draw horizontal ratio lines and labels per visible level for(int i = 0; i < nLev; i++) { if(!lvlVisible[i]) continue; const int ly = y1 + (int)MathRound((double)(y2 - y1) * lvlRatio[i]); const color lCol = lvlColor[i]; const uint lArgb = ColorWithPercentOpacity(lCol, lvlOpacity[i]); const int thick = (lvlWidth[i] > 0) ? lvlWidth[i] : 2; const int style = lvlStyle[i]; if(style == 0) { DrawThickLine(canvas, xL, ly, xR, ly, thick, lArgb); } else { int pat[]; const int pn = BuildLineStylePattern(style, thick, pat); if(pn > 0) WidgetDashedLineAA(canvas, xL, ly, xR, ly, thick, lArgb, pat); else DrawThickLine(canvas, xL, ly, xR, ly, thick, lArgb); } //--- Format ratio label and strip trailing zeros string lbl = DoubleToString(lvlRatio[i], 3); while(StringLen(lbl) > 1 && StringSubstr(lbl, StringLen(lbl) - 1, 1) == "0") lbl = StringSubstr(lbl, 0, StringLen(lbl) - 1); if(StringLen(lbl) > 0 && StringSubstr(lbl, StringLen(lbl) - 1, 1) == ".") lbl = StringSubstr(lbl, 0, StringLen(lbl) - 1); const int fontPx = 8, labelGap = 7, glyphPadX = 3; TextSetFont("Arial", -(fontPx * 10)); uint twU = 0, thU = 0; TextGetSize(lbl, twU, thU); const int tw = (int)twU, th = (int)thU; FiboDrawLabel(canvas, lbl, xL - (labelGap + glyphPadX) - tw, ly - th / 2, lCol, fontPx); } //--- Dashed connector showing the anchored swing ChannelDrawDashedLine(canvas, x1, y1, x2, y2, ColorToARGB(clrDodgerBlue, 200)); //--- 2 handles at the anchor points if(selected || hovered) { if(m_hideHandleIdx != 0) DrawHandleOnCanvas(canvas, x1, y1, selected, objColor, m_haloHandleIdx == 0); if(m_hideHandleIdx != 1) DrawHandleOnCanvas(canvas, x2, y2, selected, objColor, m_haloHandleIdx == 1); } } //+------------------------------------------------------------------+ //| Hit test Fib Retracement body | //+------------------------------------------------------------------+ bool CFibonacciTools::HitTestFibRetracement(int mx, int my, int canvasW, int x1, int y1, int x2, int y2, int threshold) { double levels[] = {0.0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0, 1.618, 2.618, 3.618, 4.236}; int nLev = ArraySize(levels); int xL = MathMin(x1, x2); int xR = MathMax(x1, x2); for(int i = 0; i < nLev; i++) { int ly = y1 + (int)MathRound((double)(y2 - y1) * levels[i]); if(PointToSegmentDistance(mx, my, xL, ly, xR, ly) <= threshold) return true; } return false; }
Здесь "FibColorForLevel" выполняет простой подбор цвета: сравнивает заданный коэффициент со стандартными значениями Фибоначчи с небольшим допуском эпсилон и возвращает соответствующий цвет (серый для 0,0 и 1,0, малиновый для 0,236, оранжевый для 0,382, золотисто-жёлтый для 0,5, морской зелёный для 0,618, тёмно-бирюзовый для 0,786 и так далее, включая уровни расширения). Для нераспознанных коэффициентов используется цвет DodgerBlue. Именно этот цвет мы использовали в качестве цвета по умолчанию с самого начала; вы можете выбрать любой, который вам нравится. "FiboDrawLabel" — это однострочная оболочка, которая делегирует вызов функции "GannDrawLabel"; единственная причина её существования — обеспечение читаемости кода в местах вызова.
Функция "FiboDrawArc" рисует параметрическую дугу со сглаживанием, проходя по диапазону углов с шагом, соответствующим примерно одному пикселю. На каждом шаге мы вычисляем координаты точки на дуге с плавающей запятой с помощью функций MathCos и MathSin, меняя знак координаты Y, поскольку экранная ось Y направлена вниз. Затем мы распределяем покрытие между четырьмя соседними пикселями с целочисленными координатами, используя билинейные веса: каждый пиксель получает значение покрытия, равное произведению весов близости по осям X и Y к точке с координатами с плавающей запятой, а перед альфа-композитингом исходное значение альфа-канала умножается на это покрытие. Именно благодаря этому края дуги получаются гладкими, а не ступенчатыми, как при наивной отрисовке. Чтобы получить толщину в два пикселя, мы выполняем два прохода со смещением: один — на полпикселя внутрь по радиальному направлению, другой — на полпикселя наружу; это удваивает толщину дуги без швов, вызванных алиасингом.
Функция "DrawFibRetracementOn" сначала вычисляет левую и правую координаты по оси X, чтобы линии пролегали строго между двумя опорными точками, независимо от направления рисования. Для заливки полос мы перебираем видимые уровни и для каждой пары соседних видимых уровней вычисляем две координаты Y линейной интерполяцией между координатами Y опорных точек с использованием коэффициента соответствующего уровня, а затем вызываем "ChannelFillQuad" для построчной заливки полосы полупрозрачным цветом с низким значением альфа-канала. Сами линии уровней рисуются с помощью функций "DrawThickLine" или "WidgetDashedLineAA" в зависимости от стиля. Слева от каждого видимого уровня с помощью FiboDrawLabel выводится значение коэффициента без конечных нулей. Соединительная штриховая линия, нарисованная с помощью "ChannelDrawDashedLine", показывает движение цены между двумя опорными точками, а при выделении объекта или наведении на него курсора в опорных точках рисуются два маркера.
Функция "HitTestFibRetracement" воссоздаёт те же одиннадцать канонических коэффициентов в виде жёстко заданного массива, вычисляет координату Y каждой линии по коэффициенту уровня и проверяет расстояние от курсора до каждого горизонтального отрезка с помощью унаследованной вспомогательной функции "PointToSegmentDistance". В результате получается следующее.

На визуализации видно, что уровни Фибоначчи корректно отображаются с цветовой заливкой. Для удобства сравнения мы вновь показали встроенный в терминал инструмент "Уровни Фибоначчи" с параметрами по умолчанию. Остальные инструменты используют тот же подход. Для завершения реализации осталось интегрировать эти инструменты в движок рисования, чтобы они унаследовали все возможности управления: проверку попадания на маркеры, перерисовку, обновление памяти настроек инструмента и регистрацию. Это будет рассмотрено в следующем разделе.
Интеграция новых инструментов в движок рисования
Передача проверок попадания функциям новых инструментов
Мы определяем функцию "HitTestAllObjects", которая перебирает все нарисованные объекты и передаёт координаты курсора соответствующей функции проверки попадания. Эту функцию движок вызывает при каждом движении мыши, чтобы определить, над каким объектом (если такой есть) находится курсор. Через ветви оператора switch новые инструменты каналов, вил, Ганна и Фибоначчи подключаются к существующему конвейеру проверки попадания.
//+------------------------------------------------------------------+ //| Hit test all drawn objects; return the topmost hit object ID | //+------------------------------------------------------------------+ int CDrawingEngine::HitTestAllObjects(int mouseX, int mouseY) { int n = ArraySize(m_drawnObjects); //--- Iterate in reverse draw order so the topmost object wins for(int i = n - 1; i >= 0; i--) { if(!m_drawnObjects[i].visible) continue; //--- Map all three anchors to screen coordinates int x1=0, y1=0, x2=0, y2=0, x3=0, y3=0; ChartTimePriceToXY(m_chartId, 0, m_drawnObjects[i].time1, m_drawnObjects[i].price1, x1, y1); if(m_drawnObjects[i].time2 != 0) ChartTimePriceToXY(m_chartId, 0, m_drawnObjects[i].time2, m_drawnObjects[i].price2, x2, y2); if(m_drawnObjects[i].time3 != 0) ChartTimePriceToXY(m_chartId, 0, m_drawnObjects[i].time3, m_drawnObjects[i].price3, x3, y3); bool hit = false; switch(m_drawnObjects[i].toolType) { case TOOL_TRENDLINE: case TOOL_RAY: case TOOL_EXTENDED_LINE: case TOOL_TREND_ANGLE: hit = HitTestTrendLine(mouseX, mouseY, x1, y1, x2, y2, m_hitThreshold); break; case TOOL_INFO_LINE: //--- Hit on either the line body or its floating info panel hit = HitTestTrendLine(mouseX, mouseY, x1, y1, x2, y2, m_hitThreshold) || HitTestInfoLinePanel(mouseX, mouseY); break; case TOOL_HLINE: hit = HitTestHorizontalLine(mouseX, mouseY, y1, m_hitThreshold); break; case TOOL_VLINE: hit = HitTestVerticalLine(mouseX, mouseY, x1, m_hitThreshold); break; case TOOL_CROSS_LINE: //--- Hit on either the H-line or V-line component hit = HitTestHorizontalLine(mouseX, mouseY, y1, m_hitThreshold) || HitTestVerticalLine(mouseX, mouseY, x1, m_hitThreshold); break; case TOOL_RECTANGLE: hit = HitTestRectangle(mouseX, mouseY, x1, y1, x2, y2); break; case TOOL_FIBO_RETRACEMENT: hit = HitTestFibRetracement(mouseX, mouseY, m_canvasDrawings.Width(), x1, y1, x2, y2, m_hitThreshold); break; case TOOL_FIBO_EXPANSION: hit = HitTestFibExpansion(mouseX, mouseY, m_canvasDrawings.Width(), x1, y1, x2, y2, x3, y3, m_hitThreshold); break; case TOOL_FIBO_CHANNEL: hit = HitTestFibChannel(mouseX, mouseY, m_canvasDrawings.Width(), x1, y1, x2, y2, x3, y3, m_hitThreshold); break; case TOOL_FIBO_TIMEZONES: hit = HitTestFibTimeZone(mouseX, mouseY, m_canvasDrawings.Height(), x1, y1, x2, y2, m_hitThreshold); break; case TOOL_FIBO_FAN: hit = HitTestFibFan(mouseX, mouseY, m_canvasDrawings.Width(), m_canvasDrawings.Height(), x1, y1, x2, y2, m_hitThreshold); break; case TOOL_FIBO_ARCS: hit = HitTestFibArcs(mouseX, mouseY, x1, y1, x2, y2, m_hitThreshold); break; case TOOL_PARALLEL_CHANNEL: hit = HitTestParallelChannel(mouseX, mouseY, x1, y1, x2, y2, x3, y3, m_hitThreshold); break; case TOOL_REGRESSION_CHANNEL: hit = HitTestRegressionChannel(mouseX, mouseY, m_chartId, m_drawnObjects[i].time1, m_drawnObjects[i].time2, m_hitThreshold); break; case TOOL_STDDEV_CHANNEL: hit = HitTestStdDevChannel(mouseX, mouseY, m_chartId, m_drawnObjects[i].time1, m_drawnObjects[i].time2, m_hitThreshold); break; case TOOL_PITCHFORK: hit = HitTestAndrewsPitchfork(mouseX, mouseY, m_canvasDrawings.Width(), m_canvasDrawings.Height(), x1, y1, x2, y2, x3, y3, m_hitThreshold); break; case TOOL_SCHIFF_PITCHFORK: hit = HitTestSchiffPitchfork(mouseX, mouseY, m_canvasDrawings.Width(), m_canvasDrawings.Height(), x1, y1, x2, y2, x3, y3, m_hitThreshold); break; case TOOL_MOD_SCHIFF: hit = HitTestModSchiffPitchfork(mouseX, mouseY, m_canvasDrawings.Width(), m_canvasDrawings.Height(), x1, y1, x2, y2, x3, y3, m_hitThreshold); break; case TOOL_GANN_LINE: hit = HitTestGannLine(mouseX, mouseY, m_canvasDrawings.Width(), m_canvasDrawings.Height(), x1, y1, x2, y2, m_hitThreshold); break; case TOOL_GANN_FAN: hit = HitTestGannFan(mouseX, mouseY, m_canvasDrawings.Width(), m_canvasDrawings.Height(), x1, y1, x2, y2, m_hitThreshold); break; case TOOL_GANN_BOX: hit = HitTestGannBox(mouseX, mouseY, x1, y1, x2, y2, m_hitThreshold); break; default: break; } if(hit) return m_drawnObjects[i].id; } return -1; }
Мы перебираем нарисованные объекты в порядке, обратном порядку отрисовки, чтобы при перекрытии приоритет получал самый верхний объект, пропуская все объекты, у которых флаг видимости равен false. Для каждого объекта-кандидата мы преобразуем координаты всех трех возможных опорных точек в экранные координаты с помощью функции ChartTimePriceToXY, причем координаты второй и третьей точек вычисляются только тогда, когда сохраненные для них значения времени не равны нулю.
Затем оператор switch направляет каждый инструмент к соответствующей функции проверки попадания. Базовые инструменты рисования линий обрабатываются в существующих ветвях без изменений. Для каждого нового инструмента предусмотрена отдельная ветвь case с вызовом соответствующей функции: "HitTestFibRetracement", "HitTestFibExpansion", "HitTestFibChannel", "HitTestFibTimeZone", "HitTestFibFan" и "HitTestFibArcs" для семейства Фибоначчи; "HitTestParallelChannel", "HitTestRegressionChannel" и "HitTestStdDevChannel" для каналов; "HitTestAndrewsPitchfork", "HitTestSchiffPitchfork" и "HitTestModSchiffPitchfork" для вил; "HitTestGannLine", "HitTestGannFan" и "HitTestGannBox" для инструментов Ганна. Как только для первого объекта обнаружено попадание, функция немедленно возвращает его идентификатор. Если курсор не попадает ни на один объект, функция возвращает -1. На этом реализация завершена. Далее мы проверим ее производительность.
Визуализация
Мы компилируем программу, прикрепляем ее к графику и поочередно размещаем каждый новый инструмент, чтобы убедиться, что движок рисования обрабатывает их все. На GIF-анимации ниже показан типичный сеанс работы: размещение параллельного канала, регрессионного канала с полосами стандартного отклонения и подписью с коэффициентом корреляции Пирсона R, вил Эндрюса, веера Ганна, уровней Фибоначчи и дуг скоростного сопротивления Фибоначчи, затем перетаскивание маркеров для изменения формы объектов и удаление всех объектов с помощью плитки действия "Удалить".

В ходе тестирования динамический предварительный просмотр плавно следовал за курсором между щелчками для каждого инструмента, требующего трех щелчков; регрессионный канал заново рассчитывал центральную линию при каждой перерисовке по мере появления новых баров; а плитка "Удалить" очищала график одним щелчком через всплывающую панель с текущим количеством нарисованных объектов.
Заключение
Подводя итог, мы расширили слой рисования на холсте, добавив пятнадцать аналитических инструментов из категорий каналов, вил, инструментов Ганна и Фибоначчи, а также плитку действия "Удалить всё". Все инструменты используют единый конвейер проверки попадания, выделения, изменения формы и динамического предварительного просмотра. Система сохранения настроек инструмента запоминает настройки оформления при последовательных размещениях одного и того же инструмента. Новые инструменты опираются на небольшой набор общих вспомогательных функций: альфа-композитинг, построчную заливку многоугольников, отсечение лучей по границам холста, вычисление конечных точек регрессионной прямой с помощью метода наименьших квадратов (OLS) и построение параметрических дуг со сглаживанием. Эти функции встроены в цепочку наследования, поэтому каждый инструмент повторно использует одни и те же примитивы. Прочитав эту статью, вы сможете:
- Реализовать на собственном слое холста аналитические инструменты рисования с несколькими опорными точками и вычисляемыми маркерами, сохраняющими геометрические инварианты при перетаскивании.
- Использовать метод наименьших квадратов (OLS) для расчёта текущих конечных точек канала по рыночным данным и пересчитывать регрессию при каждой перерисовке, чтобы канал точно следовал за движением цены.
- Отрисовывать на холсте полупрозрачные заливки полос, дуги со сглаживанием и текстовые метки с извлечением альфа-канала за два прохода, не используя встроенные графические объекты MetaTrader 5.
В следующей части серии мы добавим в палитру категории "Фигуры" и "Аннотации", а также реализуем редактирование меток, чтобы пользователь мог вводить текст непосредственно в объекты-метки, созданные на графике. Следите за новостями.
Вложения
| № п/п | Название | Тип | Описание |
|---|---|---|---|
| 1 | Tools Palette Part 6.zip | Архив | Готовый к распаковке архив, содержащий все 11 файлов проекта в одной папке. Распакуйте архив в папку данных терминала MetaTrader 5. Все файлы будут размещены в нужных местах внутри MQL5/Experts/ и готовы к компиляции. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22838
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.