English Deutsch 日本語
preview
Создание торговых систем с искусственным интеллектом на MQL5 (Часть 3): Переход к прокручиваемому интерфейсу с единым окном чата

Создание торговых систем с искусственным интеллектом на MQL5 (Часть 3): Переход к прокручиваемому интерфейсу с единым окном чата

MetaTrader 5Торговые системы |
46 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В нашей предыдущей статье (Часть 2) мы создали интерактивную программу с поддержкой ChatGPT и пользовательским интерфейсом (UI) на языке MetaQuotes Language 5 (MQL5). Инструмент позволил отправлять запросы в API OpenAI и сразу просматривать ответы непосредственно на графике. В Части 3 мы развиваем программу дальше: создаем прокручиваемую чат-панель с отметками времени, плавной динамической прокруткой и подробной историей диалогов для многоходового общения. Ниже мы подробно разберем следующее:

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

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


Устройство обновленной программы с ChatGPT

Обновленная программа с ChatGPT расширяет наш торговый интерфейс на базе ИИ: в него добавлены прокрутка, единое окно чата с поддержкой многоходовых диалогов, отметки времени и динамическая обработка сообщений. Это позволяет сохранять контекст диалога между сеансами. Задача обновленной программы – обеспечить удобное непрерывное взаимодействие в диалоге: пользователь может просматривать историю и развивать предыдущие ответы ИИ. Это важно для доработки торговых стратегий без потери результатов прошлых взаимодействий. Мы пришли к выводу, что если придерживаться одного диалога, к которому ИИ сможет обращаться, то можно в любой момент вернуться к запросам, уточнить их и внести необходимые исправления.

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

UPGRADED UI ROADMAP


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

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

//+------------------------------------------------------------------+
//|                                         ChatGPT AI EA Part 3.mq5 |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link "https://t.me/Forex_Algo_Trader"
#property version "1.00"
#property strict

//--- Input parameters
enum ENUM_SCROLLBAR_MODE
{
   SCROLL_DYNAMIC_ALWAYS, // Show when needed
   SCROLL_DYNAMIC_HOVER,  // Show on hover when needed
   SCROLL_WHEEL_ONLY      // No scrollbar, wheel scroll only
};
input ENUM_SCROLLBAR_MODE ScrollbarMode = SCROLL_DYNAMIC_HOVER;            // Scrollbar Behavior
//--- Scrollbar object names
#define SCROLL_LEADER "ChatGPT_Scroll_Leader"
#define SCROLL_UP_REC "ChatGPT_Scroll_Up_Rec"
#define SCROLL_UP_LABEL "ChatGPT_Scroll_Up_Label"
#define SCROLL_DOWN_REC "ChatGPT_Scroll_Down_Rec"
#define SCROLL_DOWN_LABEL "ChatGPT_Scroll_Down_Label"
#define SCROLL_SLIDER "ChatGPT_Scroll_Slider"

//--- Input parameters
input string OpenAI_Model = "gpt-3.5-turbo";                                 // OpenAI Model
input string OpenAI_Endpoint = "https://api.openai.com/v1/chat/completions"; // OpenAI API Endpoint
input int MaxResponseLength = 3000;                                          // Max length of ChatGPT response to display
input string LogFileName = "ChatGPT_EA_Log.txt";                             // Log file name

Реализацию обновления начинаем с определения параметров конфигурации и констант, управляющих прокруткой и настройками API. Сначала создаем перечисление ENUM_SCROLLBAR_MODE с опциями SCROLL_DYNAMIC_ALWAYS (показывать полосу прокрутки при необходимости), SCROLL_DYNAMIC_HOVER (показывать при наведении) и SCROLL_WHEEL_ONLY (без полосы прокрутки, только прокрутка колесом мыши). В качестве значения по умолчанию для пользовательской настройки присваиваем параметру ScrollbarMode значение SCROLL_DYNAMIC_HOVER.

Затем определяем константы для имен объектов полосы прокрутки: SCROLL_LEADER, SCROLL_UP_REC, SCROLL_UP_LABEL, SCROLL_DOWN_REC, SCROLL_DOWN_LABEL и SCROLL_SLIDER. Это обеспечит единообразное обращение к ним в интерфейсе. Далее входному параметру MaxResponseLength присваиваем значение 3000, чтобы ограничить длину ответа и поддерживать более длинные диалоги. Далее немного доработаем класс JSON, чтобы он также мог обрабатывать значения типа double.

bool DeserializeFromArray(char &jsonCharacterArray[], int arrayLength, int &currentIndex) { //--- Deserialize from array
   string validNumericCharacters = "0123456789+-.eE";     //--- Valid number chars
   int startPosition = currentIndex;                      //--- Start position
   for(; currentIndex < arrayLength; currentIndex++) {    //--- Loop array
      char currentCharacter = jsonCharacterArray[currentIndex]; //--- Current char
      if(currentCharacter == 0) break;                    //--- Break on null
      switch(currentCharacter) {                          //--- Switch on char
         case '\t': case '\r': case '\n': case ' ': startPosition = currentIndex + 1; break; //--- Skip whitespace
         case '[': {                                      //--- Array start
            startPosition = currentIndex + 1;             //--- Update start
            if(m_type != JsonUndefined) return false;     //--- Type check
            m_type = JsonArray;                           //--- Set array
            currentIndex++;                               //--- Increment
            JsonValue childValue(GetPointer(this), JsonUndefined); //--- Child value
            while(childValue.DeserializeFromArray(jsonCharacterArray, arrayLength, currentIndex)) { //--- Loop children
               if(childValue.m_type != JsonUndefined) AddChild(childValue); //--- Add if defined
               if(childValue.m_type == JsonInteger || childValue.m_type == JsonDouble || childValue.m_type == JsonArray) currentIndex++; //--- Adjust index
               childValue.Reset();                        //--- Reset child
               childValue.m_parent = GetPointer(this);    //--- Set parent
               if(jsonCharacterArray[currentIndex] == ']') break; //--- End array
               currentIndex++;                            //--- Increment
               if(currentIndex >= arrayLength) return false; //--- Bounds check
            }
            return (jsonCharacterArray[currentIndex] == ']' || jsonCharacterArray[currentIndex] == 0); //--- Valid end
         }                                                //--- End array case
         case ']': return (m_parent && m_parent.m_type == JsonArray); //--- Array end
         case ':': {                                      //--- Key separator
            if(m_temporaryKey == "") return false;        //--- Key check
            JsonValue childValue(GetPointer(this), JsonUndefined); //--- New child
            JsonValue *addedChild = AddChild(childValue); //--- Add
            addedChild.m_key = m_temporaryKey;            //--- Set key
            m_temporaryKey = "";                          //--- Clear temp
            currentIndex++;                               //--- Increment
            if(!addedChild.DeserializeFromArray(jsonCharacterArray, arrayLength, currentIndex)) return false; //--- Recurse
         } break;                                         //--- End key case
         case ',': {                                      //--- Value separator
            startPosition = currentIndex + 1;             //--- Update start
            if(!m_parent && m_type != JsonObject) return false; //--- Check context
            if(m_parent && m_parent.m_type != JsonArray && m_parent.m_type != JsonObject) return false; //--- Parent type
            if(m_parent && m_parent.m_type == JsonArray && m_type == JsonUndefined) return true; //--- Undefined in array
         } break;                                         //--- End separator
         case '{': {                                      //--- Object start
            startPosition = currentIndex + 1;             //--- Update start
            if(m_type != JsonUndefined) return false;     //--- Type check
            m_type = JsonObject;                          //--- Set object
            currentIndex++;                               //--- Increment
            if(!DeserializeFromArray(jsonCharacterArray, arrayLength, currentIndex)) return false; //--- Recurse
            return (jsonCharacterArray[currentIndex] == '}' || jsonCharacterArray[currentIndex] == 0); //--- Valid end
         } break;                                         //--- End object case
         case '}': return (m_type == JsonObject);         //--- Object end
         case 't': case 'T': case 'f': case 'F': {        //--- Boolean start
            if(m_type != JsonUndefined) return false;     //--- Type check
            m_type = JsonBoolean;                         //--- Set boolean
            if(currentIndex + 3 < arrayLength && StringCompare(GetSubstringFromArray(jsonCharacterArray, currentIndex, 4), "true", false) == 0) { //--- True check
               m_booleanValue = true; currentIndex += 3; return true; //--- Set true
            }
            if(currentIndex + 4 < arrayLength && StringCompare(GetSubstringFromArray(jsonCharacterArray, currentIndex, 5), "false", false) == 0) { //--- False check
               m_booleanValue = false; currentIndex += 4; return true; //--- Set false
            }
            return false;                                 //--- Invalid boolean
         } break;                                         //--- End boolean
         case 'n': case 'N': {                            //--- Null start
            if(m_type != JsonUndefined) return false;     //--- Type check
            m_type = JsonNull;                            //--- Set null
            if(currentIndex + 3 < arrayLength && StringCompare(GetSubstringFromArray(jsonCharacterArray, currentIndex, 4), "null", false) == 0) { //--- Null check
               currentIndex += 3; return true;            //--- Valid null
            }
            return false;                                 //--- Invalid null
         } break;                                         //--- End null
         case '0': case '1': case '2': case '3': case '4':
         case '5': case '6': case '7': case '8': case '9':
         case '-': case '+': case '.': {                  //--- Number start
            if(m_type != JsonUndefined) return false;     //--- Type check
            bool isDouble = false;                        //--- Double flag
            int startOfNumber = currentIndex;             //--- Number start
            while(jsonCharacterArray[currentIndex] != 0 && currentIndex < arrayLength) { //--- Parse number
               currentIndex++;                            //--- Increment
               if(StringFind(validNumericCharacters, GetSubstringFromArray(jsonCharacterArray, currentIndex, 1)) < 0) break; //--- Invalid char
               if(!isDouble) isDouble = (jsonCharacterArray[currentIndex] == '.' || jsonCharacterArray[currentIndex] == 'e' || jsonCharacterArray[currentIndex] == 'E'); //--- Set double
            }
            m_stringValue = GetSubstringFromArray(jsonCharacterArray, startOfNumber, currentIndex - startOfNumber); //--- Get string
            if(isDouble) {                                //--- Double handling
               m_type = JsonDouble;                       //--- Set type
               m_doubleValue = StringToDouble(m_stringValue); //--- Convert double
               m_integerValue = (long)m_doubleValue;      //--- Set integer
               m_booleanValue = m_integerValue != 0;      //--- Set boolean
            } else {                                      //--- Integer handling
               m_type = JsonInteger;                      //--- Set type
               m_integerValue = StringToInteger(m_stringValue); //--- Convert integer
               m_doubleValue = (double)m_integerValue;    //--- Set double
               m_booleanValue = m_integerValue != 0;      //--- Set boolean
            }
            currentIndex--;                               //--- Adjust index
            return true;                                  //--- Success
         } break;                                         //--- End number
         case '\"': {                                     //--- String or key start
            if(m_type == JsonObject) {                    //--- Key in object
               currentIndex++;                            //--- Increment
               int startOfString = currentIndex;          //--- String start
               if(!ExtractStringFromArray(jsonCharacterArray, arrayLength, currentIndex)) return false; //--- Extract
               m_temporaryKey = GetSubstringFromArray(jsonCharacterArray, startOfString, currentIndex - startOfString); //--- Set temp key
            } else {                                      //--- Value string
               if(m_type != JsonUndefined) return false;  //--- Type check
               m_type = JsonString;                       //--- Set string
               currentIndex++;                            //--- Increment
               int startOfString = currentIndex;          //--- String start
               if(!ExtractStringFromArray(jsonCharacterArray, arrayLength, currentIndex)) return false; //--- Extract
               SetFromString(JsonString, GetSubstringFromArray(jsonCharacterArray, startOfString, currentIndex - startOfString)); //--- Set value
               return true;                               //--- Success
            }
         } break;                                         //--- End string
      }
   }
   return true;                                           //--- Default success
}

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

