English
preview
Торговые инструменты на MQL5 (Часть 18): Скруглённые текстовые выноски с настройкой ориентации

Торговые инструменты на MQL5 (Часть 18): Скруглённые текстовые выноски с настройкой ориентации

MetaTrader 5Примеры |
42 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В своей предыдущей статье (Часть 17) мы рассматривали векторные методы для рисования скругленных прямоугольников и треугольников в MetaQuotes Language 5 (MQL5) с использованием объектов Canvas и суперсэмплирования для сглаживания изображения. В Части 18 мы комбинируем эти фигуры для создания скругленных текстовых выносок с возможностью управления ориентацией, направляя указатели вверх, вниз, влево или вправо. Эта интеграция включает в себя настраиваемые положения, рамки и прозрачность, предоставляя универсальные элементы пользовательского интерфейса для торговых интерфейсов. В статье рассмотрим следующие темы:

  1. Рассмотрение скругленных текстовых выносок с ориентацией
  2. Реализация средствами MQL5
  3. Тестирование на истории
  4. Заключение

В итоге у вас будет функциональная система текстовых выносок, готовая к настройке в продвинутых приложениях. Перейдём к реализации!


Рассмотрение скругленных текстовых выносок с ориентацией

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

DIFFERENT SPEECH BUBBLES SETUP OBJECTIVES


Реализация средствами MQL5

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

//+------------------------------------------------------------------+
//|                           Rounded Rectangle & Triangle PART2.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property strict

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum BUBBLE_ORIENTATION {
   ORIENT_UP    = 0, // Pointer faces up
   ORIENT_DOWN  = 1, // Pointer faces down
   ORIENT_LEFT  = 2, // Pointer faces left
   ORIENT_RIGHT = 3  // Pointer faces right
};

input group "Bubble Shape"
input int              bubbleBodyWidthPixels            = 250;          // Bubble body width
input int              bubbleBodyHeightPixels           = 100;          // Bubble body height
input int              bubbleBodyCornerRadiusPixels     = 5;            // Bubble body corner radius
input int              bubblePointerBaseWidthPixels     = 60;           // Bubble pointer base width
input int              bubblePointerHeightPixels        = 40;           // Bubble pointer height
input int              bubblePointerApexRadiusPixels    = 12;           // Bubble pointer apex radius
input int              bubblePointerBaseOffsetPixels    = 0;            // Bubble pointer offset from center (0=centered, +/-=shift)
input BUBBLE_ORIENTATION bubblePointerOrientation       = ORIENT_UP;    // Bubble pointer orientation
input bool             bubbleShowBorder                 = true;         // Show bubble border
input int              bubbleBorderThicknessPixels      = 2;            // Bubble border thickness
input color            bubbleBorderColor                = clrGreen;     // Bubble border color
input int              bubbleBorderOpacityPercent       = 80;           // Bubble border opacity (0-100%)
input color            bubbleBackgroundColor            = clrGreen;     // Bubble background color
input int              bubbleBackgroundOpacityPercent   = 30;           // Bubble background opacity (0-100%)

input group "General"
input double borderExtensionMultiplier        = 0.23;    // Border extension multiplier (fraction of thickness)

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CCanvas bubbleCanvas,    bubbleHighResCanvas;                   //--- Declare bubble canvas objects

string  bubbleCanvasName    = "BubbleCanvas";                   //--- Set bubble canvas name

double bubbleBodyLeft, bubbleBodyTop, bubbleBodyRight, bubbleBodyBottom; //--- Store bubble body coordinates
double bubblePointerVerticesX[3], bubblePointerVerticesY[3];    //--- Store bubble pointer vertices X and Y
double bubblePointerArcCentersX[3], bubblePointerArcCentersY[3]; //--- Store bubble pointer arc centers X and Y
double bubblePointerTangentPointsX[3][2], bubblePointerTangentPointsY[3][2]; //--- Store bubble pointer tangent points X and Y
double bubblePointerArcStartAngles[3], bubblePointerArcEndAngles[3]; //--- Store bubble pointer arc start and end angles
int    bubblePointerApexIndex;                                  //--- Store bubble pointer apex index
double bubblePointerBaseStart, bubblePointerBaseEnd;            //--- Store bubble pointer base start and end
bool   bubbleIsHorizontalOrientation;                           //--- Store if bubble orientation is horizontal

Далее мы определяем перечисление "BUBBLE_ORIENTATION" с параметрами для направлений указателя: "ORIENT_UP" (0), "ORIENT_DOWN" (1), "ORIENT_LEFT" (2) и "ORIENT_RIGHT" (3). Это обеспечивает гибкий контроль над расположением пузырей. В группе входных параметров "Bubble Shape" (Форма пузыря) добавляем параметры для размеров тела, такие как "bubbleBodyWidthPixels" и "bubbleBodyHeightPixels". К другим параметрам относятся радиус скругления углов "bubbleBodyCornerRadiusPixels", спецификации указателей, такие как "bubblePointerBaseWidthPixels", "bubblePointerHeightPixels", "bubblePointerApexRadiusPixels" и смещение "bubblePointerBaseOffsetPixels" для центрирования. Мы также включаем ориентацию из перечисления, переключатели рамки, толщину, цвета и прозрачность, аналогично предыдущим фигурам.

Для точной настройки рамки мы включаем общий входной параметр "borderExtensionMultiplier", представляющий собой долю толщины для расширения краев, обеспечивающий бесшовные соединения. Глобальные переменные дополняются объектами Canvas для пузыря, такими как "bubbleCanvas" и "bubbleHighResCanvas" с именем "BubbleCanvas". Мы храним границы тела в параметрах типа double, таких как "bubbleBodyLeft" и "bubbleBodyTop", вершинах указателей, центрах дуг, касательных (массивы 3x2), углах, индексе вершины "bubblePointerApexIndex", начале/конце основания и логическом значении "bubbleIsHorizontalOrientation" для динамической обработки ориентации сканирования строк. Теперь надо определить функции для управления созданием текстовой выноски, используя логику, аналогичную той, которую мы использовали с другими фигурами в предыдущей версии, следующим образом. Сначала мы предварительно вычислим геометрию пузыря.

//+------------------------------------------------------------------+
//| Bubble Shape                                                     |
//+------------------------------------------------------------------+
void PrecomputeBubbleGeometry() {
   int scalingFactor = supersamplingFactor;                     //--- Set scaling factor
   double baseOffset = 10.0 * scalingFactor;                    //--- Set base offset scaled
   
   double centeringAdjustment;                                  //--- Declare centering adjustment

   if(bubblePointerOrientation == ORIENT_UP) {                  //--- Check for up orientation
      bubbleBodyLeft = baseOffset;                              //--- Set body left
      bubbleBodyTop = baseOffset + bubblePointerHeightPixels * scalingFactor; //--- Set body top
      bubbleBodyRight = bubbleBodyLeft + bubbleBodyWidthPixels * scalingFactor; //--- Set body right
      bubbleBodyBottom = bubbleBodyTop + bubbleBodyHeightPixels * scalingFactor; //--- Set body bottom

      centeringAdjustment = (bubbleBodyWidthPixels * scalingFactor - bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute centering
      double actualOffset = centeringAdjustment + (bubblePointerBaseOffsetPixels * scalingFactor); //--- Apply offset
      
      double pointerCenterX = bubbleBodyLeft + actualOffset + (bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute pointer center X
      
      bubblePointerVerticesX[0] = pointerCenterX;               //--- Set apex X
      bubblePointerVerticesY[0] = baseOffset;                   //--- Set apex Y
      
      bubblePointerVerticesX[1] = bubbleBodyLeft + actualOffset; //--- Set left X
      bubblePointerVerticesY[1] = bubbleBodyTop;                //--- Set left Y
      
      bubblePointerVerticesX[2] = bubblePointerVerticesX[1] + bubblePointerBaseWidthPixels * scalingFactor; //--- Set right X
      bubblePointerVerticesY[2] = bubbleBodyTop;                //--- Set right Y
      
      bubblePointerApexIndex = 0;                               //--- Set apex index
      bubblePointerBaseStart = bubblePointerVerticesX[1];       //--- Set base start
      bubblePointerBaseEnd = bubblePointerVerticesX[2];         //--- Set base end
      
   } else if(bubblePointerOrientation == ORIENT_DOWN) {         //--- Check for down orientation
      bubbleBodyLeft = baseOffset;                              //--- Set body left
      bubbleBodyTop = baseOffset;                               //--- Set body top
      bubbleBodyRight = bubbleBodyLeft + bubbleBodyWidthPixels * scalingFactor; //--- Set body right
      bubbleBodyBottom = bubbleBodyTop + bubbleBodyHeightPixels * scalingFactor; //--- Set body bottom

      centeringAdjustment = (bubbleBodyWidthPixels * scalingFactor - bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute centering
      double actualOffset = centeringAdjustment + (bubblePointerBaseOffsetPixels * scalingFactor); //--- Apply offset
      
      double pointerCenterX = bubbleBodyLeft + actualOffset + (bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute pointer center X
      
      bubblePointerVerticesX[0] = pointerCenterX;               //--- Set apex X
      bubblePointerVerticesY[0] = bubbleBodyBottom + bubblePointerHeightPixels * scalingFactor; //--- Set apex Y
      
      bubblePointerVerticesX[1] = bubbleBodyLeft + actualOffset + bubblePointerBaseWidthPixels * scalingFactor; //--- Set right X
      bubblePointerVerticesY[1] = bubbleBodyBottom;             //--- Set right Y
      
      bubblePointerVerticesX[2] = bubbleBodyLeft + actualOffset; //--- Set left X
      bubblePointerVerticesY[2] = bubbleBodyBottom;             //--- Set left Y
      
      bubblePointerApexIndex = 0;                               //--- Set apex index
      bubblePointerBaseStart = bubblePointerVerticesX[2];       //--- Set base start
      bubblePointerBaseEnd = bubblePointerVerticesX[1];         //--- Set base end
      
   } else if(bubblePointerOrientation == ORIENT_LEFT) {         //--- Check for left orientation
      bubbleBodyLeft = baseOffset + bubblePointerHeightPixels * scalingFactor; //--- Set body left
      bubbleBodyTop = baseOffset;                               //--- Set body top
      bubbleBodyRight = bubbleBodyLeft + bubbleBodyWidthPixels * scalingFactor; //--- Set body right
      bubbleBodyBottom = bubbleBodyTop + bubbleBodyHeightPixels * scalingFactor; //--- Set body bottom

      centeringAdjustment = (bubbleBodyHeightPixels * scalingFactor - bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute centering
      double actualOffset = centeringAdjustment + (bubblePointerBaseOffsetPixels * scalingFactor); //--- Apply offset
      
      double pointerCenterY = bubbleBodyTop + actualOffset + (bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute pointer center Y
      
      bubblePointerVerticesX[0] = baseOffset;                   //--- Set apex X
      bubblePointerVerticesY[0] = pointerCenterY;               //--- Set apex Y
      
      bubblePointerVerticesX[1] = bubbleBodyLeft;               //--- Set bottom X
      bubblePointerVerticesY[1] = bubbleBodyTop + actualOffset + bubblePointerBaseWidthPixels * scalingFactor; //--- Set bottom Y
      
      bubblePointerVerticesX[2] = bubbleBodyLeft;               //--- Set top X
      bubblePointerVerticesY[2] = bubbleBodyTop + actualOffset; //--- Set top Y
      
      bubblePointerApexIndex = 0;                               //--- Set apex index
      bubblePointerBaseStart = bubblePointerVerticesY[2];       //--- Set base start
      bubblePointerBaseEnd = bubblePointerVerticesY[1];         //--- Set base end
      
   } else {                                                     //--- Handle right orientation
      bubbleBodyLeft = baseOffset;                              //--- Set body left
      bubbleBodyTop = baseOffset;                               //--- Set body top
      bubbleBodyRight = bubbleBodyLeft + bubbleBodyWidthPixels * scalingFactor; //--- Set body right
      bubbleBodyBottom = bubbleBodyTop + bubbleBodyHeightPixels * scalingFactor; //--- Set body bottom

      centeringAdjustment = (bubbleBodyHeightPixels * scalingFactor - bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute centering
      double actualOffset = centeringAdjustment + (bubblePointerBaseOffsetPixels * scalingFactor); //--- Apply offset
      
      double pointerCenterY = bubbleBodyTop + actualOffset + (bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute pointer center Y
      
      bubblePointerVerticesX[0] = bubbleBodyRight + bubblePointerHeightPixels * scalingFactor; //--- Set apex X
      bubblePointerVerticesY[0] = pointerCenterY;               //--- Set apex Y
      
      bubblePointerVerticesX[1] = bubbleBodyRight;              //--- Set top X
      bubblePointerVerticesY[1] = bubbleBodyTop + actualOffset; //--- Set top Y
      
      bubblePointerVerticesX[2] = bubbleBodyRight;              //--- Set bottom X
      bubblePointerVerticesY[2] = bubbleBodyTop + actualOffset + bubblePointerBaseWidthPixels * scalingFactor; //--- Set bottom Y
      
      bubblePointerApexIndex = 0;                               //--- Set apex index
      bubblePointerBaseStart = bubblePointerVerticesY[1];       //--- Set base start
      bubblePointerBaseEnd = bubblePointerVerticesY[2];         //--- Set base end
   }

   ComputeBubbleTriangleRoundedCorners();                       //--- Compute rounded corners for bubble pointer
}

void ComputeBubbleTriangleRoundedCorners() {
   double scaledRadius = (double)bubblePointerApexRadiusPixels * supersamplingFactor; //--- Scale apex radius

   int cornerIndex = bubblePointerApexIndex;                    //--- Set corner index to apex
   
   int previousIndex = (cornerIndex + 2) % 3;                   //--- Get previous index
   int nextIndex = (cornerIndex + 1) % 3;                       //--- Get next index

   double edgeA_X = bubblePointerVerticesX[cornerIndex] - bubblePointerVerticesX[previousIndex]; //--- Compute edge A X
   double edgeA_Y = bubblePointerVerticesY[cornerIndex] - bubblePointerVerticesY[previousIndex]; //--- Compute edge A Y
   double edgeA_Length = MathSqrt(edgeA_X*edgeA_X + edgeA_Y*edgeA_Y); //--- Compute edge A length
   edgeA_X /= edgeA_Length;  edgeA_Y /= edgeA_Length;           //--- Normalize edge A

   double edgeB_X = bubblePointerVerticesX[nextIndex] - bubblePointerVerticesX[cornerIndex]; //--- Compute edge B X
   double edgeB_Y = bubblePointerVerticesY[nextIndex] - bubblePointerVerticesY[cornerIndex]; //--- Compute edge B Y
   double edgeB_Length = MathSqrt(edgeB_X*edgeB_X + edgeB_Y*edgeB_Y); //--- Compute edge B length
   edgeB_X /= edgeB_Length;  edgeB_Y /= edgeB_Length;           //--- Normalize edge B

   double normalA_X =  edgeA_Y,  normalA_Y = -edgeA_X;          //--- Compute normal A
   double normalB_X =  edgeB_Y,  normalB_Y = -edgeB_X;          //--- Compute normal B

   double bisectorX = normalA_X + normalB_X,  bisectorY = normalA_Y + normalB_Y; //--- Compute bisector
   double bisectorLength = MathSqrt(bisectorX*bisectorX + bisectorY*bisectorY); //--- Compute bisector length
   if(bisectorLength < 1e-12) { bisectorX = normalA_X; bisectorY = normalA_Y; bisectorLength = MathSqrt(bisectorX*bisectorX + bisectorY*bisectorY); } //--- Handle small bisector
   bisectorX /= bisectorLength;  bisectorY /= bisectorLength;   //--- Normalize bisector

   double cosInteriorAngle = (-edgeA_X)*edgeB_X + (-edgeA_Y)*edgeB_Y; //--- Compute cosine of interior angle
   if(cosInteriorAngle >  1.0) cosInteriorAngle =  1.0;         //--- Clamp cosine upper
   if(cosInteriorAngle < -1.0) cosInteriorAngle = -1.0;         //--- Clamp cosine lower
   double halfAngle = MathArccos(cosInteriorAngle) / 2.0;       //--- Compute half angle
   double sinHalfAngle = MathSin(halfAngle);                    //--- Compute sine of half angle
   if(sinHalfAngle < 1e-12) sinHalfAngle = 1e-12;               //--- Set minimum sine value

   double distanceToCenter = scaledRadius / sinHalfAngle;       //--- Compute distance to arc center
   bubblePointerArcCentersX[cornerIndex] = bubblePointerVerticesX[cornerIndex] + bisectorX * distanceToCenter; //--- Set arc center X
   bubblePointerArcCentersY[cornerIndex] = bubblePointerVerticesY[cornerIndex] + bisectorY * distanceToCenter; //--- Set arc center Y

   double deltaX_A = bubblePointerVerticesX[cornerIndex] - bubblePointerVerticesX[previousIndex]; //--- Compute delta A X
   double deltaY_A = bubblePointerVerticesY[cornerIndex] - bubblePointerVerticesY[previousIndex]; //--- Compute delta A Y
   double lengthSquared_A = deltaX_A*deltaX_A + deltaY_A*deltaY_A; //--- Compute length squared A
   double interpolationFactor_A = ((bubblePointerArcCentersX[cornerIndex] - bubblePointerVerticesX[previousIndex])*deltaX_A + 
                                   (bubblePointerArcCentersY[cornerIndex] - bubblePointerVerticesY[previousIndex])*deltaY_A) / lengthSquared_A; //--- Compute factor A
   bubblePointerTangentPointsX[cornerIndex][1] = bubblePointerVerticesX[previousIndex] + interpolationFactor_A * deltaX_A; //--- Set tangent point X arriving
   bubblePointerTangentPointsY[cornerIndex][1] = bubblePointerVerticesY[previousIndex] + interpolationFactor_A * deltaY_A; //--- Set tangent point Y arriving

   double deltaX_B = bubblePointerVerticesX[nextIndex] - bubblePointerVerticesX[cornerIndex]; //--- Compute delta B X
   double deltaY_B = bubblePointerVerticesY[nextIndex] - bubblePointerVerticesY[cornerIndex]; //--- Compute delta B Y
   double lengthSquared_B = deltaX_B*deltaX_B + deltaY_B*deltaY_B; //--- Compute length squared B
   double interpolationFactor_B = ((bubblePointerArcCentersX[cornerIndex] - bubblePointerVerticesX[cornerIndex])*deltaX_B + 
                                   (bubblePointerArcCentersY[cornerIndex] - bubblePointerVerticesY[cornerIndex])*deltaY_B) / lengthSquared_B; //--- Compute factor B
   bubblePointerTangentPointsX[cornerIndex][0] = bubblePointerVerticesX[cornerIndex] + interpolationFactor_B * deltaX_B; //--- Set tangent point X leaving
   bubblePointerTangentPointsY[cornerIndex][0] = bubblePointerVerticesY[cornerIndex] + interpolationFactor_B * deltaY_B; //--- Set tangent point Y leaving

   bubblePointerArcStartAngles[cornerIndex] = MathArctan2(bubblePointerTangentPointsY[cornerIndex][1] - bubblePointerArcCentersY[cornerIndex], 
                                                          bubblePointerTangentPointsX[cornerIndex][1] - bubblePointerArcCentersX[cornerIndex]); //--- Set start angle
   bubblePointerArcEndAngles[cornerIndex] = MathArctan2(bubblePointerTangentPointsY[cornerIndex][0] - bubblePointerArcCentersY[cornerIndex], 
                                                        bubblePointerTangentPointsX[cornerIndex][0] - bubblePointerArcCentersX[cornerIndex]); //--- Set end angle
   
   for(int i = 0; i < 3; i++) {                                 //--- Loop over corners
      if(i == bubblePointerApexIndex) continue;                 //--- Skip apex corner
      
      bubblePointerTangentPointsX[i][0] = bubblePointerVerticesX[i]; //--- Set tangent X leaving to vertex
      bubblePointerTangentPointsY[i][0] = bubblePointerVerticesY[i]; //--- Set tangent Y leaving to vertex
      bubblePointerTangentPointsX[i][1] = bubblePointerVerticesX[i]; //--- Set tangent X arriving to vertex
      bubblePointerTangentPointsY[i][1] = bubblePointerVerticesY[i]; //--- Set tangent Y arriving to vertex
   }
}

Мы начинаем с реализации функции "PrecomputeBubbleGeometry", чтобы определить расположение текстовой выноски. Присваиваем коэффициент суперсэмплирования локальной переменной и масштабируем смещение основы для отступа, затем объявляем настройку центрирования для позиционирования указателя. В зависимости от параметра "bubblePointerOrientation" устанавливаем координаты тела: для "ORIENT_UP" размещаем его под указателем, при этом левая, верхняя, правая и нижняя точки вычисляются на основе масштабированных входных параметров; вычисляем центрирование с применением смещения для выравнивания, определяем координаты центра указателя по оси X и определяем верхние точки с вершиной в верхней центральной точке, левой и правой — в верхней части тела, устанавливая "bubblePointerApexIndex" равным 0 и начало/конец основы от левой до правой по оси X.

Для "ORIENT_DOWN" мы располагаем тело над указателем, инвертируем вершину относительно нижней части, корректируем порядок вершин, чтобы сохранить согласованное направление обхода и соответствующим образом обновляем начало/конец основания; аналогично, для "ORIENT_LEFT" сдвигаем тело вправо от указателя, устанавливаем вершины с вершиной слева, нижней и верхней осями слева от тела, а также основание в качестве значений Y; для "ORIENT_RIGHT" зеркально отображаем вправо с соответствующими корректировками. Такая условная настройка обеспечивает адаптивную геометрию в зависимости от направления, что способствует бесшовной интеграции указателя и тела без наложений.

Для скругления кончика указателя мы вызываем функцию "ComputeBubbleTriangleRoundedCorners", масштабируем радиус вершины, фокусируемся на вершине с помощью функции "bubblePointerApexIndex", вычисляем векторы ребер и нормали, как при предварительном вычислении треугольника, формируем биссектрису, вычисляем синус половинного угла для получения расстояния между центрами, проецируем для нахождения касательных и устанавливаем начальный/конечный углы с помощью MathArctan2 функции. Наконец, для углов, отличных от вершин (основания), мы устанавливаем касательные непосредственно к вершинам, отключая скругление, чтобы они плавно переходили в тело, что имеет решающее значение для четкого отображения речевого пузыря без лишних изгибов в точке прикрепления. Для рисования пикселей на основе дуги нам понадобится вспомогательная функция.

bool BubbleAngleInArcSweep(int cornerIndex, double angle) {
   double twoPi = 2.0 * M_PI;                                   //--- Define two pi constant
   double startAngleMod = MathMod(bubblePointerArcStartAngles[cornerIndex] + twoPi, twoPi); //--- Modulo start angle
   double endAngleMod = MathMod(bubblePointerArcEndAngles[cornerIndex] + twoPi, twoPi); //--- Modulo end angle
   angle = MathMod(angle + twoPi, twoPi);                       //--- Modulo angle

   double ccwSpan = MathMod(endAngleMod - startAngleMod + twoPi, twoPi); //--- Compute CCW span

   if(ccwSpan <= M_PI) {                                        //--- Check if short way is CCW
      double relativeAngle = MathMod(angle - startAngleMod + twoPi, twoPi); //--- Compute relative angle
      return(relativeAngle <= ccwSpan + 1e-6);                  //--- Return if within CCW span
   } else {                                                     //--- Else short way is CW
      double cwSpan = twoPi - ccwSpan;                          //--- Compute CW span
      double relativeAngle = MathMod(angle - endAngleMod + twoPi, twoPi); //--- Compute relative angle
      return(relativeAngle <= cwSpan + 1e-6);                   //--- Return if within CW span
   }
}

Мы реализуем функцию "BubbleAngleInArcSweep", чтобы определить, попадает ли заданный угол в область дуги для конкретного угла указателя-пузыря. Для этого мы определяем константу, равную двум пи, и нормализуем начальный, конечный и входной углы с помощью MathMod, чтобы они находились в диапазоне от 0 до 2 пи, обрабатывая любые переносы или отрицательные значения. Далее вычисляем размах дуги против часовой стрелки между нормализованными началом и концом, и если он равен pi или меньше (что указывает на более короткую дугу), проверяем относительный угол от начала относительно этого размаха дуги с небольшим значением эпсилон для точности; в противном случае вычисляем размах дуги по часовой стрелке как 2 pi минус размах против часовой стрелки и проверяем относительный угол от конца. Эта логика учитывает оба направления дуги, обеспечивая точное включение пикселей во время рендеринга, что крайне важно для плавных, без артефактов, изогнутых рамок указателя текстовой выноски при любой ориентации. Для построения прямоугольника со скругленными углами в виде пузыря мы используем следующий подход.

void FillBubbleRoundedRectangle(double left, double top, double width, double height, int radius, uint fillColor) {
   bubbleHighResCanvas.FillRectangle((int)(left + radius), (int)top, (int)(left + width - radius), (int)(top + height), fillColor); //--- Fill central rectangle
   bubbleHighResCanvas.FillRectangle((int)left, (int)(top + radius), (int)(left + radius), (int)(top + height - radius), fillColor); //--- Fill left strip
   bubbleHighResCanvas.FillRectangle((int)(left + width - radius), (int)(top + radius), (int)(left + width), (int)(top + height - radius), fillColor); //--- Fill right strip

   FillBubbleCircleQuadrant((int)(left + radius), (int)(top + radius), radius, fillColor, 2); //--- Fill top-left quadrant
   FillBubbleCircleQuadrant((int)(left + width - radius), (int)(top + radius), radius, fillColor, 1); //--- Fill top-right quadrant
   FillBubbleCircleQuadrant((int)(left + radius), (int)(top + height - radius), radius, fillColor, 3); //--- Fill bottom-left quadrant
   FillBubbleCircleQuadrant((int)(left + width - radius), (int)(top + height - radius), radius, fillColor, 4); //--- Fill bottom-right quadrant
}

void FillBubbleCircleQuadrant(int centerX, int centerY, int radius, uint fillColor, int quadrant) {
   double radiusDouble = (double)radius;                        //--- Convert radius to double

   for(int deltaY = -radius - 1; deltaY <= radius + 1; deltaY++) { //--- Loop over delta Y
      for(int deltaX = -radius - 1; deltaX <= radius + 1; deltaX++) { //--- Loop over delta X
         bool inQuadrant = false;                               //--- Initialize quadrant flag
         if(quadrant == 1 && deltaX >= 0 && deltaY <= 0) inQuadrant = true; //--- Check top-right
         else if(quadrant == 2 && deltaX <= 0 && deltaY <= 0) inQuadrant = true; //--- Check top-left
         else if(quadrant == 3 && deltaX <= 0 && deltaY >= 0) inQuadrant = true; //--- Check bottom-left
         else if(quadrant == 4 && deltaX >= 0 && deltaY >= 0) inQuadrant = true; //--- Check bottom-right

         if(!inQuadrant) continue;                              //--- Skip if not in quadrant

         double distance = MathSqrt(deltaX * deltaX + deltaY * deltaY); //--- Compute distance
         if(distance <= radiusDouble)                           //--- Check if within radius
            bubbleHighResCanvas.PixelSet(centerX + deltaX, centerY + deltaY, fillColor); //--- Set pixel
      }
   }
}

Здесь мы реализуем функцию "FillBubbleRoundedRectangle" для отображения заполненного тела текстовой выноски в виде закругленного прямоугольника на объекте Canvas высокого разрешения, преобразуя координаты в целые числа для пиксельной точности, заполняя центральную область, за исключением углов, функцией "FillRectangle", а затем добавляя вертикальные полосы слева и справа для бесшовного покрытия боковых сторон. Для завершения округления мы вызываем функцию "FillBubbleCircleQuadrant" для каждого угла, адаптируя специфичную для квадранта логику таким образом, чтобы заполнить только соответствующие пиксели четверти круга.

В функции "FillBubbleCircleQuadrant" мы преобразуем радиус в число с плавающей запятой для повышения точности, выполняем цикл по расширенному диапазону дельта-значений для сглаживания, проверяем принадлежность к квадранту на основе знаков дельта-значений (например, квадрант 1 для положительного X, отрицательного Y), вычисляем расстояние с помощью MathSqrt и устанавливаем пиксели внутри радиуса с помощью PixelSet, обеспечивая плавные кривые, которые интегрируются с прямоугольными полосами. Такой модульный способ заполнения важен для обеспечения согласованности между ориентациями, поскольку он повторно использует логику прямоугольников, позволяя при этом прикреплять указатели без пробелов в заполнении. Заполнить прямоугольную рамку очень просто и понятно. Теперь давайте определим логику для заполнения закругленного треугольника для указателя.

void FillBubbleRoundedTriangle(uint fillColor) {
   if(bubbleIsHorizontalOrientation) {                          //--- Check for horizontal orientation
      double minX = bubblePointerVerticesX[0], maxX = bubblePointerVerticesX[0]; //--- Initialize min and max X
      for(int i = 1; i < 3; i++) {                              //--- Loop over vertices
         if(bubblePointerVerticesX[i] < minX) minX = bubblePointerVerticesX[i]; //--- Update min X
         if(bubblePointerVerticesX[i] > maxX) maxX = bubblePointerVerticesX[i]; //--- Update max X
      }

      int xStart = (int)MathCeil(minX);                         //--- Compute start X
      int xEnd   = (int)MathFloor(maxX);                        //--- Compute end X

      for(int x = xStart; x <= xEnd; x++) {                     //--- Loop over scanlines
         double scanlineX = (double)x + 0.5;                    //--- Set scanline X position
         double yIntersections[12];                             //--- Declare intersections array
         int intersectionCount = 0;                             //--- Initialize intersection count

         for(int edgeIndex = 0; edgeIndex < 3; edgeIndex++) {   //--- Loop over edges
            int nextIndex = (edgeIndex + 1) % 3;                //--- Get next index

            if(edgeIndex != bubblePointerApexIndex && nextIndex != bubblePointerApexIndex) continue; //--- Skip non-apex edges

            double startX, startY, endX, endY;                  //--- Declare edge coordinates

            if(edgeIndex == bubblePointerApexIndex) {           //--- Check if from apex
               startX = bubblePointerTangentPointsX[bubblePointerApexIndex][0]; //--- Set start X from tangent
               startY = bubblePointerTangentPointsY[bubblePointerApexIndex][0]; //--- Set start Y from tangent
               endX   = bubblePointerVerticesX[nextIndex];     //--- Set end X to next vertex
               endY   = bubblePointerVerticesY[nextIndex];     //--- Set end Y to next vertex
            } else {                                            //--- Handle to apex
               startX = bubblePointerVerticesX[edgeIndex];     //--- Set start X from vertex
               startY = bubblePointerVerticesY[edgeIndex];     //--- Set start Y from vertex
               endX   = bubblePointerTangentPointsX[bubblePointerApexIndex][1]; //--- Set end X to tangent
               endY   = bubblePointerTangentPointsY[bubblePointerApexIndex][1]; //--- Set end Y to tangent
            }

            double edgeMinX = (startX < endX) ? startX : endX;  //--- Compute edge min X
            double edgeMaxX = (startX > endX) ? startX : endX;  //--- Compute edge max X

            if(scanlineX < edgeMinX || scanlineX > edgeMaxX) continue; //--- Skip if outside edge X
            if(MathAbs(endX - startX) < 1e-12) continue;        //--- Skip if vertical edge

            double interpolationFactor = (scanlineX - startX) / (endX - startX); //--- Compute factor
            if(interpolationFactor < 0.0 || interpolationFactor > 1.0) continue; //--- Skip if outside segment

            yIntersections[intersectionCount++] = startY + interpolationFactor * (endY - startY); //--- Add intersection Y
         }

         {                                                      //--- Intersect apex arc block
            int cornerIndex = bubblePointerApexIndex;           //--- Set corner index
            double centerX  = bubblePointerArcCentersX[cornerIndex]; //--- Get center X
            double centerY  = bubblePointerArcCentersY[cornerIndex]; //--- Get center Y
            double radius   = (double)bubblePointerApexRadiusPixels * supersamplingFactor; //--- Get scaled radius
            double deltaX   = scanlineX - centerX;              //--- Compute delta X

            if(MathAbs(deltaX) <= radius) {                     //--- Check if within radius
               double deltaY = MathSqrt(radius * radius - deltaX * deltaX); //--- Compute delta Y

               double candidates[2];                            //--- Declare candidates array
               candidates[0] = centerY - deltaY;                //--- Set top candidate
               candidates[1] = centerY + deltaY;                //--- Set bottom candidate

               for(int candidateIndex = 0; candidateIndex < 2; candidateIndex++) { //--- Loop over candidates
                  double angle = MathArctan2(candidates[candidateIndex] - centerY, scanlineX - centerX); //--- Compute angle
                  if(BubbleAngleInArcSweep(cornerIndex, angle)) //--- Check if in arc sweep
                     yIntersections[intersectionCount++] = candidates[candidateIndex]; //--- Add intersection
               }
            }
         }

         for(int a = 0; a < intersectionCount - 1; a++)         //--- Sort intersections (bubble sort)
            for(int b = a + 1; b < intersectionCount; b++)       //--- Inner loop for sorting
               if(yIntersections[a] > yIntersections[b]) {       //--- Check if swap needed
                  double temp = yIntersections[a];               //--- Temporary store
                  yIntersections[a] = yIntersections[b];         //--- Swap values
                  yIntersections[b] = temp;                      //--- Complete swap
               }

         for(int pairIndex = 0; pairIndex + 1 < intersectionCount; pairIndex += 2) { //--- Loop over pairs
            int yTop    = (int)MathCeil(yIntersections[pairIndex]); //--- Compute top Y
            int yBottom = (int)MathFloor(yIntersections[pairIndex + 1]); //--- Compute bottom Y
            for(int y = yTop; y <= yBottom; y++)                //--- Loop over vertical span
               bubbleHighResCanvas.PixelSet(x, y, fillColor);   //--- Set pixel with fill color
         }
      }

   } else {                                                     //--- Handle vertical orientation
      double minY = bubblePointerVerticesY[0], maxY = bubblePointerVerticesY[0]; //--- Initialize min and max Y
      for(int i = 1; i < 3; i++) {                              //--- Loop over vertices
         if(bubblePointerVerticesY[i] < minY) minY = bubblePointerVerticesY[i]; //--- Update min Y
         if(bubblePointerVerticesY[i] > maxY) maxY = bubblePointerVerticesY[i]; //--- Update max Y
      }

      int yStart = (int)MathCeil(minY);                         //--- Compute start Y
      int yEnd = (int)MathFloor(maxY);                          //--- Compute end Y

      for(int y = yStart; y <= yEnd; y++) {                     //--- Loop over scanlines
         double scanlineY = (double)y + 0.5;                    //--- Set scanline Y position
         double xIntersections[12];                             //--- Declare intersections array
         int intersectionCount = 0;                             //--- Initialize intersection count

         for(int edgeIndex = 0; edgeIndex < 3; edgeIndex++) {   //--- Loop over edges
            int nextIndex = (edgeIndex + 1) % 3;                //--- Get next index
            
            if(edgeIndex != bubblePointerApexIndex && nextIndex != bubblePointerApexIndex) continue; //--- Skip non-apex edges
            
            double startX, startY, endX, endY;                  //--- Declare edge coordinates
            
            if(edgeIndex == bubblePointerApexIndex) {           //--- Check if from apex
               startX = bubblePointerTangentPointsX[bubblePointerApexIndex][0]; //--- Set start X from tangent
               startY = bubblePointerTangentPointsY[bubblePointerApexIndex][0]; //--- Set start Y from tangent
               endX = bubblePointerVerticesX[nextIndex];       //--- Set end X to next vertex
               endY = bubblePointerVerticesY[nextIndex];       //--- Set end Y to next vertex
            } else {                                            //--- Handle to apex
               startX = bubblePointerVerticesX[edgeIndex];     //--- Set start X from vertex
               startY = bubblePointerVerticesY[edgeIndex];     //--- Set start Y from vertex
               endX = bubblePointerTangentPointsX[bubblePointerApexIndex][1]; //--- Set end X to tangent
               endY = bubblePointerTangentPointsY[bubblePointerApexIndex][1]; //--- Set end Y to tangent
            }

            double edgeMinY = (startY < endY) ? startY : endY;  //--- Compute edge min Y
            double edgeMaxY = (startY > endY) ? startY : endY;  //--- Compute edge max Y

            if(scanlineY < edgeMinY || scanlineY > edgeMaxY) continue; //--- Skip if outside edge Y
            if(MathAbs(endY - startY) < 1e-12) continue;        //--- Skip if horizontal

            double interpolationFactor = (scanlineY - startY) / (endY - startY); //--- Compute factor
            if(interpolationFactor < 0.0 || interpolationFactor > 1.0) continue; //--- Skip if outside segment

            xIntersections[intersectionCount++] = startX + interpolationFactor * (endX - startX); //--- Add intersection X
         }

         int cornerIndex = bubblePointerApexIndex;              //--- Set corner index
         double centerX = bubblePointerArcCentersX[cornerIndex]; //--- Get center X
         double centerY = bubblePointerArcCentersY[cornerIndex]; //--- Get center Y
         double radius = (double)bubblePointerApexRadiusPixels * supersamplingFactor; //--- Get scaled radius
         double deltaY = scanlineY - centerY;                   //--- Compute delta Y

         if(MathAbs(deltaY) <= radius) {                        //--- Check if within radius
            double deltaX = MathSqrt(radius*radius - deltaY*deltaY); //--- Compute delta X

            double candidates[2];                               //--- Declare candidates array
            candidates[0] = centerX - deltaX;                   //--- Set left candidate
            candidates[1] = centerX + deltaX;                   //--- Set right candidate

            for(int candidateIndex = 0; candidateIndex < 2; candidateIndex++) { //--- Loop over candidates
               double angle = MathArctan2(scanlineY - centerY, candidates[candidateIndex] - centerX); //--- Compute angle
               if(BubbleAngleInArcSweep(cornerIndex, angle))    //--- Check if in arc sweep
                  xIntersections[intersectionCount++] = candidates[candidateIndex]; //--- Add intersection
            }
         }

         for(int a = 0; a < intersectionCount - 1; a++)         //--- Sort intersections (bubble sort)
            for(int b = a + 1; b < intersectionCount; b++)       //--- Inner loop for sorting
               if(xIntersections[a] > xIntersections[b]) {       //--- Check if swap needed
                  double temp = xIntersections[a];               //--- Temporary store
                  xIntersections[a] = xIntersections[b];         //--- Swap values
                  xIntersections[b] = temp;                      //--- Complete swap
               }

         for(int pairIndex = 0; pairIndex + 1 < intersectionCount; pairIndex += 2) { //--- Loop over pairs
            int xLeft = (int)MathCeil(xIntersections[pairIndex]); //--- Compute left X
            int xRight = (int)MathFloor(xIntersections[pairIndex + 1]); //--- Compute right X
            for(int x = xLeft; x <= xRight; x++)                //--- Loop over horizontal span
               bubbleHighResCanvas.PixelSet(x, y, fillColor);   //--- Set pixel with fill color
         }
      }
   }
}

Мы реализуем функцию "FillBubbleRoundedTriangle", чтобы заполнить треугольный указатель текстовой выноски на объекте Canvas с высоким разрешением, адаптируя направление сканирования строк в зависимости от ориентации для достижения оптимальной эффективности - используя горизонтальное сканирование (x—loop), если "bubbleIsHorizontalOrientation" имеет значение true для указателей влево/вправо, или, в противном случае, вертикальное (y-loop). Для горизонтальной ориентации мы определяем минимальное и максимальное значение X по вершинам указателей, перебираем целое число x со смещением в полпикселя, собираем пересечения по оси y только с ребрами, смежными с вершинами, с помощью интерполяции (пропуская основание), добавляем потенциальные дуги, решая уравнение окружности в точке deltaX, если она находится в пределах радиуса и подтверждена функцией "BubbleAngleInArcSweep", сортируем с помощью пузырьковой сортировки и заполняем вертикальные размахи дуг между парами с помощью PixelSet с параметром fillColor.

В случаях вертикального направления мы отзеркаливаем процесс с использованием минимальных/максимальных значений Y, Y-петли по строке сканирования Y, пересечений по оси X от ребер и дуг (используя deltaY для потенциальных дуг), сортировки и горизонтальной заливки, обеспечивая полное покрытие без разрывов. После завершения заполнения пузырькового прямоугольника и указателя треугольника, мы определяем логику рамок, в которые будут заключены пузыри. Для этого мы используем следующий подход.

void DrawBubbleBorder(uint borderColorARGB) {
   int scaledThickness = bubbleBorderThicknessPixels * supersamplingFactor; //--- Scale border thickness
   int scaledBodyRadius = bubbleBodyCornerRadiusPixels * supersamplingFactor; //--- Scale body radius

   if(bubblePointerOrientation == ORIENT_UP || bubblePointerOrientation == ORIENT_DOWN) { //--- Check for up or down orientation
      if(bubblePointerOrientation == ORIENT_DOWN) {             //--- Check for down orientation
         DrawBubbleHorizontalEdge(bubbleBodyLeft + scaledBodyRadius, bubbleBodyTop, bubbleBodyRight - scaledBodyRadius, bubbleBodyTop, scaledThickness, borderColorARGB); //--- Draw top edge
      } else {                                                  //--- Handle up orientation
         if(bubblePointerBaseStart > bubbleBodyLeft + scaledBodyRadius) //--- Check if base start exceeds left radius
            DrawBubbleHorizontalEdge(bubbleBodyLeft + scaledBodyRadius, bubbleBodyTop, bubblePointerBaseStart, bubbleBodyTop, scaledThickness, borderColorARGB); //--- Draw left segment of top edge
         if(bubblePointerBaseEnd < bubbleBodyRight - scaledBodyRadius) //--- Check if base end below right radius
            DrawBubbleHorizontalEdge(bubblePointerBaseEnd, bubbleBodyTop, bubbleBodyRight - scaledBodyRadius, bubbleBodyTop, scaledThickness, borderColorARGB); //--- Draw right segment of top edge
      }

      if(bubblePointerOrientation == ORIENT_UP) {               //--- Check for up orientation
         DrawBubbleHorizontalEdge(bubbleBodyRight - scaledBodyRadius, bubbleBodyBottom, bubbleBodyLeft + scaledBodyRadius, bubbleBodyBottom, scaledThickness, borderColorARGB); //--- Draw bottom edge
      } else {                                                  //--- Handle down orientation
         if(bubblePointerBaseStart > bubbleBodyLeft + scaledBodyRadius) //--- Check if base start exceeds left radius
            DrawBubbleHorizontalEdge(bubblePointerBaseStart, bubbleBodyBottom, bubbleBodyLeft + scaledBodyRadius, bubbleBodyBottom, scaledThickness, borderColorARGB); //--- Draw left segment of bottom edge
         if(bubblePointerBaseEnd < bubbleBodyRight - scaledBodyRadius) //--- Check if base end below right radius
            DrawBubbleHorizontalEdge(bubbleBodyRight - scaledBodyRadius, bubbleBodyBottom, bubblePointerBaseEnd, bubbleBodyBottom, scaledThickness, borderColorARGB); //--- Draw right segment of bottom edge
      }

      DrawBubbleVerticalEdge(bubbleBodyLeft, bubbleBodyBottom - scaledBodyRadius, bubbleBodyLeft, bubbleBodyTop + scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw left edge
      DrawBubbleVerticalEdge(bubbleBodyRight, bubbleBodyTop + scaledBodyRadius, bubbleBodyRight, bubbleBodyBottom - scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw right edge

   } else {                                                     //--- Handle left or right orientation
      DrawBubbleHorizontalEdge(bubbleBodyLeft + scaledBodyRadius, bubbleBodyTop, bubbleBodyRight - scaledBodyRadius, bubbleBodyTop, scaledThickness, borderColorARGB); //--- Draw top edge
      DrawBubbleHorizontalEdge(bubbleBodyRight - scaledBodyRadius, bubbleBodyBottom, bubbleBodyLeft + scaledBodyRadius, bubbleBodyBottom, scaledThickness, borderColorARGB); //--- Draw bottom edge

      if(bubblePointerOrientation == ORIENT_RIGHT) {            //--- Check for right orientation
         DrawBubbleVerticalEdge(bubbleBodyLeft, bubbleBodyBottom - scaledBodyRadius, bubbleBodyLeft, bubbleBodyTop + scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw left edge
      } else {                                                  //--- Handle left orientation
         if(bubblePointerBaseStart > bubbleBodyTop + scaledBodyRadius) //--- Check if base start exceeds top radius
            DrawBubbleVerticalEdge(bubbleBodyLeft, bubblePointerBaseStart, bubbleBodyLeft, bubbleBodyTop + scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw top segment of left edge
         if(bubblePointerBaseEnd < bubbleBodyBottom - scaledBodyRadius) //--- Check if base end below bottom radius
            DrawBubbleVerticalEdge(bubbleBodyLeft, bubbleBodyBottom - scaledBodyRadius, bubbleBodyLeft, bubblePointerBaseEnd, scaledThickness, borderColorARGB); //--- Draw bottom segment of left edge
      }

      if(bubblePointerOrientation == ORIENT_LEFT) {             //--- Check for left orientation
         DrawBubbleVerticalEdge(bubbleBodyRight, bubbleBodyTop + scaledBodyRadius, bubbleBodyRight, bubbleBodyBottom - scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw right edge
      } else {                                                  //--- Handle right orientation
         if(bubblePointerBaseStart > bubbleBodyTop + scaledBodyRadius) //--- Check if base start exceeds top radius
            DrawBubbleVerticalEdge(bubbleBodyRight, bubbleBodyTop + scaledBodyRadius, bubbleBodyRight, bubblePointerBaseStart, scaledThickness, borderColorARGB); //--- Draw top segment of right edge
         if(bubblePointerBaseEnd < bubbleBodyBottom - scaledBodyRadius) //--- Check if base end below bottom radius
            DrawBubbleVerticalEdge(bubbleBodyRight, bubblePointerBaseEnd, bubbleBodyRight, bubbleBodyBottom - scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw bottom segment of right edge
      }
   }

   DrawBubbleCornerArc((int)(bubbleBodyLeft + scaledBodyRadius), (int)(bubbleBodyTop + scaledBodyRadius), scaledBodyRadius, scaledThickness, borderColorARGB, M_PI, M_PI * 1.5); //--- Draw top-left arc
   DrawBubbleCornerArc((int)(bubbleBodyRight - scaledBodyRadius), (int)(bubbleBodyTop + scaledBodyRadius), scaledBodyRadius, scaledThickness, borderColorARGB, M_PI * 1.5, M_PI * 2.0); //--- Draw top-right arc
   DrawBubbleCornerArc((int)(bubbleBodyLeft + scaledBodyRadius), (int)(bubbleBodyBottom - scaledBodyRadius), scaledBodyRadius, scaledThickness, borderColorARGB, M_PI * 0.5, M_PI); //--- Draw bottom-left arc
   DrawBubbleCornerArc((int)(bubbleBodyRight - scaledBodyRadius), (int)(bubbleBodyBottom - scaledBodyRadius), scaledBodyRadius, scaledThickness, borderColorARGB, 0.0, M_PI * 0.5); //--- Draw bottom-right arc

   for(int edgeIndex = 0; edgeIndex < 3; edgeIndex++) {         //--- Loop over pointer edges
      int nextIndex = (edgeIndex + 1) % 3;                      //--- Get next index
      
      if(edgeIndex != bubblePointerApexIndex && nextIndex != bubblePointerApexIndex) continue; //--- Skip non-apex edges
      
      double startX, startY, endX, endY;                        //--- Declare edge coordinates
      
      if(edgeIndex == bubblePointerApexIndex) {                 //--- Check if from apex
         startX = bubblePointerTangentPointsX[bubblePointerApexIndex][0]; //--- Set start X from tangent
         startY = bubblePointerTangentPointsY[bubblePointerApexIndex][0]; //--- Set start Y from tangent
         endX = bubblePointerVerticesX[nextIndex];              //--- Set end X to next vertex
         endY = bubblePointerVerticesY[nextIndex];              //--- Set end Y to next vertex
      } else {                                                  //--- Handle to apex
         startX = bubblePointerVerticesX[edgeIndex];            //--- Set start X from vertex
         startY = bubblePointerVerticesY[edgeIndex];            //--- Set start Y from vertex
         endX = bubblePointerTangentPointsX[bubblePointerApexIndex][1]; //--- Set end X to tangent
         endY = bubblePointerTangentPointsY[bubblePointerApexIndex][1]; //--- Set end Y to tangent
      }

      DrawBubbleStraightEdge(startX, startY, endX, endY, scaledThickness, borderColorARGB); //--- Draw straight edge
   }

   DrawBubbleTriangleCornerArc(bubblePointerApexIndex, scaledThickness, borderColorARGB); //--- Draw apex arc
}

void DrawBubbleHorizontalEdge(double startX, double startY, double endX, double endY, int thickness, uint edgeColor) {
   DrawBubbleStraightEdge(startX, startY, endX, endY, thickness, edgeColor); //--- Draw horizontal edge using straight edge
}

void DrawBubbleVerticalEdge(double startX, double startY, double endX, double endY, int thickness, uint edgeColor) {
   DrawBubbleStraightEdge(startX, startY, endX, endY, thickness, edgeColor); //--- Draw vertical edge using straight edge
}

void DrawBubbleStraightEdge(double startX, double startY, double endX, double endY, int thickness, uint edgeColor) {
   double deltaX = endX - startX;                               //--- Compute delta X
   double deltaY = endY - startY;                               //--- Compute delta Y
   double edgeLength = MathSqrt(deltaX*deltaX + deltaY*deltaY); //--- Compute edge length
   if(edgeLength < 1e-6) return;                                //--- Return if length too small

   double perpendicularX = -deltaY / edgeLength;                //--- Compute perpendicular X
   double perpendicularY = deltaX / edgeLength;                 //--- Compute perpendicular Y

   double edgeDirectionX = deltaX / edgeLength;                 //--- Compute edge direction X
   double edgeDirectionY = deltaY / edgeLength;                 //--- Compute edge direction Y

   double halfThickness = (double)thickness / 2.0;              //--- Compute half thickness
   double extensionLength = borderExtensionMultiplier * (double)thickness; //--- Set extension length
   
   double extendedStartX = startX - edgeDirectionX * extensionLength; //--- Extend start X
   double extendedStartY = startY - edgeDirectionY * extensionLength; //--- Extend start Y
   double extendedEndX = endX + edgeDirectionX * extensionLength; //--- Extend end X
   double extendedEndY = endY + edgeDirectionY * extensionLength; //--- Extend end Y

   double verticesX[4], verticesY[4];                           //--- Declare vertices arrays
   verticesX[0] = extendedStartX - perpendicularX * halfThickness;  verticesY[0] = extendedStartY - perpendicularY * halfThickness; //--- Set vertex 0
   verticesX[1] = extendedStartX + perpendicularX * halfThickness;  verticesY[1] = extendedStartY + perpendicularY * halfThickness; //--- Set vertex 1
   verticesX[2] = extendedEndX + perpendicularX * halfThickness;  verticesY[2] = extendedEndY + perpendicularY * halfThickness; //--- Set vertex 2
   verticesX[3] = extendedEndX - perpendicularX * halfThickness;  verticesY[3] = extendedEndY - perpendicularY * halfThickness; //--- Set vertex 3

   FillQuadrilateral(bubbleHighResCanvas, verticesX, verticesY, edgeColor); //--- Fill quadrilateral for edge
}

void DrawBubbleCornerArc(int centerX, int centerY, int radius, int thickness, uint edgeColor,
                         double startAngle, double endAngle) {
   int halfThickness = thickness / 2;                           //--- Compute half thickness
   double outerRadius = (double)radius + halfThickness;         //--- Compute outer radius
   double innerRadius = (double)radius - halfThickness;         //--- Compute inner radius
   if(innerRadius < 0) innerRadius = 0;                         //--- Set inner radius to zero if negative

   int pixelRange = (int)(outerRadius + 2);                     //--- Compute pixel range

   for(int deltaY = -pixelRange; deltaY <= pixelRange; deltaY++) { //--- Loop over delta Y
      for(int deltaX = -pixelRange; deltaX <= pixelRange; deltaX++) { //--- Loop over delta X
         double distance = MathSqrt(deltaX * deltaX + deltaY * deltaY); //--- Compute distance
         if(distance < innerRadius || distance > outerRadius) continue; //--- Skip if outside radii

         double angle = MathArctan2((double)deltaY, (double)deltaX); //--- Compute angle
         
         if(IsAngleBetween(angle, startAngle, endAngle))        //--- Check if angle within range
            bubbleHighResCanvas.PixelSet(centerX + deltaX, centerY + deltaY, edgeColor); //--- Set pixel
      }
   }
}

void DrawBubbleTriangleCornerArc(int cornerIndex, int thickness, uint edgeColor) {
   double centerX = bubblePointerArcCentersX[cornerIndex];      //--- Get center X
   double centerY = bubblePointerArcCentersY[cornerIndex];      //--- Get center Y
   double radius = (double)bubblePointerApexRadiusPixels * supersamplingFactor; //--- Get scaled radius

   int halfThickness = thickness / 2;                           //--- Compute half thickness
   double outerRadius = radius + halfThickness;                 //--- Compute outer radius
   double innerRadius = radius - halfThickness;                 //--- Compute inner radius
   if(innerRadius < 0) innerRadius = 0;                         //--- Set inner radius to zero if negative

   int pixelRange = (int)(outerRadius + 2);                     //--- Compute pixel range

   for(int deltaY = -pixelRange; deltaY <= pixelRange; deltaY++) { //--- Loop over delta Y
      for(int deltaX = -pixelRange; deltaX <= pixelRange; deltaX++) { //--- Loop over delta X
         double distance = MathSqrt((double)(deltaX*deltaX + deltaY*deltaY)); //--- Compute distance
         if(distance < innerRadius || distance > outerRadius) continue; //--- Skip if outside radii

         double angle = MathArctan2((double)deltaY, (double)deltaX); //--- Compute angle
         
         if(BubbleAngleInArcSweep(cornerIndex, angle)) {        //--- Check if in arc sweep
            int pixelX = (int)MathRound(centerX + deltaX);      //--- Round to pixel X
            int pixelY = (int)MathRound(centerY + deltaY);      //--- Round to pixel Y
            if(pixelX >= 0 && pixelX < bubbleHighResCanvas.Width() && pixelY >= 0 && pixelY < bubbleHighResCanvas.Height()) //--- Check if within bounds
               bubbleHighResCanvas.PixelSet(pixelX, pixelY, edgeColor); //--- Set pixel
         }
      }
   }
}

Здесь мы реализуем функцию "DrawBubbleBorder" для отрисовки всей границы текстовой выноски на объекте Canvas высокого разрешения, масштабируя толщину и радиус тела с помощью суперсэмплирования, а затем условно рисуя сегментированные края на основе ориентации: для направления вверх/вниз, обрабатывая верхние и нижние горизонтальные линии с разделением вокруг основания указателя, если оно присутствует, и полные вертикальные линии; для направления влево/вправо — аналогично полные горизонтальные линии и сегментированные вертикальные линии, используя функции "DrawBubbleHorizontalEdge" и "DrawBubbleVerticalEdge", чтобы избежать отрисовки за точкой соединения указателя. Далее, с помощью функции "DrawBubbleCornerArc" мы рисуем четыре дуги углов тела, используя предопределенные диапазоны углов в радианах, затем перебираем циклом три края указателя (сосредоточиваясь на смежных с вершиной сторонах), чтобы добавить прямые рамки от касательных с помощью функции "DrawBubbleStraightEdge", и завершаем построением дуги вершины с помощью функции "DrawBubbleTriangleCornerArc".

Для стандартизации отрисовки ребер функции "DrawBubbleHorizontalEdge" и "DrawBubbleVerticalEdge" просто вызывают функцию "DrawBubbleStraightEdge", которая вычисляет направление и перпендикулярные векторы, расширяет конечные точки на величину "borderExtensionMultiplier" умноженную на толщину для плавных соединений, формирует четырехугольную полосу и заполняет ее с помощью функции "FillQuadrilateral", используя цвет ребра. В функции "DrawBubbleCornerArc" мы создаем изогнутое кольцо рамки, вычисляя внутренний/внешний радиусы, исходя из половины толщины, перебирая расширенную пиксельную сетку и устанавливая пиксели, если они находятся в пределах расстояния, а функция "IsAngleBetween" подтверждает сегмент дуги, обеспечивая точную кривизну. Наконец, функция "DrawBubbleTriangleCornerArc" аналогично обрабатывает кончик указателя, используя масштабированный радиус, пиксельные циклы с MathSqrt для расстояния, MathArctan2  для угла, "BubbleAngleInArcSweep" для включения и MathRound  для координат с проверкой границ перед PixelSet, обеспечивая сглаженные границы, которые плавно сливаются с основным содержимым. Теперь мы можем объединить эту логику для создания пузыря.

void DrawBubble() {
   uint backgroundColorARGB = ColorToARGBWithOpacity(bubbleBackgroundColor, bubbleBackgroundOpacityPercent); //--- Get background ARGB
   uint borderColorARGB = ColorToARGBWithOpacity(bubbleBorderColor, bubbleBorderOpacityPercent); //--- Get border ARGB

   FillBubble(backgroundColorARGB);                             //--- Fill bubble

   if(bubbleShowBorder && bubbleBorderThicknessPixels > 0)      //--- Check if border should be shown
      DrawBubbleBorder(borderColorARGB);                        //--- Draw bubble border

   BicubicDownsample(bubbleCanvas, bubbleHighResCanvas);        //--- Downsample to display canvas

   bubbleCanvas.FontSet("Arial", 13, FW_NORMAL);                //--- Set font for text
   string displayText = "Bubble";                               //--- Set display text
   int textWidth, textHeight;                                   //--- Declare text dimensions
   bubbleCanvas.TextSize(displayText, textWidth, textHeight);   //--- Get text size
   
   int bodyDisplayLeft = (int)(bubbleBodyLeft / supersamplingFactor); //--- Compute display left
   int bodyDisplayTop = (int)(bubbleBodyTop / supersamplingFactor); //--- Compute display top
   int bodyDisplayWidth = (int)((bubbleBodyRight - bubbleBodyLeft) / supersamplingFactor); //--- Compute display width
   int bodyDisplayHeight = (int)((bubbleBodyBottom - bubbleBodyTop) / supersamplingFactor); //--- Compute display height
   
   int textPositionX = bodyDisplayLeft + (bodyDisplayWidth - textWidth) / 2; //--- Compute text X position
   int textPositionY = bodyDisplayTop + (bodyDisplayHeight - textHeight) / 2; //--- Compute text Y position
   bubbleCanvas.TextOut(textPositionX, textPositionY, displayText, (uint)0xFF000000, TA_LEFT); //--- Draw text on canvas
}

void FillBubble(uint fillColor) {
   FillBubbleRoundedRectangle(bubbleBodyLeft, bubbleBodyTop, bubbleBodyRight - bubbleBodyLeft, bubbleBodyBottom - bubbleBodyTop, 
                              bubbleBodyCornerRadiusPixels * supersamplingFactor, fillColor); //--- Fill bubble body rectangle

   FillBubbleRoundedTriangle(fillColor);                        //--- Fill bubble pointer triangle
}

Для объединения логики отрисовки мы определяем функцию "DrawBubble", которая управляет отображением текстовой выноски на объекте Canvas, начиная с преобразования цветов фона и рамки в ARGB с прозрачностью с помощью "ColorToARGBWithOpacity" для создания полупрозрачных эффектов. Для создания фигуры мы вызываем функцию "FillBubble" с цветом фона, чтобы заполнить объект Canvas высокого разрешения, а если включены рамки, вызываем "DrawBubbleBorder" для добавления контура. Затем мы понижаем дискретизацию изображения до стандартного объекта Canvas, используя "BicubicDownsample" для получения эффекта сглаживания. Наконец, на стандартном объекте Canvas мы устанавливаем шрифт "Arial" размером 13 с параметром FW_NORMAL, измеряем и центрируем метку "Bubble" в пределах тела пузыря, масштабированных до координат отображения, и рисуем ее с помощью TextOut  сплошным черным цветом, выровненным по левому краю для идентификации.

В функции "FillBubble" мы заполняем тело элементом "FillBubbleRoundedRectangle", используя предварительно вычисленные координаты и масштабированный радиус, затем добавляем заливку указателя с помощью функции "FillBubbleRoundedTriangle", плавно объединяя фигуры в высоком разрешении для получения единого пузыря. Теперь можно вызвать эту функцию во время инициализации, чтобы также отобразить пузырь.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   
   // added this for the bubble in initialization alongside the other prior shapes
   
   bubbleIsHorizontalOrientation = (bubblePointerOrientation == ORIENT_LEFT || bubblePointerOrientation == ORIENT_RIGHT); //--- Determine if orientation is horizontal

   int bubbleCanvasWidth, bubbleCanvasHeight;                   //--- Declare bubble canvas dimensions
   if(bubbleIsHorizontalOrientation) {                          //--- Check for horizontal orientation
      bubbleCanvasWidth = bubbleBodyWidthPixels + bubblePointerHeightPixels + 40; //--- Compute width for horizontal
      bubbleCanvasHeight = bubbleBodyHeightPixels + 40;         //--- Compute height for horizontal
   } else {                                                     //--- Handle vertical orientation
      bubbleCanvasWidth = bubbleBodyWidthPixels + 40;           //--- Compute width for vertical
      bubbleCanvasHeight = bubbleBodyHeightPixels + bubblePointerHeightPixels + 40; //--- Compute height for vertical
   }

   int bubblePositionY = trianglePositionY + triangleCanvasHeight + shapesGapPixels; //--- Set bubble Y position below triangle

   if(!bubbleCanvas.CreateBitmapLabel(0, 0, bubbleCanvasName, shapesPositionX, bubblePositionY,
                                    bubbleCanvasWidth, bubbleCanvasHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create bubble canvas bitmap label
      Print("Error creating bubble canvas: ", GetLastError());  //--- Print error message if creation fails
      return(INIT_FAILED);                                      //--- Return initialization failure
   }

   if(!bubbleHighResCanvas.Create(bubbleCanvasName + "_hires",
                        bubbleCanvasWidth * supersamplingFactor,
                        bubbleCanvasHeight * supersamplingFactor, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create high-res bubble canvas
      Print("Error creating bubble hi-res canvas: ", GetLastError()); //--- Print error message if creation fails
      return(INIT_FAILED);                                      //--- Return initialization failure
   }

   bubbleCanvas.Erase(ColorToARGB(clrNONE, 0));                 //--- Clear bubble canvas
   bubbleHighResCanvas.Erase(ColorToARGB(clrNONE, 0));          //--- Clear high-res bubble canvas

   PrecomputeBubbleGeometry();                                  //--- Precompute bubble geometry
   
   DrawBubble();                                                //--- Draw bubble

   bubbleCanvas.Update();                                       //--- Update bubble canvas display

   return(INIT_SUCCEEDED);                                      //--- Return initialization success
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   rectangleHighResCanvas.Destroy();                            //--- Destroy high-res rectangle canvas
   rectangleCanvas.Destroy();                                   //--- Destroy rectangle canvas
   ObjectDelete(0, rectangleCanvasName);                        //--- Delete rectangle canvas object

   triangleHighResCanvas.Destroy();                             //--- Destroy high-res triangle canvas
   triangleCanvas.Destroy();                                    //--- Destroy triangle canvas
   ObjectDelete(0, triangleCanvasName);                         //--- Delete triangle canvas object

   bubbleHighResCanvas.Destroy();                               //--- Destroy high-res bubble canvas
   bubbleCanvas.Destroy();                                      //--- Destroy bubble canvas
   ObjectDelete(0, bubbleCanvasName);                           //--- Delete bubble canvas object

   ChartRedraw();                                               //--- Redraw chart
}

//+------------------------------------------------------------------+
//| Chart event function                                             |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam) {
   if(id == CHARTEVENT_CHART_CHANGE) {                          //--- Check for chart change event
      rectangleCanvas.Erase(ColorToARGB(clrNONE, 0));           //--- Clear rectangle canvas
      rectangleHighResCanvas.Erase(ColorToARGB(clrNONE, 0));    //--- Clear high-res rectangle canvas
      DrawRoundedRectangle();                                   //--- Redraw rounded rectangle
      rectangleCanvas.Update();                                 //--- Update rectangle canvas display

      triangleCanvas.Erase(ColorToARGB(clrNONE, 0));            //--- Clear triangle canvas
      triangleHighResCanvas.Erase(ColorToARGB(clrNONE, 0));     //--- Clear high-res triangle canvas
      DrawRoundedTriangle();                                    //--- Redraw rounded triangle
      triangleCanvas.Update();                                  //--- Update triangle canvas display

      bubbleCanvas.Erase(ColorToARGB(clrNONE, 0));              //--- Clear bubble canvas
      bubbleHighResCanvas.Erase(ColorToARGB(clrNONE, 0));       //--- Clear high-res bubble canvas
      DrawBubble();                                             //--- Redraw bubble
      bubbleCanvas.Update();                                    //--- Update bubble canvas display
   }
}

Мы улучшаем обработчик OnInit, чтобы интегрировать текстовую выноску с предыдущими фигурами, определяя, является ли ориентация горизонтальной, с помощью параметра "bubbleIsHorizontalOrientation" на основе левого или правого указателей, а затем соответствующим образом вычисляя размеры объекта Canvas, добавляя высоту указателя к ширине для горизонтальной ориентации или к высоте для вертикальной, с отступами. Далее размещаем пузырь под треугольником, используя значение Y из предыдущего объекта Canvas плюс зазор, создаем стандартный объект Canvas с помощью функции "CreateBitmapLabel" с нормализацией ARGB для прозрачности, а также версию с высоким разрешением с помощью функции Create, используя масштабированные размеры, выводя сообщения об ошибках и сообщая о неудачной инициализации в случае сбоя. Для подготовки к рендерингу мы очищаем оба параметра прозрачным ARGB, вызываем метод "PrecomputeBubbleGeometry" для формирования структуры, рисуем с помощью метода "DrawBubble", обновляем отображение и возвращаем INIT_SUCCEEDED.

В обработчике OnDeinit расширяем процесс очистки, уничтожая объекты Canvas в виде пузырей высокого разрешения и стандартные, удаляя объект и перерисовывая график. Для большей ясности мы выделили конкретные расширения.

Наконец, в обработчике OnChartEvent для события CHARTEVENT_CHART_CHANGE мы добавляем обработку пузырей путем очистки, перерисовки с помощью функции "DrawBubble" и обновления вместе с прямоугольником и треугольником для адаптивного отображения изменений на графике. После компиляции получаем следующий результат.

ROUNDED SPEECH BUBBLE

На изображении видно, что пузырь отрисован корректно в соответствии с нашими целями. Теперь еще одно внесенное нами изменение для контроля длины расширения рамки путем удаления фиксированной длины и включения соотношения длин для более динамичного управления. Мы посчитали, что так лучше, потому что при увеличении ширины рамки соединение между рамками указателя и прямоугольника не было бесшовным. Для обеспечения согласованности мы обновили расширение рамки в функциях "DrawRectStraightEdge" и "DrawTriStraightEdge" следующим образом.

double extensionLength = borderExtensionMultiplier * (double)thickness; //--- Set extension length

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

EXTENSION LENGTHS COMPARISON

На изображении видно, как коэффициент удлинения влияет на пузырьковые соединения. Это особенно важно, когда вы используете фигуры большой ширины. Это был самый простой способ решить проблему с соединением, динамически задавая коэффициент расширения. Если вам нужен более сложный и плавный подход, можно рассмотреть возможность использования подхода с переходными кривыми, обычно называемого подходом кривых Безье. Я изучал это на факультете проектирования автомобильных дорог, и вкратце это выглядит так.

BEZIER CURVES

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


Тестирование на истории

Мы провели тестирование, а ниже показан итоговый результат в формате Graphics Interchange Format (GIF).

BACKTEST GIF


Заключение

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

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

Прикрепленные файлы |
Самооптимизирующиеся советники на MQL5 (Часть 12): Построение линейных классификаторов с использованием факторизации матриц Самооптимизирующиеся советники на MQL5 (Часть 12): Построение линейных классификаторов с использованием факторизации матриц
В данной статье рассматривается важная роль матричного разложения в алгоритмической торговле, в частности в приложениях MQL5. От регрессионных моделей до многоклассовых классификаторов — мы рассмотрим практические примеры, демонстрирующие, насколько легко эти методы можно интегрировать с помощью встроенных функций MQL5. Независимо от того, занимаетесь ли вы прогнозированием направления движения цен или моделированием поведения индикаторов, данное руководство заложит прочную основу для создания интеллектуальных торговых систем с использованием матричных методов.
Самооптимизирующиеся советники в MQL5 (Часть 11): Введение в основы линейной алгебры Самооптимизирующиеся советники в MQL5 (Часть 11): Введение в основы линейной алгебры
В ходе этого обсуждения мы заложим основу для использования мощных инструментов линейной алгебры, реализованных в API матриц и векторов MQL5. Чтобы умело использовать этот API, нам необходимо хорошо понимать принципы линейной алгебры, лежащие в основе эффективного применения этих методов. Цель этой статьи — дать читателю интуитивное представление о некоторых из наиболее важных правил линейной алгебры, которые нам, как алгоритмическим трейдерам в MQL5, необходимы для начала работы с этой мощной библиотекой.
Код, слёзы и Algo Forge Код, слёзы и Algo Forge
В статье рассматривается переход на MQL5 Algo Forge как современный и удобный формат публикации программного кода и вложений к статьям. Использование репозиториев вместо классических ZIP-архивов и исходных кодов позволяет поддерживать проекты в актуальном состоянии, оперативно вносить правки и профессионально взаимодействовать с аудиторией. Приводятся рекомендации по быстрой миграции наработок в облачную среду через интерфейс MetaEditor.
Статистический арбитраж на коинтегрированных акциях (Часть 2): Советник, тестирование  и оптимизация Статистический арбитраж на коинтегрированных акциях (Часть 2): Советник, тестирование и оптимизация
В данной статье представлен пример реализации советника для торговли корзиной из четырёх акций компаний, котирующихся на Nasdaq. Сначала акции были отфильтрованы на основе тестов на корреляцию Пирсона. Затем для отфильтрованной группы была проведена проверка на коинтеграцию с помощью тестов Йохансена. Наконец, стационарность коинтегрированного спреда проверялась с помощью тестов ADF и KPSS. Здесь мы рассмотрим некоторые замечания по поводу этого процесса, а также результаты бэктестов после небольшой оптимизации.