bool clear_hover = false;
bool new_chat_hover = false;

color clear_original_bg = clrLightCoral;
color clear_darker_bg;
color new_chat_original_bg = clrLightBlue;
color new_chat_darker_bg;
int g_mainX = 10;
int g_mainY = 30;
int g_mainWidth = 550;
int g_mainHeight = 0;
int g_padding = 10;
int g_sidePadding = 6;
int g_textPadding = 10;
int g_headerHeight = 40;
int g_displayHeight = 280;
int g_footerHeight = 50;
int g_lineSpacing = 2;
bool scroll_visible = false;
bool mouse_in_display = false;
int scroll_pos = 0;
int prev_scroll_pos = -1;
int slider_height = 20;
bool movingStateSlider = false;
int mlbDownX_Slider = 0;
int mlbDownY_Slider = 0;
int mlbDown_YD_Slider = 0;
int g_total_height = 0;
int g_visible_height = 0;

Здесь инициализируем дополнительные глобальные переменные и цветовые схемы для обновленной программы. Они обеспечат динамические эффекты наведения и управление компоновкой. Для кнопок очистки и нового чата инициализируем флаги наведения clear_hover и new_chat_hover значением false. В качестве исходных цветов фона задаем clear_original_bg = clrLightCoral и new_chat_original_bg = clrLightBlue; для состояния наведения определяем более темные варианты clear_darker_bg и new_chat_darker_bg. Затем настраиваем размеры панели: g_mainX = 10, g_mainY = 30, g_mainWidth = 550, g_mainHeight = 0 (будет вычислена); отступы g_padding = 10, g_sidePadding = 6 и g_textPadding = 10; высоты заголовка g_headerHeight = 40, области отображения g_displayHeight = 280 и нижней области g_footerHeight = 50; межстрочный интервал g_lineSpacing = 2.

Наконец, инициализируем переменные прокрутки: scroll_visible и mouse_in_display = false; scroll_pos = 0 и prev_scroll_pos = -1; slider_height = 20; флаг перетаскивания movingStateSlider = false; позиции mlbDownX_Slider, mlbDownY_Slider и mlbDown_YD_Slider = 0; значения высоты g_total_height и g_visible_height = 0. Затем до обновления области отображения нужно определить логику полосы прокрутки, поскольку она будет динамической. Определим функции полосы прокрутки.

//+------------------------------------------------------------------+
//| Calculate font size based on screen DPI                          |
//+------------------------------------------------------------------+
int getFontSizeByDPI(int baseFontSize, int baseDPI = 96) {
   int currentDPI = (int)TerminalInfoInteger(TERMINAL_SCREEN_DPI);          //--- Retrieve current screen DPI
   int scaledFontSize = (int)(baseFontSize * (double)baseDPI / currentDPI); //--- Calculate scaled font size
   return MathMax(scaledFontSize, 8);                                       //--- Ensure minimum font size of 8
}

//+------------------------------------------------------------------+
//| Create scrollbar elements                                        |
//+------------------------------------------------------------------+
void CreateScrollbar() {
   int displayX = g_mainX + g_sidePadding;          //--- Calculate display x position
   int displayY = g_mainY + g_headerHeight + g_padding; //--- Calculate display y position
   int displayW = g_mainWidth - 2 * g_sidePadding;  //--- Calculate display width
   int scrollbar_x = displayX + displayW - 16;      //--- Set scrollbar x position
   int scrollbar_y = displayY + 16;                 //--- Set scrollbar y position
   int scrollbar_width = 16;                        //--- Set scrollbar width
   int scrollbar_height = g_displayHeight - 2 * 16; //--- Calculate scrollbar height
   int button_size = 16;                            //--- Set button size
   if (!createRecLabel(SCROLL_LEADER, scrollbar_x, scrollbar_y, scrollbar_width, scrollbar_height, C'220,220,220', 1, clrGainsboro, BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER)) { //--- Create scrollbar leader
      FileWriteString(logFileHandle, "Failed to create scrollbar leader\n"); //--- Log failure
   }
   if (!createRecLabel(SCROLL_UP_REC, scrollbar_x, displayY, scrollbar_width, button_size, clrGainsboro, 1, clrGainsboro, BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER)) { //--- Create scroll up button
      FileWriteString(logFileHandle, "Failed to create scrollbar up button\n"); //--- Log failure
   }
   if (!createLabel(SCROLL_UP_LABEL, scrollbar_x + 2, displayY + -2, CharToString(0x35), clrDimGray, getFontSizeByDPI(10), "Webdings", CORNER_LEFT_UPPER)) { //--- Create scroll up label
      FileWriteString(logFileHandle, "Failed to create scrollbar up label\n"); //--- Log failure
   }
   if (!createRecLabel(SCROLL_DOWN_REC, scrollbar_x, displayY + g_displayHeight - button_size, scrollbar_width, button_size, clrGainsboro, 1, clrGainsboro, BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER)) { //--- Create scroll down button
      FileWriteString(logFileHandle, "Failed to create scrollbar down button\n"); //--- Log failure
   }
   if (!createLabel(SCROLL_DOWN_LABEL, scrollbar_x + 2, displayY + g_displayHeight - button_size + -2, CharToString(0x36), clrDimGray, getFontSizeByDPI(10), "Webdings", CORNER_LEFT_UPPER)) { //--- Create scroll down label
      FileWriteString(logFileHandle, "Failed to create scrollbar down label\n"); //--- Log failure
   }
   slider_height = CalculateSliderHeight();         //--- Calculate slider height
   if (!createRecLabel(SCROLL_SLIDER, scrollbar_x, displayY + g_displayHeight - button_size - slider_height, scrollbar_width, slider_height, clrSilver, 1, clrGainsboro, BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER)) { //--- Create scrollbar slider
      FileWriteString(logFileHandle, "Failed to create scrollbar slider\n"); //--- Log failure
   }
   FileWriteString(logFileHandle, "Scrollbar created: x=" + IntegerToString(scrollbar_x) + ", y=" + IntegerToString(scrollbar_y) + ", height=" + IntegerToString(scrollbar_height) + ", slider_height=" + IntegerToString(slider_height) + "\n"); //--- Log scrollbar creation
}

//+------------------------------------------------------------------+
//| Delete scrollbar elements                                        |
//+------------------------------------------------------------------+
void DeleteScrollbar() {
   ObjectDelete(0, SCROLL_LEADER);                  //--- Remove scrollbar leader
   ObjectDelete(0, SCROLL_UP_REC);                  //--- Remove scroll up rectangle
   ObjectDelete(0, SCROLL_UP_LABEL);                //--- Remove scroll up label
   ObjectDelete(0, SCROLL_DOWN_REC);                //--- Remove scroll down rectangle
   ObjectDelete(0, SCROLL_DOWN_LABEL);              //--- Remove scroll down label
   ObjectDelete(0, SCROLL_SLIDER);                  //--- Remove scrollbar slider
}

//+------------------------------------------------------------------+
//| Calculate scrollbar slider height                                |
//+------------------------------------------------------------------+
int CalculateSliderHeight() {
   int scroll_area_height = g_displayHeight - 2 * 16;                 //--- Calculate scroll area height
   int slider_min_height = 20;                                        //--- Set minimum slider height
   if (g_total_height <= g_visible_height) return scroll_area_height; //--- Return full height if no scroll
   double visible_ratio = (double)g_visible_height / g_total_height;  //--- Calculate visible ratio
   int height = (int)MathFloor(scroll_area_height * visible_ratio);   //--- Calculate slider height
   return MathMax(slider_min_height, height);                         //--- Return minimum or calculated height
}

//+------------------------------------------------------------------+
//| Update scrollbar slider position                                 |
//+------------------------------------------------------------------+
void UpdateSliderPosition() {
   int displayX = g_mainX + g_sidePadding;                              //--- Calculate display x position
   int displayY = g_mainY + g_headerHeight + g_padding;                 //--- Calculate display y position
   int scrollbar_x = displayX + (g_mainWidth - 2 * g_sidePadding) - 16; //--- Set scrollbar x position
   int scrollbar_y = displayY + 16;                                     //--- Set scrollbar y position
   int scroll_area_height = g_displayHeight - 2 * 16;                   //--- Calculate scroll area height
   int max_scroll = MathMax(0, g_total_height - g_visible_height);      //--- Calculate maximum scroll
   if (max_scroll <= 0) return;                                         //--- Exit if no scroll needed
   double scroll_ratio = (double)scroll_pos / max_scroll;               //--- Calculate scroll ratio
   int scroll_area_y_max = scrollbar_y + scroll_area_height - slider_height; //--- Calculate max slider y
   int scroll_area_y_min = scrollbar_y;                                 //--- Set min slider y
   int new_y = scroll_area_y_min + (int)(scroll_ratio * (scroll_area_y_max - scroll_area_y_min)); //--- Calculate new y position
   new_y = MathMax(scroll_area_y_min, MathMin(new_y, scroll_area_y_max)); //--- Clamp y position
   ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE, new_y);        //--- Update slider y position
   FileWriteString(logFileHandle, "Slider position updated: scroll_pos=" + IntegerToString(scroll_pos) + ", max_scroll=" + IntegerToString(max_scroll) + ", new_y=" + IntegerToString(new_y) + "\n"); //--- Log slider update
}

//+------------------------------------------------------------------+
//| Update scrollbar button colors                                   |
//+------------------------------------------------------------------+
void UpdateButtonColors() {
   int max_scroll = MathMax(0, g_total_height - g_visible_height);      //--- Calculate maximum scroll
   if (scroll_pos == 0) {                                               //--- Check if at top
      ObjectSetInteger(0, SCROLL_UP_LABEL, OBJPROP_COLOR, clrSilver);   //--- Set scroll up label to disabled color
   } else {                                                             //--- Not at top
      ObjectSetInteger(0, SCROLL_UP_LABEL, OBJPROP_COLOR, clrDimGray);  //--- Set scroll up label to active color
   }
   if (scroll_pos == max_scroll) {                                      //--- Check if at bottom
      ObjectSetInteger(0, SCROLL_DOWN_LABEL, OBJPROP_COLOR, clrSilver); //--- Set scroll down label to disabled color
   } else {                                                             //--- Not at bottom
      ObjectSetInteger(0, SCROLL_DOWN_LABEL, OBJPROP_COLOR, clrDimGray); //--- Set scroll down label to active color
   }
   FileWriteString(logFileHandle, "Button colors updated: scroll_pos=" + IntegerToString(scroll_pos) + ", max_scroll=" + IntegerToString(max_scroll) + "\n"); //--- Log button color update
}

//+------------------------------------------------------------------+
//| Scroll up (show earlier messages)                                |
//+------------------------------------------------------------------+
void ScrollUp() {
   if (scroll_pos > 0) {                            //--- Check if scroll possible
      scroll_pos = MathMax(0, scroll_pos - 30);     //--- Decrease scroll position
      UpdateResponseDisplay();                      //--- Update response display
      if (scroll_visible) {                         //--- Check if scrollbar visible
         UpdateSliderPosition();                    //--- Update slider position
         UpdateButtonColors();                      //--- Update button colors
      }
      FileWriteString(logFileHandle, "Scrolled up: scroll_pos=" + IntegerToString(scroll_pos) + "\n"); //--- Log scroll up
   }
}

//+------------------------------------------------------------------+
//| Scroll down (show later messages)                                |
//+------------------------------------------------------------------+
void ScrollDown() {
   int max_scroll = MathMax(0, g_total_height - g_visible_height); //--- Calculate maximum scroll
   if (scroll_pos < max_scroll) {                   //--- Check if scroll possible
      scroll_pos = MathMin(max_scroll, scroll_pos + 30); //--- Increase scroll position
      UpdateResponseDisplay();                      //--- Update response display
      if (scroll_visible) {                         //--- Check if scrollbar visible
         UpdateSliderPosition();                    //--- Update slider position
         UpdateButtonColors();                      //--- Update button colors
      }
      FileWriteString(logFileHandle, "Scrolled down: scroll_pos=" + IntegerToString(scroll_pos) + "\n"); //--- Log scroll down
   }
}

Для адаптивного интерфейса с чатом функция getFontSizeByDPI получает DPI экрана (точек на дюйм) через TerminalInfoInteger с использованием TERMINAL_SCREEN_DPI, масштабирует базовый размер шрифта относительно стандартного DPI 96 и с помощью MathMax задает минимальный размер 8. Это поддерживает читаемость текста на разных экранах. В функции CreateScrollbar вычисляем позиции displayX и displayY, а также размеры полосы прокрутки. Затем создаем направляющий прямоугольник SCROLL_LEADER со светло-серым фоном C'220,220,220', кнопки вверх и вниз SCROLL_UP_REC и SCROLL_DOWN_REC с цветом clrGainsboro и их метки SCROLL_UP_LABEL и SCROLL_DOWN_LABEL со стрелками Webdings 0x35 и 0x36. В функциях createLabel и createRecLabel используем размер шрифта с учетом DPI, а ошибки записываем в файл журнала через FileWriteString.

Функция DeleteScrollbar удаляет все объекты полосы прокрутки с помощью ObjectDelete. В функции CalculateSliderHeight вычисляем высоту ползунка полосы прокрутки по отношению высоты видимой области к общей высоте текста, при этом минимальная высота составляет 20 пикселей. Благодаря этому ползунок остается удобным в использовании при увеличении длины диалога. Функция UpdateSliderPosition корректирует координату Y ползунка по коэффициенту прокрутки, рассчитанному на основе scroll_pos и max_scroll. Она ограничивает положение допустимыми значениями и записывает обновления в журнал. В UpdateButtonColors задаем цвет кнопок прокрутки: clrSilver, если кнопка отключена при достижении верхнего или нижнего положения, и clrDimGray, если она активна. Изменения записываются в журнал. Функции ScrollUp и ScrollDown изменяют scroll_pos на 30 пикселей, вызывают UpdateResponseDisplay, обновляют полосу прокрутки, если она видима, и записывают действия в журнал. В результате получается динамический прокручиваемый интерфейс чата с адаптивным размером текста.

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

//+------------------------------------------------------------------+
//| Wrap text respecting newlines and max width                      |
//+------------------------------------------------------------------+
void WrapText(const string inputText, const string font, const int fontSize, const int maxWidth, string &wrappedLines[], int offset = 0) {
   const int maxChars = 60;                         //--- Set maximum characters per line
   ArrayResize(wrappedLines, 0);                    //--- Clear wrapped lines array
   TextSetFont(font, fontSize);                     //--- Set font
   string paragraphs[];                             //--- Declare paragraphs array
   int numParagraphs = StringSplit(inputText, '\n', paragraphs); //--- Split text into paragraphs
   for (int p = 0; p < numParagraphs; p++) {        //--- Iterate through paragraphs
      string para = paragraphs[p];                  //--- Get current paragraph
      if (StringLen(para) == 0) {                   //--- Check empty paragraph
         int size = ArraySize(wrappedLines);        //--- Get current size
         ArrayResize(wrappedLines, size + 1);       //--- Resize lines array
         wrappedLines[size] = " ";                  //--- Add empty line
         continue;                                  //--- Skip to next
      }
      string words[];                               //--- Declare words array
      int numWords = StringSplit(para, ' ', words); //--- Split paragraph into words
      string currentLine = "";                      //--- Initialize current line
      for (int w = 0; w < numWords; w++) {          //--- Iterate through words
         string testLine = currentLine + (StringLen(currentLine) > 0 ? " " : "") + words[w]; //--- Build test line
         uint wid, hei;                             //--- Declare width and height
         TextGetSize(testLine, wid, hei);           //--- Get test line size
         int textWidth = (int)wid;                  //--- Get text width
         if (textWidth + offset <= maxWidth && StringLen(testLine) <= maxChars) { //--- Check line fits
            currentLine = testLine;                 //--- Update current line
         } else {                                   //--- Line exceeds limits
            if (StringLen(currentLine) > 0) {       //--- Check non-empty line
               int size = ArraySize(wrappedLines);  //--- Get current size
               ArrayResize(wrappedLines, size + 1); //--- Resize lines array
               wrappedLines[size] = currentLine;    //--- Add line
            }
            currentLine = words[w];                 //--- Start new line
            TextGetSize(currentLine, wid, hei);     //--- Get new line size
            textWidth = (int)wid;                   //--- Update text width
            if (textWidth + offset > maxWidth || StringLen(currentLine) > maxChars) { //--- Check word too long
               string wrappedWord = "";             //--- Initialize wrapped word
               for (int c = 0; c < StringLen(words[w]); c++) { //--- Iterate through characters
                  string testWord = wrappedWord + StringSubstr(words[w], c, 1); //--- Build test word
                  TextGetSize(testWord, wid, hei);  //--- Get test word size
                  int wordWidth = (int)wid;         //--- Get word width
                  if (wordWidth + offset > maxWidth || StringLen(testWord) > maxChars) { //--- Check word fits
                     if (StringLen(wrappedWord) > 0) {       //--- Check non-empty word
                        int size = ArraySize(wrappedLines);  //--- Get current size
                        ArrayResize(wrappedLines, size + 1); //--- Resize lines array
                        wrappedLines[size] = wrappedWord;    //--- Add wrapped word
                     }
                     wrappedWord = StringSubstr(words[w], c, 1); //--- Start new word
                  } else {                          //--- Word fits
                     wrappedWord = testWord;        //--- Update wrapped word
                  }
               }
               currentLine = wrappedWord;           //--- Set current line to wrapped word
            }
         }
      }
      if (StringLen(currentLine) > 0) {             //--- Check remaining line
         int size = ArraySize(wrappedLines);        //--- Get current size
         ArrayResize(wrappedLines, size + 1);       //--- Resize lines array
         wrappedLines[size] = currentLine;          //--- Add line
      }
   }
}

Мы уже встречались с этой функцией, поэтому рассмотрим ее еще раз и выделим наиболее важную логику обновления. В функции WrapText задаем максимальное число символов в строке maxChars, равное 60, и очищаем выходной массив wrappedLines с помощью ArrayResize. С помощью TextSetFont задаем шрифт и его размер, а с помощью StringSplit разбиваем входной текст на абзацы по символу переноса строки. Для пустого абзаца добавляем пробел в wrappedLines и переходим к следующему абзацу.

Непустые абзацы разбиваем на слова с помощью StringSplit и формируем строки, добавляя слова, если они помещаются в maxWidth с учетом offset и ограничения на число символов. Ширину проверяем с помощью TextGetSize. Если строка превышает ограничения, добавляем ее в wrappedLines и начинаем новую строку с текущего слова. Слишком длинные слова разбиваем посимвольно: при превышении ограничения по ширине или числу символов добавляем сформированные фрагменты в новые строки. Каждый фрагмент сохраняется в wrappedLines. Оставшаяся строка добавляется в выходной массив. При инициализации нужно задать цвета элементов, а при завершении работы программы – удалить созданные элементы.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   button_darker_bg = DarkenColor(button_original_bg);     //--- Set darker button background
   clear_darker_bg = DarkenColor(clear_original_bg);       //--- Set darker clear button background
   new_chat_darker_bg = DarkenColor(new_chat_original_bg); //--- Set darker new chat button background
   logFileHandle = FileOpen(LogFileName, FILE_READ | FILE_WRITE | FILE_TXT); //--- Open log file
   if (logFileHandle == INVALID_HANDLE) {                  //--- Check file open failure
      Print("Failed to open log file: ", GetLastError());  //--- Log failure
      return(INIT_FAILED);                                 //--- Return initialization failure
   }
   FileSeek(logFileHandle, 0, SEEK_END);                   //--- Move to end of log file
   FileWriteString(logFileHandle, "EA Initialized at " + TimeToString(TimeCurrent()) + "\n"); //--- Log initialization
   CreateDashboard();                                      //--- Create dashboard UI
   UpdateResponseDisplay();                                //--- Update response display
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);       //--- Enable mouse move events
   ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, true);      //--- Enable mouse wheel events
   ChartSetInteger(0, CHART_MOUSE_SCROLL, true);           //--- Enable chart scrolling
   return(INIT_SUCCEEDED);                                 //--- Return initialization success
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   ObjectsDeleteAll(0, "ChatGPT_");                        //--- Remove all ChatGPT objects
   DeleteScrollbar();                                      //--- Delete scrollbar elements
   if (logFileHandle != INVALID_HANDLE) {                  //--- Check if log file open
      FileClose(logFileHandle);                            //--- Close log file
   }
}

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

//+------------------------------------------------------------------+
//| Create dashboard UI                                              |
//+------------------------------------------------------------------+
void CreateDashboard() {
   g_mainHeight = g_headerHeight + 2 * g_padding + g_displayHeight + g_footerHeight; //--- Calculate main height
   int displayX = g_mainX + g_sidePadding;          //--- Calculate display x
   int displayY = g_mainY + g_headerHeight + g_padding; //--- Calculate display y
   int displayW = g_mainWidth - 2 * g_sidePadding;  //--- Calculate display width
   int footerY = displayY + g_displayHeight + g_padding; //--- Calculate footer y
   int inputWidth = 448;                            //--- Set input field width
   int sendWidth = 80;                              //--- Set send button width
   int gap = 10;                                    //--- Set gap between elements
   int totalW = inputWidth + gap + sendWidth;       //--- Calculate total width
   int centerX = g_mainX + (g_mainWidth - totalW) / 2; //--- Calculate center x
   int inputX = centerX;                            //--- Set input field x
   int sendX = inputX + inputWidth + gap;           //--- Calculate send button x
   int elemHeight = 36;                             //--- Set element height
   int elemY = footerY + 8;                         //--- Calculate element y
   createRecLabel("ChatGPT_MainContainer", g_mainX, g_mainY, g_mainWidth, g_mainHeight, clrWhite, 1, clrLightGray); //--- Create main container
   createRecLabel("ChatGPT_HeaderBg", g_mainX, g_mainY, g_mainWidth, g_headerHeight, clrWhiteSmoke, 0, clrNONE); //--- Create header background
   string title = "ChatGPT AI EA";                  //--- Set title
   string titleFont = "Arial Rounded MT Bold";      //--- Set title font
   int titleSize = 14;                              //--- Set title font size
   TextSetFont(titleFont, titleSize);               //--- Set title font
   uint titleWid, titleHei;                         //--- Declare title dimensions
   TextGetSize(title, titleWid, titleHei);          //--- Get title size
   int titleY = g_mainY + (g_headerHeight - (int)titleHei) / 2 - 4; //--- Calculate title y
   int titleX = g_mainX + g_sidePadding;            //--- Set title x
   createLabel("ChatGPT_TitleLabel", titleX, titleY, title, clrDarkSlateGray, titleSize, titleFont, CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER); //--- Create title label
   string dateStr = TimeToString(TimeTradeServer(), TIME_DATE|TIME_MINUTES); //--- Get current date
   string dateFont = "Arial";                       //--- Set date font
   int dateSize = 12;                               //--- Set date font size
   TextSetFont(dateFont, dateSize);                 //--- Set date font
   uint dateWid, dateHei;                           //--- Declare date dimensions
   TextGetSize(dateStr, dateWid, dateHei);          //--- Get date size
   int dateX = g_mainX + g_mainWidth / 2 - (int)(dateWid / 2) - 50; //--- Calculate date x
   int dateY = g_mainY + (g_headerHeight - (int)dateHei) / 2 - 4; //--- Calculate date y
   createLabel("ChatGPT_DateLabel", dateX, dateY, dateStr, clrSlateGray, dateSize, dateFont, CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER); //--- Create date label
   int clearWidth = 100;                            //--- Set clear button width
   int clearX = g_mainX + g_mainWidth - clearWidth - g_sidePadding; //--- Calculate clear button x
   int clearY = g_mainY + 4;                        //--- Set clear button y
   createButton("ChatGPT_ClearButton", clearX, clearY, clearWidth, g_headerHeight - 8, "Clear", clrWhite, 11, clear_original_bg, clrIndianRed); //--- Create clear button
   int new_chat_width = 100;                        //--- Set new chat button width
   int new_chat_x = clearX - new_chat_width - g_sidePadding; //--- Calculate new chat button x
   createButton("ChatGPT_NewChatButton", new_chat_x, clearY, new_chat_width, g_headerHeight - 8, "New Chat", clrWhite, 11, new_chat_original_bg, clrRoyalBlue); //--- Create new chat button
   createRecLabel("ChatGPT_ResponseBg", displayX, displayY, displayW, g_displayHeight, clrWhite, 1, clrGainsboro, BORDER_FLAT, STYLE_SOLID); //--- Create response background
   createRecLabel("ChatGPT_FooterBg", g_mainX, footerY, g_mainWidth, g_footerHeight, clrGainsboro, 0, clrNONE); //--- Create footer background
   createEdit("ChatGPT_InputEdit", inputX, elemY, inputWidth, elemHeight, "", clrBlack, 11, clrWhite, clrSilver); //--- Create input field
   createButton("ChatGPT_SubmitButton", sendX, elemY, sendWidth, elemHeight, "Send", clrWhite, 11, button_original_bg, clrDarkBlue); //--- Create send button
   ChartRedraw();                                   //--- Redraw chart
}

В основной функции компоновки панели вычисляем высоту главного контейнера g_mainHeight как сумму g_headerHeight, g_displayHeight, g_footerHeight и удвоенного g_padding. С учетом отступов определяем координаты области отображения displayX и displayY, а также координату нижней области footerY, чтобы панель была динамической, а не статичной, как в предыдущей версии. С помощью createRecLabel создаем главный контейнер ChatGPT_MainContainer белого цвета и фон заголовка ChatGPT_HeaderBg серого цвета. Затем добавляем заголовок ChatGPT_TitleLabel с текстом "ChatGPT AI EA", шрифтом Arial Rounded MT Bold и размером 14; для его выравнивания используем TextGetSize. Создаем метку даты ChatGPT_DateLabel с текущим серверным временем, полученным через TimeTradeServer. Для нее задаем шрифт Arial размером 12 и выравнивание по центру.

В заголовок добавляем кнопки Clear (ChatGPT_ClearButton) и New Chat (ChatGPT_NewChatButton) с помощью createButton. Для них используем разные цвета – clrLightCoral и clrLightBlue – и размер шрифта 11. С помощью createRecLabel создаем область отображения чата ChatGPT_ResponseBg и нижнюю область ChatGPT_FooterBg для поля ввода. В нижней области по центру размещаем поле ввода ChatGPT_InputEdit шириной 448 пикселей и кнопку Send (ChatGPT_SubmitButton) шириной 80 пикселей. Для этого используем createEdit и createButton, задавая между элементами промежуток 10 пикселей. Наконец, перерисовываем график с помощью функции ChartRedraw. После компиляции получаем следующий результат.

FINAL UI WITH ELEMENTS

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

//+------------------------------------------------------------------+
//| Check if string is a timestamp in HH:MM format                   |
//+------------------------------------------------------------------+
bool IsTimestamp(string line) {
   StringTrimLeft(line);                                 //--- Trim left whitespace
   StringTrimRight(line);                                //--- Trim right whitespace
   if (StringLen(line) != 5) return false;               //--- Check length
   if (StringGetCharacter(line, 2) != ':') return false; //--- Check colon
   string hh = StringSubstr(line, 0, 2);                 //--- Extract hours
   string mm = StringSubstr(line, 3, 2);                 //--- Extract minutes
   int h = (int)StringToInteger(hh);                     //--- Convert hours to integer
   int m = (int)StringToInteger(mm);                     //--- Convert minutes to integer
   if (h < 0 || h > 23 || m < 0 || m > 59) return false; //--- Validate time
   return true;                                          //--- Confirm valid timestamp
}

//+------------------------------------------------------------------+
//| Compute lines and height for messages                            |
//+------------------------------------------------------------------+
void ComputeLinesAndHeight(const string &font, const int fontSize, const int timestampFontSize,
                           const int adjustedLineHeight, const int adjustedTimestampHeight,
                           const int messageMargin, const int maxTextWidth,
                           const string &msgRoles[], const string &msgContents[], const string &msgTimestamps[],
                           const int numMessages, int &totalHeight_out, int &totalLines_out,
                           string &allLines_out[], string &lineRoles_out[], int &lineHeights_out[]) {
   ArrayResize(allLines_out, 0);                    //--- Clear lines array
   ArrayResize(lineRoles_out, 0);                   //--- Clear roles array
   ArrayResize(lineHeights_out, 0);                 //--- Clear heights array
   totalLines_out = 0;                              //--- Initialize total lines
   totalHeight_out = 0;                             //--- Initialize total height
   for (int m = 0; m < numMessages; m++) {          //--- Iterate through messages
      string wrappedLines[];                        //--- Declare wrapped lines
      WrapText(msgContents[m], font, fontSize, maxTextWidth, wrappedLines); //--- Wrap message content
      int numLines = ArraySize(wrappedLines);       //--- Get number of lines
      int currSize = ArraySize(allLines_out);       //--- Get current size
      ArrayResize(allLines_out, currSize + numLines + 1); //--- Resize lines array
      ArrayResize(lineRoles_out, currSize + numLines + 1); //--- Resize roles array
      ArrayResize(lineHeights_out, currSize + numLines + 1); //--- Resize heights array
      for (int l = 0; l < numLines; l++) {          //--- Iterate through wrapped lines
         allLines_out[currSize + l] = wrappedLines[l]; //--- Add line
         lineRoles_out[currSize + l] = msgRoles[m]; //--- Add role
         lineHeights_out[currSize + l] = adjustedLineHeight; //--- Set line height
         totalHeight_out += adjustedLineHeight;     //--- Update total height
      }
      allLines_out[currSize + numLines] = msgTimestamps[m]; //--- Add timestamp
      lineRoles_out[currSize + numLines] = msgRoles[m] + "_timestamp"; //--- Add timestamp role
      lineHeights_out[currSize + numLines] = adjustedTimestampHeight; //--- Set timestamp height
      totalHeight_out += adjustedTimestampHeight;   //--- Update total height
      totalLines_out += numLines + 1;               //--- Update total lines
      if (m < numMessages - 1) {                    //--- Check for margin
         totalHeight_out += messageMargin;          //--- Add message margin
      }
   }
}

Здесь реализуем вспомогательные функции для проверки отметок времени и вычисления параметров отображения сообщений. В функции IsTimestamp удаляем начальные и конечные пробельные символы из входной строки с помощью StringTrimLeft и StringTrimRight. Затем проверяем, что строка содержит ровно 5 символов, а на позиции 2 находится двоеточие – с помощью StringGetCharacter. С помощью StringSubstr извлекаем часы и минуты, преобразуем их в целые числа через StringToInteger и возвращаем true, если часы находятся в диапазоне 0-23, а минуты – 0-59. Это обеспечивает точное распознавание отметок времени в истории диалога. При другом подходе потребуется определить собственные правила.

В функции ComputeLinesAndHeight очищаем выходные массивы allLines_out, lineRoles_out и lineHeights_out с помощью ArrayResize и присваиваем totalLines_out и totalHeight_out значение 0. Для каждого сообщения переносим текст с помощью WrapText, используя заданные шрифт, размер шрифта и максимальную ширину. Затем изменяем размер выходных массивов, чтобы разместить строки после переноса и отметку времени. Текст строки, роль User или AI и высоту записываем соответственно в allLines_out, lineRoles_out и lineHeights_out: для текста используется adjustedLineHeight, а для отметки времени – adjustedTimestampHeight. После этого обновляем totalHeight_out и totalLines_out. Между сообщениями, кроме последнего, добавляем отступ messageMargin для визуального разделения. Одновременно проверяем отметки времени и подготавливаем текст сообщений к отображению в прокручиваемом интерфейсе чата.

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

//+------------------------------------------------------------------+
//| Update response display with scrolling                           |
//+------------------------------------------------------------------+
void UpdateResponseDisplay() {
   int total = ObjectsTotal(0, 0, -1);              //--- Get total objects
   for (int j = total - 1; j >= 0; j--) {           //--- Iterate through objects
      string name = ObjectName(0, j, 0, -1);        //--- Get object name
      if (StringFind(name, "ChatGPT_ResponseLine_") == 0 ||
          StringFind(name, "ChatGPT_MessageBg_") == 0 ||
          StringFind(name, "ChatGPT_MessageText_") == 0 ||
          StringFind(name, "ChatGPT_Timestamp_") == 0) { //--- Check for message objects
         ObjectDelete(0, name);                     //--- Delete object
      }
   }
   string displayText = conversationHistory;        //--- Get conversation history
   int textX = g_mainX + g_sidePadding + g_textPadding; //--- Calculate text x position
   int textY = g_mainY + g_headerHeight + g_padding + g_textPadding; //--- Calculate text y position
   int fullMaxWidth = g_mainWidth - 2 * g_sidePadding - 2 * g_textPadding; //--- Calculate max text width
   if (displayText == "") {                         //--- Check empty history
      string objName = "ChatGPT_ResponseLine_0";    //--- Set default label name
      createLabel(objName, textX, textY, "Type your message below and click Send to chat with the AI.", clrGray, 10, "Arial", CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER); //--- Create default label
      g_total_height = 0;                           //--- Reset total height
      g_visible_height = g_displayHeight - 2 * g_textPadding; //--- Set visible height
      if (scroll_visible) {                         //--- Check scrollbar visible
         DeleteScrollbar();                         //--- Delete scrollbar
         scroll_visible = false;                    //--- Reset scrollbar visibility
      }
      ChartRedraw();                                //--- Redraw chart
      return;                                       //--- Exit function
   }
   string parts[];                                  //--- Declare parts array
   int numParts = StringSplit(displayText, '\n', parts); //--- Split history into parts
   string msgRoles[];                               //--- Declare roles array
   string msgContents[];                            //--- Declare contents array
   string msgTimestamps[];                          //--- Declare timestamps array
   string currentRole = "";                         //--- Initialize current role
   string currentContent = "";                      //--- Initialize current content
   string currentTimestamp = "";                    //--- Initialize current timestamp
   for (int p = 0; p < numParts; p++) {             //--- Iterate through parts
      string line = parts[p];                       //--- Get current line
      StringTrimLeft(line);                         //--- Trim left whitespace
      StringTrimRight(line);                        //--- Trim right whitespace
      if (StringLen(line) == 0) {                   //--- Check empty line
         if (currentRole != "") currentContent += "\n"; //--- Append newline
         continue;                                  //--- Skip to next
      }
      if (StringFind(line, "You: ") == 0) {         //--- Check user message
         if (currentRole != "") {                   //--- Check existing message
            int size = ArraySize(msgRoles);         //--- Get current size
            ArrayResize(msgRoles, size + 1);        //--- Resize roles array
            ArrayResize(msgContents, size + 1);     //--- Resize contents array
            ArrayResize(msgTimestamps, size + 1);   //--- Resize timestamps array
            msgRoles[size] = currentRole;           //--- Add role
            msgContents[size] = currentContent;     //--- Add content
            msgTimestamps[size] = currentTimestamp; //--- Add timestamp
         }
         currentRole = "User";                      //--- Set role to User
         currentContent = StringSubstr(line, 5);    //--- Extract user content
         currentTimestamp = "";                     //--- Reset timestamp
         continue;                                  //--- Skip to next
      } else if (StringFind(line, "AI: ") == 0) {   //--- Check AI message
         if (currentRole != "") {                   //--- Check existing message
            int size = ArraySize(msgRoles);         //--- Get current size
            ArrayResize(msgRoles, size + 1);        //--- Resize roles array
            ArrayResize(msgContents, size + 1);     //--- Resize contents array
            ArrayResize(msgTimestamps, size + 1);   //--- Resize timestamps array
            msgRoles[size] = currentRole;           //--- Add role
            msgContents[size] = currentContent;     //--- Add content
            msgTimestamps[size] = currentTimestamp; //--- Add timestamp
         }
         currentRole = "AI";                        //--- Set role to AI
         currentContent = StringSubstr(line, 4);    //--- Extract AI content
         currentTimestamp = "";                     //--- Reset timestamp
         continue;                                  //--- Skip to next
      } else if (IsTimestamp(line)) {               //--- Check timestamp
         if (currentRole != "") {                   //--- Check existing message
            currentTimestamp = line;                //--- Set timestamp
            int size = ArraySize(msgRoles);         //--- Get current size
            ArrayResize(msgRoles, size + 1);        //--- Resize roles array
            ArrayResize(msgContents, size + 1);     //--- Resize contents array
            ArrayResize(msgTimestamps, size + 1);   //--- Resize timestamps array
            msgRoles[size] = currentRole;           //--- Add role
            msgContents[size] = currentContent;     //--- Add content
            msgTimestamps[size] = currentTimestamp; //--- Add timestamp
            currentRole = "";                       //--- Reset role
         }
      } else {                                      //--- Append to content
         if (currentRole != "") {                   //--- Check active message
            currentContent += "\n" + line;          //--- Append line
         }
      }
   }
   if (currentRole != "") {                         //--- Check final message
      int size = ArraySize(msgRoles);               //--- Get current size
      ArrayResize(msgRoles, size + 1);              //--- Resize roles array
      ArrayResize(msgContents, size + 1);           //--- Resize contents array
      ArrayResize(msgTimestamps, size + 1);         //--- Resize timestamps array
      msgRoles[size] = currentRole;                 //--- Add role
      msgContents[size] = currentContent;           //--- Add content
      msgTimestamps[size] = currentTimestamp;       //--- Add timestamp
   }
   int numMessages = ArraySize(msgRoles);           //--- Get number of messages
   if (numMessages == 0) {                          //--- Check no messages
      string objName = "ChatGPT_ResponseLine_0";    //--- Set default label name
      createLabel(objName, textX, textY, "Type your message below and click Send to chat with the AI.", clrGray, 10, "Arial", CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER); //--- Create default label
      g_total_height = 0;                           //--- Reset total height
      g_visible_height = g_displayHeight - 2 * g_textPadding; //--- Set visible height
      if (scroll_visible) {                         //--- Check scrollbar visible
         DeleteScrollbar();                         //--- Delete scrollbar
         scroll_visible = false;                    //--- Reset scrollbar visibility
      }
      ChartRedraw();                                //--- Redraw chart
      return;                                       //--- Exit function
   }
   string font = "Arial";                           //--- Set font
   int fontSize = 10;                               //--- Set font size
   int timestampFontSize = 8;                       //--- Set timestamp font size
   int lineHeight = TextGetHeight("A", font, fontSize); //--- Get line height
   int timestampHeight = TextGetHeight("A", font, timestampFontSize); //--- Get timestamp height
   int adjustedLineHeight = lineHeight + g_lineSpacing; //--- Calculate adjusted line height
   int adjustedTimestampHeight = timestampHeight + g_lineSpacing; //--- Calculate adjusted timestamp height
   int messageMargin = 12;                          //--- Set message margin
   int visibleHeight = g_displayHeight - 2 * g_textPadding; //--- Calculate visible height
   g_visible_height = visibleHeight;                //--- Set visible height
   string tentativeAllLines[];                      //--- Declare tentative lines
   string tentativeLineRoles[];                     //--- Declare tentative roles
   int tentativeLineHeights[];                      //--- Declare tentative heights
   int tentativeTotalHeight, tentativeTotalLines;   //--- Declare tentative totals
   ComputeLinesAndHeight(font, fontSize, timestampFontSize, adjustedLineHeight, adjustedTimestampHeight,
                         messageMargin, fullMaxWidth, msgRoles, msgContents, msgTimestamps, numMessages,
                         tentativeTotalHeight, tentativeTotalLines, tentativeAllLines, tentativeLineRoles, tentativeLineHeights); //--- Compute tentative lines
   bool need_scroll = tentativeTotalHeight > visibleHeight; //--- Check if scrolling needed
   bool should_show_scrollbar = false;              //--- Initialize scrollbar visibility
   int reserved_width = 0;                          //--- Initialize reserved width
   if (ScrollbarMode != SCROLL_WHEEL_ONLY) {        //--- Check scrollbar mode
      should_show_scrollbar = need_scroll && (ScrollbarMode == SCROLL_DYNAMIC_ALWAYS || (ScrollbarMode == SCROLL_DYNAMIC_HOVER && mouse_in_display)); //--- Determine scrollbar visibility
      if (should_show_scrollbar) {                  //--- Check if scrollbar needed
         reserved_width = 16;                       //--- Reserve scrollbar width
      }
   }
   string allLines[];                               //--- Declare final lines
   string lineRoles[];                              //--- Declare final roles
   int lineHeights[];                               //--- Declare final heights
   int totalHeight, totalLines;                     //--- Declare final totals
   int maxTextWidth = fullMaxWidth - reserved_width; //--- Calculate max text width
   if (reserved_width > 0) {                        //--- Check if scrollbar reserved
      ComputeLinesAndHeight(font, fontSize, timestampFontSize, adjustedLineHeight, adjustedTimestampHeight,
                            messageMargin, maxTextWidth, msgRoles, msgContents, msgTimestamps, numMessages,
                            totalHeight, totalLines, allLines, lineRoles, lineHeights); //--- Compute lines with reduced width
   } else {                                         //--- Use tentative values
      totalHeight = tentativeTotalHeight;           //--- Set total height
      totalLines = tentativeTotalLines;             //--- Set total lines
      ArrayCopy(allLines, tentativeAllLines);       //--- Copy lines
      ArrayCopy(lineRoles, tentativeLineRoles);     //--- Copy roles
      ArrayCopy(lineHeights, tentativeLineHeights); //--- Copy heights
   }
   FileWriteString(logFileHandle, "UpdateResponseDisplay: totalHeight=" + IntegerToString(totalHeight) + ", visibleHeight=" + IntegerToString(visibleHeight) + ", totalLines=" + IntegerToString(totalLines) + ", reserved_width=" + IntegerToString(reserved_width) + "\n"); //--- Log display update
   g_total_height = totalHeight;                    //--- Set total height
   bool prev_scroll_visible = scroll_visible;       //--- Store previous scrollbar state
   scroll_visible = should_show_scrollbar;          //--- Update scrollbar visibility
   if (scroll_visible != prev_scroll_visible) {     //--- Check scrollbar state change
      if (scroll_visible) {                         //--- Show scrollbar
         CreateScrollbar();                         //--- Create scrollbar
      } else {                                      //--- Hide scrollbar
         DeleteScrollbar();                         //--- Delete scrollbar
      }
   }
   int max_scroll = MathMax(0, totalHeight - visibleHeight); //--- Calculate max scroll
   if (scroll_pos > max_scroll) scroll_pos = max_scroll; //--- Clamp scroll position
   if (scroll_pos < 0) scroll_pos = 0;              //--- Ensure non-negative scroll
   if (totalHeight > visibleHeight && scroll_pos == prev_scroll_pos && prev_scroll_pos == -1) { //--- Check initial scroll
      scroll_pos = max_scroll;                      //--- Set to bottom
   }
   if (scroll_visible) {                            //--- Update scrollbar
      slider_height = CalculateSliderHeight();      //--- Calculate slider height
      ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height); //--- Set slider height
      UpdateSliderPosition();                       //--- Update slider position
      UpdateButtonColors();                         //--- Update button colors
   }
   int currentY = textY - scroll_pos;               //--- Calculate current y position
   int endY = textY + visibleHeight;                //--- Calculate end y position
   int startLineIndex = 0;                          //--- Initialize start line index
   int currentHeight = 0;                           //--- Initialize current height
   for (int line = 0; line < totalLines; line++) {  //--- Find start line
      if (currentHeight >= scroll_pos) {            //--- Check if at scroll position
         startLineIndex = line;                     //--- Set start line
         currentY = textY + (currentHeight - scroll_pos); //--- Set current y
         break;                                     //--- Exit loop
      }
      currentHeight += lineHeights[line];           //--- Add line height
      if (line < totalLines - 1 && StringFind(lineRoles[line], "_timestamp") >= 0 && StringFind(lineRoles[line + 1], "_timestamp") < 0) { //--- Check message gap
         currentHeight += messageMargin;            //--- Add message margin
      }
   }
   int numVisibleLines = 0;                         //--- Initialize visible lines
   int visibleHeightUsed = 0;                       //--- Initialize used height
   for (int line = startLineIndex; line < totalLines; line++) { //--- Count visible lines
      int lineHeight = lineHeights[line];           //--- Get line height
      if (visibleHeightUsed + lineHeight > visibleHeight) break; //--- Check height limit
      visibleHeightUsed += lineHeight;              //--- Add line height
      numVisibleLines++;                            //--- Increment visible lines
      if (line < totalLines - 1 && StringFind(lineRoles[line], "_timestamp") >= 0 && StringFind(lineRoles[line + 1], "_timestamp") < 0) { //--- Check message gap
         if (visibleHeightUsed + messageMargin > visibleHeight) break; //--- Check margin limit
         visibleHeightUsed += messageMargin;        //--- Add message margin
      }
   }
   FileWriteString(logFileHandle, "Visible lines: startLineIndex=" + IntegerToString(startLineIndex) + ", numVisibleLines=" + IntegerToString(numVisibleLines) + ", scroll_pos=" + IntegerToString(scroll_pos) + ", currentY=" + IntegerToString(currentY) + "\n"); //--- Log visible lines
   int leftX = g_mainX + g_sidePadding + g_textPadding; //--- Set left text x
   int rightX = g_mainX + g_mainWidth - g_sidePadding - g_textPadding - reserved_width; //--- Set right text x
   color userColor = clrGray;                       //--- Set user text color
   color aiColor = clrBlue;                         //--- Set AI text color
   color timestampColor = clrDarkGray;              //--- Set timestamp color
   for (int li = 0; li < numVisibleLines; li++) {   //--- Display visible lines
      int lineIndex = startLineIndex + li;          //--- Calculate line index
      if (lineIndex >= totalLines) break;           //--- Check valid index
      string line = allLines[lineIndex];            //--- Get line text
      string role = lineRoles[lineIndex];           //--- Get line role
      bool isTimestamp = StringFind(role, "_timestamp") >= 0; //--- Check if timestamp
      int currFontSize = isTimestamp ? timestampFontSize : fontSize; //--- Set font size
      color textCol = isTimestamp ? timestampColor : (StringFind(role, "User") >= 0 ? userColor : aiColor); //--- Set text color
      string display_line = line;                   //--- Set display line
      if (line == " ") {                            //--- Check empty line
         display_line = " ";                        //--- Set to space
         textCol = clrWhite;                        //--- Set to white
      }
      int textX_pos = (StringFind(role, "User") >= 0) ? rightX : leftX; //--- Set text x position
      ENUM_ANCHOR_POINT textAnchor = (StringFind(role, "User") >= 0) ? ANCHOR_RIGHT_UPPER : ANCHOR_LEFT_UPPER; //--- Set text anchor
      string lineName = "ChatGPT_MessageText_" + IntegerToString(lineIndex); //--- Generate line name
      if (currentY >= textY && currentY < endY) {   //--- Check if visible
         createLabel(lineName, textX_pos, currentY, display_line, textCol, currFontSize, font, CORNER_LEFT_UPPER, textAnchor); //--- Create label
      }
      currentY += lineHeights[lineIndex];           //--- Increment y position
      if (lineIndex < totalLines - 1 && StringFind(lineRoles[lineIndex], "_timestamp") >= 0 && StringFind(lineRoles[lineIndex + 1], "_timestamp") < 0) { //--- Check message gap
         currentY += messageMargin;                 //--- Add message margin
      }
   }
   ChartRedraw();                                   //--- Redraw chart
}

В функции UpdateResponseDisplay сначала удаляем существующие объекты сообщений с префиксами ChatGPT_ResponseLine_, ChatGPT_MessageBg_, ChatGPT_MessageText_ и ChatGPT_Timestamp_. Для этого используем ObjectsTotal и ObjectDelete, после чего обновляем область отображения. Если история диалога conversationHistory пуста, с помощью createLabel создаем метку с предложением ввести сообщение. Затем присваиваем g_total_height значение 0, а g_visible_height – значение высоты области отображения за вычетом отступов; если полоса прокрутки отображается, удаляем ее через DeleteScrollbar и перерисовываем график с помощью функции ChartRedraw. В противном случае разбиваем историю по символам переноса строки с помощью StringSplit. Строки разбираем в массивы msgRoles, msgContents и msgTimestamps: определяем префиксы "You: " и "AI: ", распознаем отметки времени через IsTimestamp, накапливаем текст из нескольких строк и сохраняем завершенные сообщения.

Вычисляем координаты текста textX и textY, а также максимальную ширину fullMaxWidth; задаем размер шрифта 10 для сообщений и 8 для отметок времени; высоту строк определяем с помощью TextGetHeight и g_lineSpacing. С помощью ComputeLinesAndHeight формируем предварительные массивы строк и их высот, проверяем необходимость прокрутки и определяем видимость полосы прокрутки на основе ScrollbarMode и mouse_in_display. Если полоса прокрутки отображается, резервируем для нее 16 пикселей. При необходимости повторно вычисляем строки с учетом измененной ширины, обновляем g_total_height и управляем видимостью полосы прокрутки через CreateScrollbar или DeleteScrollbar. Затем ограничиваем scroll_pos диапазоном от 0 до max_scroll и для новых сообщений устанавливаем его в нижнее положение.

На основе scroll_pos вычисляем начальную строку и координату Y, определяем видимые строки в пределах g_visible_height и отображаем каждую через createLabel. Сообщения ИИ выравниваем по левому краю и окрашиваем в clrBlue, сообщения пользователя – по правому краю и окрашиваем в clrGray, а отметки времени – в clrDarkGray; между сообщениями задаем отступ 12 пикселей. Наконец, записываем сведения об отображении в журнал через FileWriteString и выполняем перерисовку. В результате область отображения будет заполнена доступной историей диалога. Теперь нужно обеспечить отправку запроса при нажатии кнопки Send. Для упрощения дальнейшей доработки разобьем существующую функцию на несколько более компактных.

//+------------------------------------------------------------------+
//| Build messages array from history                                |
//+------------------------------------------------------------------+
string BuildMessagesFromHistory(string newPrompt) {
   string messages = "[";                          //--- Start JSON array
   string temp = conversationHistory;              //--- Copy conversation history
   while (StringLen(temp) > 0) {                   //--- Process history
      int you_pos = StringFind(temp, "You: ");     //--- Find user message
      if (you_pos != 0) break;                     //--- Exit if no user message
      temp = StringSubstr(temp, 5);                //--- Extract after "You: "
      int end_user = StringFind(temp, "\n");       //--- Find end of user message
      string user_content = StringSubstr(temp, 0, end_user); //--- Get user content
      temp = StringSubstr(temp, end_user + 1);     //--- Move past user message
      int end_ts1 = StringFind(temp, "\n");        //--- Find end of timestamp
      temp = StringSubstr(temp, end_ts1 + 1);      //--- Move past timestamp
      int ai_pos = StringFind(temp, "AI: ");       //--- Find AI message
      if (ai_pos != 0) break;                      //--- Exit if no AI message
      temp = StringSubstr(temp, 4);                //--- Extract after "AI: "
      int end_ai = StringFind(temp, "\n");         //--- Find end of AI message
      string ai_content = StringSubstr(temp, 0, end_ai); //--- Get AI content
      temp = StringSubstr(temp, end_ai + 1);       //--- Move past AI message
      int end_ts2 = StringFind(temp, "\n\n");      //--- Find end of conversation block
      temp = StringSubstr(temp, end_ts2 + 2);      //--- Move past block
      messages += "{\"role\":\"user\",\"content\":\"" + JsonEscape(user_content) + "\"},"; //--- Add user message
      messages += "{\"role\":\"assistant\",\"content\":\"" + JsonEscape(ai_content) + "\"},"; //--- Add AI message
   }
   messages += "{\"role\":\"user\",\"content\":\"" + JsonEscape(newPrompt) + "\"}]"; //--- Add new prompt
   return messages;                                //--- Return JSON messages
}

//+------------------------------------------------------------------+
//| Get ChatGPT response via API                                     |
//+------------------------------------------------------------------+
string GetChatGPTResponse(string prompt) {
   string messages = BuildMessagesFromHistory(prompt); //--- Build JSON messages
   string requestData = "{\"model\":\"" + OpenAI_Model + "\",\"messages\":" + messages + ",\"max_tokens\":" + IntegerToString(MaxResponseLength) + "}"; //--- Create request JSON
   FileWriteString(logFileHandle, "Request Data: " + requestData + "\n"); //--- Log request data
   char postData[];                                    //--- Declare post data array
   int dataLen = StringToCharArray(requestData, postData, 0, WHOLE_ARRAY, CP_UTF8); //--- Convert request to char array
   ArrayResize(postData, dataLen - 1);                 //--- Remove null terminator
   FileWriteString(logFileHandle, "Raw Post Data (Hex): " + LogCharArray(postData) + "\n"); //--- Log raw data
   string headers = "Authorization: Bearer " + OpenAI_API_Key + "\r\n" +
                    "Content-Type: application/json; charset=UTF-8\r\n" +
                    "Content-Length: " + IntegerToString(dataLen - 1) + "\r\n\r\n"; //--- Set request headers
   FileWriteString(logFileHandle, "Request Headers: " + headers + "\n"); //--- Log headers
   char result[];                                     //--- Declare result array
   string resultHeaders;                              //--- Declare result headers
   int res = WebRequest("POST", OpenAI_Endpoint, headers, 10000, postData, result, resultHeaders); //--- Send API request
   if (res != 200) {                                  //--- Check request failure
      string response = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8); //--- Convert result to string
      string errMsg = "API request failed: HTTP Code " + IntegerToString(res) + ", Error: " + IntegerToString(GetLastError()) + ", Response: " + response; //--- Create error message
      Print(errMsg);                                  //--- Print error
      FileWriteString(logFileHandle, errMsg + "\n");  //--- Log error
      FileWriteString(logFileHandle, "Raw Response Data (Hex): " + LogCharArray(result) + "\n"); //--- Log raw response
      return errMsg;                                  //--- Return error message
   }
   string response = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8); //--- Convert response to string
   FileWriteString(logFileHandle, "API Response: " + response + "\n"); //--- Log response
   JsonValue jsonObject;                              //--- Declare JSON object
   int index = 0;                                     //--- Initialize parse index
   char charArray[];                                  //--- Declare char array
   int arrayLength = StringToCharArray(response, charArray, 0, WHOLE_ARRAY, CP_UTF8); //--- Convert response to char array
   if (!jsonObject.DeserializeFromArray(charArray, arrayLength, index)) { //--- Parse JSON
      string errMsg = "Error: Failed to parse API response JSON: " + response; //--- Create error message
      Print(errMsg);                                  //--- Print error
      FileWriteString(logFileHandle, errMsg + "\n");  //--- Log error
      return errMsg;                                  //--- Return error message
   }
   JsonValue *error = jsonObject.FindChildByKey("error"); //--- Check for error
   if (error != NULL) {                               //--- Check error exists
      string errMsg = "API Error: " + error["message"].ToString(); //--- Get error message
      Print(errMsg);                                  //--- Print error
      FileWriteString(logFileHandle, errMsg + "\n");  //--- Log error
      return errMsg;                                  //--- Return error message
   }
   string content = jsonObject["choices"][0]["message"]["content"].ToString(); //--- Extract response content
   if (StringLen(content) > 0) {                      //--- Check non-empty content
      StringReplace(content, "\\n", "\n");            //--- Replace escaped newlines
      StringTrimLeft(content);                        //--- Trim left whitespace
      StringTrimRight(content);                       //--- Trim right whitespace
      return content;                                 //--- Return content
   }
   string errMsg = "Error: No content in API response: " + response; //--- Create error message
   Print(errMsg);                                     //--- Print error
   FileWriteString(logFileHandle, errMsg + "\n");     //--- Log error
   return errMsg;                                     //--- Return error message
}

//+------------------------------------------------------------------+
//| Submit user message to ChatGPT                                   |
//+------------------------------------------------------------------+
void SubmitMessage() {
   string prompt = (string)ObjectGetString(0, "ChatGPT_InputEdit", OBJPROP_TEXT); //--- Get user input
   if (StringLen(prompt) > 0) {                      //--- Check non-empty input
      string response = GetChatGPTResponse(prompt);  //--- Get AI response
      Print("User: " + prompt);                      //--- Log user prompt
      Print("AI: " + response);                      //--- Log AI response
      string timestamp = TimeToString(TimeCurrent(), TIME_MINUTES); //--- Get current timestamp
      conversationHistory += "You: " + prompt + "\n" + timestamp + "\nAI: " + response + "\n" + timestamp + "\n\n"; //--- Append to history
      ObjectSetString(0, "ChatGPT_InputEdit", OBJPROP_TEXT, ""); //--- Clear input field
      UpdateResponseDisplay();                       //--- Update display with new content
      scroll_pos = MathMax(0, g_total_height - g_visible_height); //--- Scroll to bottom
      UpdateResponseDisplay();                       //--- Redraw display
      if (scroll_visible) {                          //--- Check scrollbar visible
         UpdateSliderPosition();                     //--- Update slider position
         UpdateButtonColors();                       //--- Update button colors
      }
      FileWriteString(logFileHandle, "Prompt: " + prompt + " | Response: " + response + " | Time: " + timestamp + "\n"); //--- Log interaction
      ChartRedraw();                                 //--- Redraw chart
   }
}

Здесь реализуем основные функции взаимодействия с API и обработки сообщений. Сначала определяем функцию BuildMessagesFromHistory, которая формирует массив JSON для запросов к API на основе conversationHistory. Она перебирает историю, извлекает сообщения пользователя с префиксом "You: " и сообщения ИИ с префиксом "AI: " с помощью StringFind и StringSubstr, пропускает отметки времени и пустые строки. С помощью JsonEscape содержимое преобразуется в объекты JSON с ролями "user" и "assistant"; в конце добавляется новый запрос пользователя. В результате формируется корректный массив для многоходовых диалогов.

В функции GetChatGPTResponse формируем JSON-запрос с помощью BuildMessagesFromHistory, OpenAI_Model и MaxResponseLength, преобразуем его в массив char через StringToCharArray, задаем заголовки с API-ключом OpenAI_API_Key и отправляем POST-запрос на OpenAI_Endpoint с помощью WebRequest. Обрабатывая ответ, сначала проверяем статус ответа HTTP: если он отличен от 200, записываем необработанные данные и ошибки в logFileHandle через FileWriteString. Затем разбираем ответ JSON с помощью JsonValue::DeserializeFromArray, проверяем ошибки API и извлекаем содержимое из choices[0][message][content]. Перед возвратом результата убираем экранирование символов переноса строки и удаляем лишние пробельные символы, как и в предыдущей версии.

В функции SubmitMessage получаем пользовательский ввод из ChatGPT_InputEdit через ObjectGetString. Если строка не пуста, вызываем GetChatGPTResponse, записываем запрос и ответ через Print, добавляем их в conversationHistory с отметками времени, полученными через TimeCurrent, и очищаем поле ввода с помощью ObjectSetString. Затем обновляем область отображения через UpdateResponseDisplay, прокручиваем ее вниз, задавая scroll_pos, и при необходимости обновляем визуальное представление полосы прокрутки. Взаимодействие также записывается в журнал. В результате получается система для управления диалогами с ИИ, взаимодействия с API и динамического обновления интерфейса чата. Вызвать эту функцию при нажатии кнопки Send можно следующим образом.

//+------------------------------------------------------------------+
//| Chart event handler for ChatGPT UI                               |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {
   if (id == CHARTEVENT_OBJECT_CLICK && sparam == "ChatGPT_SubmitButton") { //--- Handle submit button click
      SubmitMessage();                              //--- Submit user message
   }
}

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

OUR FIRST PROMPT

На изображении видно, что диалог стал длиннее, а интерфейс – интуитивнее: сообщения пользователя отображаются справа, ответы ИИ – слева, и у каждого сообщения есть отметка времени. Теперь остается сделать область отображения интерактивной, чтобы обеспечить перетаскивание полосы прокрутки и состояния наведения для добавленных кнопок. Ниже приведена полная логика, реализующая это поведение.

//+------------------------------------------------------------------+
//| Chart event handler for ChatGPT UI                               |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {
   int displayX = g_mainX + g_sidePadding;          //--- Calculate display x position
   int displayY = g_mainY + g_headerHeight + g_padding; //--- Calculate display y position
   int displayW = g_mainWidth - 2 * g_sidePadding;  //--- Calculate display width
   int displayH = g_displayHeight;                  //--- Set display height
   int clearX = g_mainX + g_mainWidth - 100 - g_sidePadding; //--- Calculate clear button x
   int clearY = g_mainY + 4;                        //--- Set clear button y
   int clearW = 100;                                //--- Set clear button width
   int clearH = g_headerHeight - 8;                 //--- Calculate clear button height
   int new_chat_x = clearX - 100 - g_sidePadding;   //--- Calculate new chat button x
   int new_chat_w = 100;                            //--- Set new chat button width
   int new_chat_h = clearH;                         //--- Set new chat button height
   int sendX = g_mainX + (g_mainWidth - 448 - 10 - 80) / 2 + 448 + 10; //--- Calculate send button x
   int sendY = g_mainY + g_headerHeight + g_padding + g_displayHeight + g_padding; //--- Calculate send button y
   int sendW = 80;                                  //--- Set send button width
   int sendH = g_footerHeight;                      //--- Set send button height
   bool need_scroll = g_total_height > g_visible_height; //--- Check if scrolling needed
   if (id == CHARTEVENT_OBJECT_CLICK && sparam == "ChatGPT_SubmitButton") { //--- Handle submit button click
      SubmitMessage();                              //--- Submit user message
   } else if (id == CHARTEVENT_OBJECT_CLICK && sparam == "ChatGPT_ClearButton") { //--- Handle clear button click
      conversationHistory = "";                     //--- Clear conversation history
      scroll_pos = 0;                               //--- Reset scroll position
      prev_scroll_pos = -1;                         //--- Reset previous scroll position
      UpdateResponseDisplay();                      //--- Update response display
      ObjectSetString(0, "ChatGPT_InputEdit", OBJPROP_TEXT, ""); //--- Clear input field
      ChartRedraw();                                //--- Redraw chart
   } else if (id == CHARTEVENT_OBJECT_CLICK && sparam == "ChatGPT_NewChatButton") { //--- Handle new chat button click
      conversationHistory = "";                     //--- Clear conversation history
      scroll_pos = 0;                               //--- Reset scroll position
      prev_scroll_pos = -1;                         //--- Reset previous scroll position
      UpdateResponseDisplay();                      //--- Update response display
      ObjectSetString(0, "ChatGPT_InputEdit", OBJPROP_TEXT, ""); //--- Clear input field
      ChartRedraw();                                //--- Redraw chart
   } else if (id == CHARTEVENT_OBJECT_CLICK && (sparam == SCROLL_UP_REC || sparam == SCROLL_UP_LABEL)) { //--- Handle scroll up click
      ScrollUp();                                   //--- Scroll up
   } else if (id == CHARTEVENT_OBJECT_CLICK && (sparam == SCROLL_DOWN_REC || sparam == SCROLL_DOWN_LABEL)) { //--- Handle scroll down click
      ScrollDown();                                 //--- Scroll down
   } else if (id == CHARTEVENT_MOUSE_MOVE) {        //--- Handle mouse move events
      int mouseX = (int)lparam;                     //--- Get mouse x coordinate
      int mouseY = (int)dparam;                     //--- Get mouse y coordinate
      bool isOverSend = (mouseX >= sendX && mouseX <= sendX + sendW && mouseY >= sendY && mouseY <= sendY + sendH); //--- Check send button hover
      if (isOverSend && !button_hover) {            //--- Check send button hover start
         ObjectSetInteger(0, "ChatGPT_SubmitButton", OBJPROP_BGCOLOR, button_darker_bg); //--- Set hover background
         button_hover = true;                       //--- Set hover flag
         ChartRedraw();                             //--- Redraw chart
      } else if (!isOverSend && button_hover) {     //--- Check send button hover end
         ObjectSetInteger(0, "ChatGPT_SubmitButton", OBJPROP_BGCOLOR, button_original_bg); //--- Reset background
         button_hover = false;                      //--- Reset hover flag
         ChartRedraw();                             //--- Redraw chart
      }
      bool isOverClear = (mouseX >= clearX && mouseX <= clearX + clearW && mouseY >= clearY && mouseY <= clearY + clearH); //--- Check clear button hover
      if (isOverClear && !clear_hover) {            //--- Check clear button hover start
         ObjectSetInteger(0, "ChatGPT_ClearButton", OBJPROP_BGCOLOR, clear_darker_bg); //--- Set hover background
         clear_hover = true;                        //--- Set hover flag
         ChartRedraw();                             //--- Redraw chart
      } else if (!isOverClear && clear_hover) {     //--- Check clear button hover end
         ObjectSetInteger(0, "ChatGPT_ClearButton", OBJPROP_BGCOLOR, clear_original_bg); //--- Reset background
         clear_hover = false;                       //--- Reset hover flag
         ChartRedraw();                             //--- Redraw chart
      }
      bool isOverNewChat = (mouseX >= new_chat_x && mouseX <= new_chat_x + new_chat_w && mouseY >= clearY && mouseY <= clearY + new_chat_h); //--- Check new chat button hover
      if (isOverNewChat && !new_chat_hover) {       //--- Check new chat button hover start
         ObjectSetInteger(0, "ChatGPT_NewChatButton", OBJPROP_BGCOLOR, new_chat_darker_bg); //--- Set hover background
         new_chat_hover = true;                     //--- Set hover flag
         ChartRedraw();                             //--- Redraw chart
      } else if (!isOverNewChat && new_chat_hover) { //--- Check new chat button hover end
         ObjectSetInteger(0, "ChatGPT_NewChatButton", OBJPROP_BGCOLOR, new_chat_original_bg); //--- Reset background
         new_chat_hover = false;                    //--- Reset hover flag
         ChartRedraw();                             //--- Redraw chart
      }
      bool is_in = (mouseX >= displayX && mouseX <= displayX + displayW &&
                    mouseY >= displayY && mouseY <= displayY + displayH); //--- Check if mouse in display
      if (is_in != mouse_in_display) {              //--- Check display hover change
         mouse_in_display = is_in;                  //--- Update display hover status
         ChartSetInteger(0, CHART_MOUSE_SCROLL, !(mouse_in_display && need_scroll)); //--- Update chart scroll
         if (ScrollbarMode == SCROLL_DYNAMIC_HOVER) { //--- Check dynamic hover mode
            UpdateResponseDisplay();                //--- Update response display
         }
      }
      static int prevMouseState = 0;                //--- Store previous mouse state
      int MouseState = (int)sparam;                 //--- Get current mouse state
      if (prevMouseState == 0 && MouseState == 1 && scroll_visible) { //--- Check slider drag start
         int scrollbar_x = displayX + displayW - 16; //--- Calculate scrollbar x
         int xd_slider = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XDISTANCE); //--- Get slider x
         int yd_slider = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE); //--- Get slider y
         int xs_slider = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XSIZE); //--- Get slider width
         int ys_slider = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE); //--- Get slider height
         if (mouseX >= xd_slider && mouseX <= xd_slider + xs_slider &&
             mouseY >= yd_slider && mouseY <= yd_slider + ys_slider) { //--- Check slider click
            movingStateSlider = true;              //--- Set drag state
            mlbDownX_Slider = mouseX;              //--- Store mouse x
            mlbDownY_Slider = mouseY;              //--- Store mouse y
            mlbDown_YD_Slider = yd_slider;         //--- Store slider y
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrDimGray); //--- Set drag color
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height); //--- Set slider height
            ChartSetInteger(0, CHART_MOUSE_SCROLL, false); //--- Disable chart scroll
            FileWriteString(logFileHandle, "Slider drag started: x=" + IntegerToString(mouseX) + ", y=" + IntegerToString(mouseY) + "\n"); //--- Log drag start
         }
      }
      if (movingStateSlider) {                     //--- Handle slider drag
         int delta_y = mouseY - mlbDownY_Slider;   //--- Calculate y displacement
         int new_y = mlbDown_YD_Slider + delta_y;  //--- Calculate new y position
         int scroll_area_y_min = (g_mainY + g_headerHeight + g_padding) + 16; //--- Set min slider y
         int scroll_area_y_max = (g_mainY + g_headerHeight + g_padding + g_displayHeight - 16 - slider_height); //--- Set max slider y
         new_y = MathMax(scroll_area_y_min, MathMin(new_y, scroll_area_y_max)); //--- Clamp y position
         ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE, new_y); //--- Update slider y
         int max_scroll = MathMax(0, g_total_height - g_visible_height); //--- Calculate max scroll
         double scroll_ratio = (double)(new_y - scroll_area_y_min) / (scroll_area_y_max - scroll_area_y_min); //--- Calculate scroll ratio
         int new_scroll_pos = (int)MathRound(scroll_ratio * max_scroll); //--- Calculate new scroll position
         if (new_scroll_pos != scroll_pos) {       //--- Check if scroll changed
            scroll_pos = new_scroll_pos;           //--- Update scroll position
            UpdateResponseDisplay();               //--- Update response display
            if (scroll_visible) {                  //--- Check scrollbar visible
               UpdateSliderPosition();             //--- Update slider position
               UpdateButtonColors();               //--- Update button colors
            }
            FileWriteString(logFileHandle, "Slider dragged: new_scroll_pos=" + IntegerToString(new_scroll_pos) + "\n"); //--- Log drag
         }
         ChartRedraw();                            //--- Redraw chart
      }
      if (MouseState == 0) {                       //--- Handle mouse release
         if (movingStateSlider) {                  //--- Check if dragging
            movingStateSlider = false;             //--- Reset drag state
            if (scroll_visible) {                  //--- Check scrollbar visible
               ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrGray); //--- Reset slider color
            }
            ChartSetInteger(0, CHART_MOUSE_SCROLL, !(mouse_in_display && need_scroll)); //--- Restore chart scroll
            FileWriteString(logFileHandle, "Slider drag ended\n"); //--- Log drag end
         }
      }
      prevMouseState = MouseState;                   //--- Update previous mouse state
      static bool prevMouseInsideScrollUp = false;   //--- Track previous scroll up hover
      static bool prevMouseInsideScrollDown = false; //--- Track previous scroll down hover
      static bool prevMouseInsideSlider = false;     //--- Track previous slider hover
      if (scroll_visible) {                          //--- Check scrollbar visible
         int scrollbar_x = displayX + displayW - 16; //--- Calculate scrollbar x
         int button_size = 16;                       //--- Set button size
         int xd_slider = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XDISTANCE); //--- Get slider x
         int yd_slider = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE); //--- Get slider y
         int xs_slider = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XSIZE); //--- Get slider width
         int ys_slider = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE); //--- Get slider height
         bool isMouseInsideUp = (mouseX >= scrollbar_x && mouseX <= scrollbar_x + 16 &&
                                 mouseY >= displayY &&
                                 mouseY <= displayY + button_size); //--- Check scroll up hover
         bool isMouseInsideDown = (mouseX >= scrollbar_x && mouseX <= scrollbar_x + 16 &&
                                   mouseY >= displayY + g_displayHeight - button_size &&
                                   mouseY <= displayY + g_displayHeight); //--- Check scroll down hover
         bool isMouseInsideSlider = (mouseX >= xd_slider && mouseX <= xd_slider + xs_slider &&
                                     mouseY >= yd_slider && mouseY <= yd_slider + ys_slider); //--- Check slider hover
         if (isMouseInsideUp != prevMouseInsideScrollUp) { //--- Check scroll up hover change
            ObjectSetInteger(0, SCROLL_UP_REC, OBJPROP_BGCOLOR, isMouseInsideUp ? clrSilver : clrGainsboro); //--- Update scroll up color
            prevMouseInsideScrollUp = isMouseInsideUp; //--- Update hover state
            ChartRedraw();                         //--- Redraw chart
         }
         if (isMouseInsideDown != prevMouseInsideScrollDown) { //--- Check scroll down hover change
            ObjectSetInteger(0, SCROLL_DOWN_REC, OBJPROP_BGCOLOR, isMouseInsideDown ? clrSilver : clrGainsboro); //--- Update scroll down color
            prevMouseInsideScrollDown = isMouseInsideDown; //--- Update hover state
            ChartRedraw();                         //--- Redraw chart
         }
         if (isMouseInsideSlider != prevMouseInsideSlider && !movingStateSlider) { //--- Check slider hover change
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_BGCOLOR, isMouseInsideSlider ? clrDarkGray : clrSilver); //--- Update slider color
            prevMouseInsideSlider = isMouseInsideSlider; //--- Update hover state
            ChartRedraw();                         //--- Redraw chart
         }
      }
   } else if (id == CHARTEVENT_MOUSE_WHEEL) {      //--- Handle mouse wheel events
      int mouseX = (int)lparam;                    //--- Get mouse x coordinate
      int mouseY = (int)dparam;                    //--- Get mouse y coordinate
      int delta = (int)sparam;                     //--- Get wheel delta
      bool in_display = (mouseX >= displayX && mouseX <= displayX + displayW &&
                         mouseY >= displayY && mouseY <= displayY + displayH); //--- Check if mouse in display
      if (in_display != mouse_in_display) {        //--- Check display hover change
         mouse_in_display = in_display;            //--- Update display hover
         ChartSetInteger(0, CHART_MOUSE_SCROLL, !(mouse_in_display && need_scroll)); //--- Update chart scroll
         if (ScrollbarMode == SCROLL_DYNAMIC_HOVER) { //--- Check dynamic hover mode
            UpdateResponseDisplay();               //--- Update response display
         }
      }
      if (in_display && need_scroll) {             //--- Check scroll conditions
         int scroll_amount = 30 * (delta > 0 ? -1 : 1); //--- Calculate scroll amount
         scroll_pos = MathMax(0, MathMin(MathMax(0, g_total_height - g_visible_height), scroll_pos + scroll_amount)); //--- Update scroll position
         UpdateResponseDisplay();                  //--- Update response display
         if (scroll_visible) {                     //--- Check scrollbar visible
            UpdateSliderPosition();                //--- Update slider position
            UpdateButtonColors();                  //--- Update button colors
         }
         ChartRedraw();                            //--- Redraw chart
      }
   }
}

Чтобы обеспечить полную интерактивность, в функции OnChartEvent вычисляем координаты области отображения (displayX, displayY, displayW, displayH), кнопки очистки (clearX, clearY, clearW, clearH), кнопки нового чата (new_chat_x, new_chat_w, new_chat_h) и кнопки отправки (sendX, sendY, sendW, sendH), используя глобальные переменные макета. Для событий клика (CHARTEVENT_OBJECT_CLICK) обрабатываем ChatGPT_ClearButton и ChatGPT_NewChatButton: очищаем conversationHistory, сбрасываем scroll_pos и prev_scroll_pos, очищаем поле ввода с помощью ObjectSetString и обновляем область отображения функцией UpdateResponseDisplay. Для кнопок прокрутки SCROLL_UP_REC, SCROLL_UP_LABEL, SCROLL_DOWN_REC и SCROLL_DOWN_LABEL вызываем соответственно ScrollUp или ScrollDown.

Для событий перемещения мыши (CHARTEVENT_MOUSE_MOVE) отслеживаем наведение на кнопки отправки, очистки и нового чата. При наведении с помощью ObjectSetInteger обновляем цвета их фона: button_darker_bg, clear_darker_bg и new_chat_darker_bg. Также проверяем, находится ли указатель в области отображения, присваиваем флагу mouse_in_display соответствующее значение, обновляем прокрутку графика через ChartSetInteger и в режиме SCROLL_DYNAMIC_HOVER обновляем область отображения.

Чтобы обработать перетаскивание ползунка, отслеживаем клики по SCROLL_SLIDER, присваиваем movingStateSlider нужное значение, обновляем Y-координату ползунка с помощью ObjectSetInteger с учетом движения мыши, вычисляем scroll_pos по коэффициенту прокрутки и записываем данные в журнал через функцию FileWriteString. При отпускании кнопки мыши сбрасываем состояние перетаскивания и восстанавливаем цвет ползунка. Для событий колесика мыши (CHARTEVENT_MOUSE_WHEEL) изменяем scroll_pos на 30 пикселей в зависимости от направления прокрутки, обновляем область отображения и, если полоса прокрутки отображается, обновляем ее внешний вид. Также обрабатываем эффекты наведения на полосу прокрутки, обновляя цвета кнопок прокрутки вверх и вниз, а также ползунка. После каждого действия вызываем ChartRedraw и выполняем перерисовку. В результате программа поддерживает клики, наведение, перетаскивание и прокрутку. Ниже показан окончательный результат.

UPGRADED VS PREVIOUS VERSION COMPARISON

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


Тестирование программы с ChatGPT

Мы провели тестирование; ниже представлена итоговая визуализация в виде одного изображения формата Graphics Interchange Format (GIF).

BACKTEST GIF


Заключение

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

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

Прикрепленные файлы |
Встраивание торговой дисциплины в код (Часть 7): Автоматизация защиты эквити с помощью логики управления Встраивание торговой дисциплины в код (Часть 7): Автоматизация защиты эквити с помощью логики управления
Автоматизированные торговые системы часто уделяют большое внимание генерации сигналов, пренебрегая механизмами, необходимыми для защиты капитала в периоды стресса. В данной статье представлен фреймворк управления эквити в MQL5, который отслеживает условия просадки, оценивает давление на эквити и динамически управляет торговой активностью с помощью модели управления рисками, основанной на состояниях. Объединяя анализ просадки, логику охлаждения, авторизацию сделок и ограничения на исполнение, эта структура демонстрирует, как торговая дисциплина может быть непосредственно встроена в код с использованием модульной и расширяемой архитектуры.
Анализ CSV-данных (Часть 4): Разработка автоматизированного Python-решения для сравнительного анализа и валидации стратегий MQL5 Анализ CSV-данных (Часть 4): Разработка автоматизированного Python-решения для сравнительного анализа и валидации стратегий MQL5
В статье представлен воспроизводимый пайплайн передачи данных из MetaTrader 5 в Python для масштабного исследования индикаторов. Схема экспорта MQL5 фиксирует обязательные столбцы, включая пользовательские счётчики задержки и ложных срабатываний. Базовый модуль выполняет сопоставление результатов с одинаковыми параметрами по инструментам и таймфреймам, а модуль walk-forward-валидации жёстко фиксирует оптимум, найденный на окне InSample, и оценивает его на ранее не виденных данных OutSample OutSample. В результате читатель получает несмещённые метрики устойчивости и автоматизацию, устраняющую смещение из-за ручного отбора.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
От начального до среднего уровня: Очереди, списки и деревья (I) От начального до среднего уровня: Очереди, списки и деревья (I)
В данной статье мы начнем изучать небольшую серию концепций, имеющих огромное значение для тех, кто действительно хочет научиться программировать правильно. Поскольку вначале это может показаться очень сложным, несмотря на то что в основе лежат простые элементы, мы будем разбирать материал постепенно. Итак, давайте начнём разбираться, что такое очереди данных.