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

Создание торговых систем с искусственным интеллектом на MQL5 (Часть 2): Разработка программы с интеграцией ChatGPT и пользовательским интерфейсом

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

Введение

В предыдущей статье (часть 1) был разработан модуль разбора JSON (JavaScript Object Notation) на языке MetaQuotes Language 5 (MQL5) на основе класса, обеспечивающего сериализацию и десериализацию JSON-данных для взаимодействия с API ИИ. Во второй части создается программа с интеграцией ChatGPT и пользовательским интерфейсом. Модуль JSON применяется для отправки промптов в API OpenAI и отображения ответов на графике MetaTrader 5, что позволяет интерактивно получать ИИ-аналитику для торговли. Мы рассмотрим следующие темы:

  1. Общая схема программы с интеграцией ChatGPT
  2. Настройка доступа к API OpenAI и конфигурация MetaTrader 5
  3. Реализация на MQL5
  4. Тестирование программы ChatGPT
  5. Заключение

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


Общая схема программы с интеграцией ChatGPT

Разрабатываемая программа с ChatGPT интегрирует возможности модели ИИ в MQL5. Она позволяет отправлять промпты и получать аналитику непосредственно на графике MetaTrader 5, используя модуль разбора JSON из предыдущей части. Программа предоставит удобный интерфейс для ввода запросов и просмотра ответов ИИ. Это практический шаг к торговым системам на основе ИИ, способным анализировать рынки и предлагать стратегии.

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

PROGRAM FRAMEWORK

Перейдем к настройке API и конфигурации MetaTrader 5.


Настройка доступа к API OpenAI и конфигурация MetaTrader 5

Чтобы программа с интеграцией ChatGPT могла взаимодействовать с API OpenAI, необходимо получить действующий API-ключ и настроить MetaTrader 5 (MT5) для разрешения HTTP-запросов для беспрепятственной интеграции. Рассмотрим получение API-ключа OpenAI, проверку его работоспособности с помощью тестов curl и настройку торгового терминала для разрешения URL-адреса эндпоинта API. Это обеспечит надежную связь.

Получение API-ключа OpenAI

Создайте аккаунт OpenAI: перейдите на platform.openai.com и зарегистрируйтесь либо войдите в аккаунт. Перейдите в раздел API и создайте новый API-ключ на панели API Keys. Безопасно скопируйте ключ, например начинающийся со строки "sk-", поскольку он будет использоваться в конфигурации программы.

Начальная страница:

INITIAL LANDING PAGE

Генерация секретного ключа:

API SECRET KEY GENERATION

Защитите API-ключ: храните ключ в безопасном месте, поскольку он предоставляет доступ к сервисам OpenAI. Обратите внимание, что при утечке ключ может быть аннулирован.

Проверка API-ключа с помощью тестов curl

Чтобы убедиться в работоспособности API-ключа, выполним тест curl из командной строки, моделирующий запрос к API:

  • Установите curl: убедитесь, что curl установлен на вашей системе (доступен по умолчанию на большинстве систем Linux/macOS или может быть загружен для Windows).
  • Выполните тестовый запрос: откройте терминал (в Windows – командную строку) и выполните следующую команду curl, заменив <YOUR_API_KEY> на действующий API-ключ OpenAI:

curl -X POST https://api.openai.com/v1/chat/completions -H "Authorization: Bearer <YOUR_API_KEY>" -H "Content-Type: application/json" -d "{\"model\":\"gpt-3.5-turbo\",\"messages\":[{\"role\":\"user\",\"content\":\"Test prompt\"}],\"max_tokens\":50}"

  • Проверьте ответ: успешный ответ (HTTP 200) возвращает JSON-объект с массивом choices, содержащим ответ ИИ (например, "content": "Это тестовый ответ"). Если в ответе присутствует поле error, например из-за недействительного API-ключа или превышения квоты, проверьте действительность ключа и платежный статус аккаунта OpenAI. Сохраните ответ в журнале, чтобы подтвердить работоспособность. После успешного запуска должен получиться результат, подобный следующему.

API CURL TEST ON CMD

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

<!DOCTYPE html>
<html>
<head>
    <title>OpenAI API Test</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
        .container { border: 1px solid #ddd; padding: 20px; border-radius: 5px; }
        input, textarea, button { width: 100%; padding: 10px; margin: 5px 0; }
        textarea { height: 100px; }
        .response { background-color: #f5f5f5; padding: 15px; margin-top: 10px; }
    </style>
</head>
<body>
    <div class="container">
        <h2>OpenAI API Test</h2>
        
        <div>
            <label for="apiKey">API Key:</label>
            <input type="text" id="apiKey" placeholder="Enter your API key">
        </div>
        
        <div>
            <label for="prompt">Prompt:</label>
            <textarea id="prompt" placeholder="Enter your prompt">Test prompt</textarea>
        </div>
        
        <div>
            <label for="maxTokens">Max Tokens:</label>
            <input type="number" id="maxTokens" value="50">
        </div>
        
        <button onclick="testAPI()">Test API</button>
        
        <div id="response" class="response" style="display: none;">
            <h3>Response:</h3>
            <pre id="responseContent"></pre>
        </div>
    </div>

    <script>
        async function testAPI() {
            const apiKey = document.getElementById('apiKey').value;
            const prompt = document.getElementById('prompt').value;
            const maxTokens = document.getElementById('maxTokens').value;
            
            if (!apiKey) {
                alert('Please enter your API key');
                return;
            }
            
            try {
                const response = await fetch('https://api.openai.com/v1/chat/completions', {
                    method: 'POST',
                    headers: {
                        'Authorization': `Bearer ${apiKey}`,
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({
                        model: 'gpt-3.5-turbo',
                        messages: [{role: 'user', content: prompt}],
                        max_tokens: parseInt(maxTokens)
                    })
                });
                
                const data = await response.json();
                
                // Display response
                document.getElementById('responseContent').textContent = JSON.stringify(data, null, 2);
                document.getElementById('response').style.display = 'block';
                
                // Check for errors
                if (data.error) {
                    console.error('API Error:', data.error);
                }
            } catch (error) {
                console.error('Error:', error);
                document.getElementById('responseContent').textContent = `Error: ${error.message}`;
                document.getElementById('response').style.display = 'block';
            }
        }
        
        // Pre-fill with your API key (optional - remove if you don't want this)
        document.getElementById('apiKey').value = 'sk-proj-fb... <YOUR_API_KEY>... X75...';

    </script>
</body>
</html>

Сохраните этот код в HTML-файл, например api_test.html, откройте его в браузере и добавьте свой ключ. Должен появиться следующий интерфейс.

PROGRAM INTERFACE

При нажатии кнопки Test API должен появиться следующий ответ.

TEST RESPONSE

Настройка MetaTrader 5 для взаимодействия с API

Чтобы программа могла отправлять HTTP-запросы к эндпоинту OpenAI, торговый терминал настраивается следующим образом:

  • Разрешение WebRequest. Откройте MT5, перейдите в Сервис → Настройки → Советники и установите флажок "Разрешить WebRequest для указанных URL".
  • Добавление эндпоинта API. В поле "WebRequest URLs" добавьте https://api.openai.com, чтобы разрешить запросы к API OpenAI. Благодаря этому функция WebRequest программы сможет взаимодействовать без блокировки настройками безопасности MT5.

Ниже приведена полная визуализация.

MT5 WEB REQUEST LINK ENDPOINT

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


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

Чтобы создать программу на языке MQL5, откройте MetaEditor, перейдите в Навигатор, найдите папку Indicators, нажмите кнопку "Создать" и следуйте указаниям для создания файла. После создания файла в среде разработки необходимо объявить глобальные переменные и входные параметры, используемые во всей программе.

//+------------------------------------------------------------------+
//|                                             a. ChatGPT AI EA.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
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 = 500;                                           // Max length of ChatGPT response to display
input string LogFileName = "ChatGPT_EA_Log.txt";                             // Log file name
//--- Hardcoded API key (confirmed valid via curl test)
//--- Keep your API key private, for us, we added some parts so you can get the actual thing you should be having
string OpenAI_API_Key = "sk-proj-vjKCf ... <YOUR FULL ACTUAL API KEY HERE> ... jY..79n...";

Начнем реализацию программы с первичной настройки входных параметров для подключения к API OpenAI. Сначала определяется строковый входной параметр OpenAI_Model со значением "gpt-3.5-turbo". Он задает модель ChatGPT для API-запросов и при необходимости позволяет переключаться между моделями. Здесь можно выбрать другую модель. Затем присваиваем OpenAI_Endpoint значение https://api.openai.com/v1/chat/completions, определяющее URL-адрес для отправки HTTP POST-запросов к API OpenAI.

Далее настраивается целочисленный параметр MaxResponseLength со значением 500. Он ограничивает длину отображаемого ответа ChatGPT, обеспечивая удобочитаемый вывод на графике MetaTrader 5. Наконец, LogFileName присваивается значение "ChatGPT_EA_Log.txt" для логирования взаимодействий с API и ошибок. В OpenAI_API_Key жестко прописывается ранее проверенный API-ключ, например начинающийся с "sk-proj-", для аутентификации запросов и работы программы. Для логирования и визуализации кнопок добавляются следующие переменные.

//--- Global variables
string conversationHistory = "";                             //--- Stores conversation history
int logFileHandle = INVALID_HANDLE;                          //--- Handle for log file
bool button_hover = false;                                   //--- Flag for button hover state
color button_original_bg = clrRoyalBlue;                     //--- Original button background color
color button_darker_bg;                                      //--- Darkened button background for hover

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

//+------------------------------------------------------------------+
//| Creates a rectangle label object                                 |
//+------------------------------------------------------------------+
bool createRecLabel(string objName, int xDistance, int yDistance, int xSize, int ySize,
                    color bgColor, int borderWidth, color borderColor = clrNONE,
                    ENUM_BORDER_TYPE borderType = BORDER_FLAT,
                    ENUM_LINE_STYLE borderStyle = STYLE_SOLID,
                    ENUM_BASE_CORNER corner = CORNER_LEFT_UPPER) {   //--- Create rectangle label
   ResetLastError();                                         //--- Reset previous errors
   if (!ObjectCreate(0, objName, OBJ_RECTANGLE_LABEL, 0, 0, 0)) { //--- Attempt creation
      Print(__FUNCTION__, ": failed to create rec label! Error code = ", _LastError); //--- Print error
      return (false);                                        //--- Return failure
   }
   ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xDistance); //--- Set x distance
   ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yDistance); //--- Set y distance
   ObjectSetInteger(0, objName, OBJPROP_XSIZE, xSize);       //--- Set width
   ObjectSetInteger(0, objName, OBJPROP_YSIZE, ySize);       //--- Set height
   ObjectSetInteger(0, objName, OBJPROP_CORNER, corner);     //--- Set corner
   ObjectSetInteger(0, objName, OBJPROP_BGCOLOR, bgColor);   //--- Set background color
   ObjectSetInteger(0, objName, OBJPROP_BORDER_TYPE, borderType); //--- Set border type
   ObjectSetInteger(0, objName, OBJPROP_STYLE, borderStyle); //--- Set border style
   ObjectSetInteger(0, objName, OBJPROP_WIDTH, borderWidth); //--- Set border width
   ObjectSetInteger(0, objName, OBJPROP_COLOR, borderColor); //--- Set border color
   ObjectSetInteger(0, objName, OBJPROP_BACK, false);        //--- Not background
   ObjectSetInteger(0, objName, OBJPROP_STATE, false);       //--- Not pressed
   ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);  //--- Not selectable
   ObjectSetInteger(0, objName, OBJPROP_SELECTED, false);    //--- Not selected
   ChartRedraw(0);                                           //--- Redraw chart
   return (true);                                            //--- Success
}
//+------------------------------------------------------------------+
//| Creates a button object                                          |
//+------------------------------------------------------------------+
bool createButton(string objName, int xDistance, int yDistance, int xSize, int ySize,
                  string text = "", color textColor = clrBlack, int fontSize = 12,
                  color bgColor = clrNONE, color borderColor = clrNONE,
                  string font = "Arial Rounded MT Bold",
                  ENUM_BASE_CORNER corner = CORNER_LEFT_UPPER, bool isBack = false) { //--- Create button
   ResetLastError();                                         //--- Reset errors
   if (!ObjectCreate(0, objName, OBJ_BUTTON, 0, 0, 0)) {    //--- Attempt creation
      Print(__FUNCTION__, ": failed to create the button! Error code = ", _LastError); //--- Print error
      return (false);                                        //--- Failure
   }
   ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xDistance); //--- Set x distance
   ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yDistance); //--- Set y distance
   ObjectSetInteger(0, objName, OBJPROP_XSIZE, xSize);       //--- Set width
   ObjectSetInteger(0, objName, OBJPROP_YSIZE, ySize);       //--- Set height
   ObjectSetInteger(0, objName, OBJPROP_CORNER, corner);     //--- Set corner
   ObjectSetString(0, objName, OBJPROP_TEXT, text);          //--- Set text
   ObjectSetInteger(0, objName, OBJPROP_COLOR, textColor);   //--- Set text color
   ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontSize); //--- Set font size
   ObjectSetString(0, objName, OBJPROP_FONT, font);          //--- Set font
   ObjectSetInteger(0, objName, OBJPROP_BGCOLOR, bgColor);   //--- Set background
   ObjectSetInteger(0, objName, OBJPROP_BORDER_COLOR, borderColor); //--- Set border color
   ObjectSetInteger(0, objName, OBJPROP_BACK, isBack);       //--- Set back
   ObjectSetInteger(0, objName, OBJPROP_STATE, false);       //--- Not pressed
   ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);  //--- Not selectable
   ObjectSetInteger(0, objName, OBJPROP_SELECTED, false);    //--- Not selected
   ChartRedraw(0);                                           //--- Redraw
   return (true);                                            //--- Success
}
//+------------------------------------------------------------------+
//| Creates an edit field object                                     |
//+------------------------------------------------------------------+
bool createEdit(string objName, int xDistance, int yDistance, int xSize, int ySize,
                string text = "", color textColor = clrBlack, int fontSize = 12,
                color bgColor = clrNONE, color borderColor = clrNONE,
                string font = "Arial Rounded MT Bold",
                ENUM_BASE_CORNER corner = CORNER_LEFT_UPPER,
                int align = ALIGN_LEFT, bool readOnly = false) {  //--- Create edit
   ResetLastError();                                         //--- Reset errors
   if (!ObjectCreate(0, objName, OBJ_EDIT, 0, 0, 0)) {      //--- Attempt creation
      Print(__FUNCTION__, ": failed to create the edit! Error code = ", _LastError); //--- Print error
      return (false);                                        //--- Failure
   }
   ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xDistance); //--- Set x distance
   ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yDistance); //--- Set y distance
   ObjectSetInteger(0, objName, OBJPROP_XSIZE, xSize);       //--- Set width
   ObjectSetInteger(0, objName, OBJPROP_YSIZE, ySize);       //--- Set height
   ObjectSetInteger(0, objName, OBJPROP_CORNER, corner);     //--- Set corner
   ObjectSetString(0, objName, OBJPROP_TEXT, text);          //--- Set text
   ObjectSetInteger(0, objName, OBJPROP_COLOR, textColor);   //--- Set text color
   ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontSize); //--- Set font size
   ObjectSetString(0, objName, OBJPROP_FONT, font);          //--- Set font
   ObjectSetInteger(0, objName, OBJPROP_BGCOLOR, bgColor);   //--- Set background
   ObjectSetInteger(0, objName, OBJPROP_BORDER_COLOR, borderColor); //--- Set border color
   ObjectSetInteger(0, objName, OBJPROP_ALIGN, align);       //--- Set alignment
   ObjectSetInteger(0, objName, OBJPROP_READONLY, readOnly); //--- Set read-only
   ObjectSetInteger(0, objName, OBJPROP_BACK, false);        //--- Not back
   ObjectSetInteger(0, objName, OBJPROP_STATE, false);       //--- Not active
   ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);  //--- Not selectable
   ObjectSetInteger(0, objName, OBJPROP_SELECTED, false);    //--- Not selected
   ChartRedraw(0);                                           //--- Redraw
   return (true);                                            //--- Success
}
//+------------------------------------------------------------------+
//| Creates a text label object                                      |
//+------------------------------------------------------------------+
bool createLabel(string objName, int xDistance, int yDistance,
                 string text, color textColor = clrBlack, int fontSize = 12,
                 string font = "Arial Rounded MT Bold",
                 ENUM_BASE_CORNER corner = CORNER_LEFT_UPPER,
                 ENUM_ANCHOR_POINT anchor = ANCHOR_LEFT_UPPER) {   //--- Create label
   ResetLastError();                                         //--- Reset errors
   if (!ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0)) {     //--- Attempt creation
      Print(__FUNCTION__, ": failed to create the label! Error code = ", _LastError); //--- Print error
      return (false);                                        //--- Failure
   }
   ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xDistance); //--- Set x distance
   ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yDistance); //--- Set y distance
   ObjectSetInteger(0, objName, OBJPROP_CORNER, corner);     //--- Set corner
   ObjectSetString(0, objName, OBJPROP_TEXT, text);          //--- Set text
   ObjectSetInteger(0, objName, OBJPROP_COLOR, textColor);   //--- Set color
   ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontSize); //--- Set font size
   ObjectSetString(0, objName, OBJPROP_FONT, font);          //--- Set font
   ObjectSetInteger(0, objName, OBJPROP_BACK, false);        //--- Not back
   ObjectSetInteger(0, objName, OBJPROP_STATE, false);       //--- Not active
   ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);  //--- Not selectable
   ObjectSetInteger(0, objName, OBJPROP_SELECTED, false);    //--- Not selected
   ObjectSetInteger(0, objName, OBJPROP_ANCHOR, anchor);     //--- Set anchor
   ChartRedraw(0);                                           //--- Redraw
   return (true);                                            //--- Success
}

Здесь создаются основные графические элементы для взаимодействия трейдера с программой на графике. Сначала реализуется функция createRecLabel. Она использует ObjectCreate для создания прямоугольной метки OBJ_RECTANGLE_LABEL в координатах xDistance, yDistance с размерами xSize, ySize. Через ObjectSetInteger задаются цвет фона OBJPROP_BGCOLOR, ширина и цвет рамки OBJPROP_COLOR, тип рамки OBJPROP_BORDER_TYPE со значением BORDER_FLAT, стиль OBJPROP_STYLE со значением STYLE_SOLID и угол привязки OBJPROP_CORNER со значением CORNER_LEFT_UPPER. Функция делает объект невыбираемым, отключает отображение объекта в качестве фонового, обновляет график через ChartRedraw и возвращает false при ошибке создания, фиксируя ошибку в журнале терминала через функцию Print.

Затем реализуется функция createButton, создающая кнопку OBJ_BUTTON с аналогичными параметрами координат и размеров. Через ObjectSetString и ObjectSetInteger задаются текст OBJPROP_TEXT, цвет текста OBJPROP_COLOR, размер и имя шрифта Arial Rounded MT Bold, цвет фона, цвет рамки и угол привязки. Опциональный флаг фона isBack определяет отображение на заднем плане. Функция задает для кнопки невыбираемое ненажатое состояние, обновляет график и возвращает false при ошибке, фиксируя ее в журнале.

Далее реализуется функция createEdit, создающая редактируемое текстовое поле OBJ_EDIT. Она принимает параметры текста, выравнивания OBJPROP_ALIGN со значением ALIGN_LEFT, режима только для чтения и аналогичных свойств оформления. Через ObjectSetString и ObjectSetInteger настраиваются внешний вид и поведение, после чего график обновляется, а ошибки обрабатываются аналогичным способом. Наконец, добавляется функция createLabel, создающая текстовую метку OBJ_LABEL с указанными текстом, цветом, размером и именем шрифта, углом и точкой привязки OBJPROP_ANCHOR со значением ANCHOR_LEFT_UPPER. Функция обеспечивает невыбираемое отображение метки и обновляет график. Эти функции формируют графическую основу панели управления программы, обеспечивая ввод запросов и отображение взаимодействий с ИИ. Требуется кнопка, которая затемняется при наведении. Для этого создается отдельная функция.

//+------------------------------------------------------------------+
//| Darkens a given color by a factor                                |
//+------------------------------------------------------------------+
color DarkenColor(color colorValue, double factor = 0.8) {   //--- Darken color function
   int red = int((colorValue & 0xFF) * factor);              //--- Calculate darkened red component
   int green = int(((colorValue >> 8) & 0xFF) * factor);     //--- Calculate darkened green component
   int blue = int(((colorValue >> 16) & 0xFF) * factor);     //--- Calculate darkened blue component
   return (color)(red | (green << 8) | (blue << 16));        //--- Combine and return darkened color
}

Здесь реализуется функция DarkenColor, принимающая значение цвета и необязательный коэффициент, по умолчанию 0,8, для уменьшения яркости. Красный, зеленый и синий компоненты извлекаются побитовыми операциями: "colorValue & 0xFF" для красного, "(colorValue >> 8) & 0xFF" для зеленого и "(colorValue >> 16) & 0xFF" для синего. Затем компоненты умножаются на коэффициент, объединяются с помощью побитовых сдвигов и операции ИЛИ "red \| (green << 8) \| (blue << 16)", после чего функция возвращает новый цвет. Теперь создадим панель управления.

//+------------------------------------------------------------------+
//| Creates the dashboard UI                                         |
//+------------------------------------------------------------------+
void CreateDashboard() {                                     //--- Create UI
   createEdit("ChatGPT_InputEdit", 20, 20, 400, 40, "", clrBlack, 12, clrWhiteSmoke, clrDarkGray, "Arial", CORNER_LEFT_UPPER, ALIGN_LEFT, false); //--- Input edit
   createButton("ChatGPT_SubmitButton", 430, 20, 100, 40, "Send", clrWhite, 12, button_original_bg, clrDarkBlue, "Arial", CORNER_LEFT_UPPER, false); //--- Submit button
   createRecLabel("ChatGPT_ResponseBg", 20, 70, 510, 300, clrWhite, 2, clrLightGray, BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER); //--- Response background
   ChartRedraw();                                            //--- Redraw
}

Реализуется функция CreateDashboard, создающая пользовательский интерфейс на графике для взаимодействия с ИИ. Сначала вызывается createEdit для создания редактируемого текстового поля ChatGPT_InputEdit в координатах (20, 20) с размерами 400x40 пикселей. Для него задаются черный текст, шрифт Arial размером 12 пунктов, фон цвета WhiteSmoke, темно-серая рамка, выравнивание по левому краю и доступ для редактирования. Поле позволяет вводить промпты. Затем createButton добавляет ChatGPT_SubmitButton в координатах (430, 20) с размерами 100x40 пикселей. Кнопка имеет метку Send, белый текст, шрифт Arial размером 12 пунктов, фон королевского синего цвета button_original_bg, темно-синюю рамку и расположение в верхнем левом углу. Она запускает API-запросы.

Далее вызывается createRecLabel для создания фона ответов ChatGPT_ResponseBg в координатах (20, 70) с размерами 510x300 пикселей. Задаются белый фон, светло-серая рамка толщиной 2 пикселя, плоский тип рамки и сплошной стиль. Это создает четкую область для отображения ответов ИИ. Наконец, вызывается ChartRedraw для обновления графика, чтобы все элементы интерфейса стали видимыми. Теперь функцию можно вызвать в обработчике событий OnInit, чтобы увидеть полученный результат.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {                                               //--- Initialization
   button_darker_bg = DarkenColor(button_original_bg);       //--- Set darker button color
   logFileHandle = FileOpen(LogFileName, FILE_READ | FILE_WRITE | FILE_TXT); //--- Open log
   if(logFileHandle == INVALID_HANDLE) {                     //--- Check handle
      Print("Failed to open log file: ", GetLastError());    //--- Print error
      return(INIT_FAILED);                                   //--- Fail init
   }
   FileSeek(logFileHandle, 0, SEEK_END);                     //--- Seek end
   FileWriteString(logFileHandle, "EA Initialized at " + TimeToString(TimeCurrent()) + "\n"); //--- Log init
   CreateDashboard();                                        //--- Create UI
   return(INIT_SUCCEEDED);                                   //--- Success
}
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {                            //--- Deinit
   ObjectsDeleteAll(0, "ChatGPT_");                          //--- Delete objects
   if(logFileHandle != INVALID_HANDLE) {                     //--- Check handle
      FileClose(logFileHandle);                              //--- Close log
   }
}

В функции OnInit  вызывается функция DarkenColor с аргументом button_original_bg для вычисления более темного оттенка для button_darker_bg. Это реализует эффект наведения для кнопки отправки. Затем с помощью FileOpen и флагов чтения, записи и текста открывается файл лога, указанный в LogFileName; его хэндл сохраняется в logFileHandle. Если хэндл недействителен, ошибка фиксируется в журнале терминала и возвращается INIT_FAILED. Это позволит отслеживать, что программа делает в фоновом режиме.

Далее с помощью FileSeek выполняется переход в конец файла лога, после чего через FileWriteString записывается сообщение инициализации с текущим временем, полученным с помощью функции TimeToString. Затем вызывается CreateDashboard для настройки пользовательского интерфейса, после чего возвращается INIT_SUCCEEDED, что означает успешную инициализацию. В функции OnDeinit выполняется очистка: ObjectsDeleteAll удаляет все графические объекты с префиксом "ChatGPT_", а при действительном logFileHandle файл лога закрывается с помощью FileClose. Пока программа прикреплена к графику, файл остается открытым. Это означает, что файл нельзя открыть, пока программа работает. Файл можно закрывать после отправки запроса и получения ответа, но это неудобно при активном взаимодействии. После запуска программы мы получаем следующий результат.

UI CREATED

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

//+------------------------------------------------------------------+
//| Wraps 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) { //--- Wrap text function
   const int maxChars = 60;                                  //--- Max chars per line
   ArrayResize(wrappedLines, 0);                             //--- Clear output array
   TextSetFont(font, -fontSize * 10, 0);                     //--- Set font for measurement
   string paragraphs[];                                      //--- Array for paragraphs
   int numParagraphs = StringSplit(inputText, '\n', paragraphs); //--- Split by newline
   for (int p = 0; p < numParagraphs; p++) {                 //--- Loop paragraphs
      string para = paragraphs[p];                           //--- Get paragraph
      if (StringLen(para) == 0) continue;                    //--- Skip empty
      string words[];                                        //--- Array for words
      int numWords = StringSplit(para, ' ', words);          //--- Split by space
      string currentLine = "";                               //--- Current line builder
      for (int w = 0; w < numWords; w++) {                   //--- Loop words
         string testLine = currentLine + (StringLen(currentLine) > 0 ? " " : "") + words[w]; //--- Test add word
         uint wid, hei;                                      //--- Width, height
         TextGetSize(testLine, wid, hei);                    //--- Get size
         int textWidth = (int)wid;                           //--- Cast width
         if (textWidth + offset <= maxWidth && StringLen(testLine) <= maxChars) { //--- Fits
            currentLine = testLine;                          //--- Update line
         } else {                                            //--- Doesn't fit
            if (StringLen(currentLine) > 0) {                //--- Add current if not empty
               int size = ArraySize(wrappedLines);           //--- Get size
               ArrayResize(wrappedLines, size + 1);          //--- Resize
               wrappedLines[size] = currentLine;             //--- Add line
            }
            currentLine = words[w];                          //--- Start new with word
            TextGetSize(currentLine, wid, hei);              //--- Get size
            textWidth = (int)wid;                            //--- Cast
            if (textWidth + offset > maxWidth || StringLen(currentLine) > maxChars) { //--- Word too long
               string wrappedWord = "";                      //--- Word builder
               for (int c = 0; c < StringLen(words[w]); c++) { //--- Char loop
                  string testWord = wrappedWord + StringSubstr(words[w], c, 1); //--- Test add char
                  TextGetSize(testWord, wid, hei);           //--- Get size
                  int wordWidth = (int)wid;                  //--- Cast
                  if (wordWidth + offset > maxWidth || StringLen(testWord) > maxChars) { //--- Char exceeds
                     if (StringLen(wrappedWord) > 0) {       //--- Add if not empty
                        int size = ArraySize(wrappedLines);  //--- Get size
                        ArrayResize(wrappedLines, size + 1); //--- Resize
                        wrappedLines[size] = wrappedWord;    //--- Add
                     }
                     wrappedWord = StringSubstr(words[w], c, 1); //--- New with char
                  } else {                                   //--- Fits
                     wrappedWord = testWord;                 //--- Update
                  }
               }
               currentLine = wrappedWord;                    //--- Set current
            }
         }
      }
      if (StringLen(currentLine) > 0) {                      //--- Add remaining line
         int size = ArraySize(wrappedLines);                 //--- Get size
         ArrayResize(wrappedLines, size + 1);                //--- Resize
         wrappedLines[size] = currentLine;                   //--- Add
      }
   }
}

Реализуется функция WrapText для форматирования текста на графике и обеспечения читаемости ответов ИИ. Сначала определяется константа maxChars со значением 60 для ограничения длины строки, хотя предельное значение можно установить равным 63. Выходной массив wrappedLines очищается с помощью ArrayResize, а шрифт задается через TextSetFont с указанными именем и размером, масштабируемым как -fontSize * 10. Затем входной текст разбивается на абзацы с помощью StringSplit, где символ переноса строки служит разделителем; после этого перебирается каждый непустой абзац. Каждый абзац снова разбивается на слова с помощью StringSplit, но теперь с пробелом в качестве разделителя; затем инициализируется пустая переменная currentLine.

Далее слова перебираются в цикле: testLine формируется добавлением каждого слова с пробелом, если currentLine не пуста; ширина testLine измеряется через TextGetSize. Если ширина со смещением не превышает maxWidth, а длина меньше maxChars, обновляется currentLine. В противном случае непустая currentLine добавляется в wrappedLines, а новая строка начинается с текущего слова. Для слишком длинных слов перебираются символы, формируя wrappedWord и проверяя его ширину. Когда ширина превышает maxWidth или число символов превышает maxChars, wrappedWord добавляется в wrappedLines, а currentLine обновляется оставшейся частью слова. Наконец, оставшаяся currentLine добавляется в wrappedLines, обеспечивая форматирование всего текста для четкого отображения в области ответов программы. Теперь эта логика используется для обновления области отображения примером текста, поясняющим пользователю, как начать работу.

//+------------------------------------------------------------------+
//| Updates the response display                                     |
//+------------------------------------------------------------------+
void UpdateResponseDisplay() {                               //--- Update display
   int total = ObjectsTotal(0, 0, -1);                       //--- Get total objects
   for (int j = total - 1; j >= 0; j--) {                    //--- Loop backwards
      string name = ObjectName(0, j, 0, -1);                 //--- Get name
      if (StringFind(name, "ChatGPT_ResponseLine_") == 0 || StringFind(name, "ChatGPT_MessageBg_") == 0) { //--- Check prefix
         ObjectDelete(0, name);                              //--- Delete
      }
   }
   string displayText = conversationHistory;                 //--- Get history
   if (displayText == "") {                                  //--- If empty
      string objName = "ChatGPT_ResponseLine_0";             //--- Name
      createLabel(objName, 30, 80, "Type your message above and click Send to chat with the AI.", clrGray, 10, "Arial", CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER); //--- Placeholder
      ChartRedraw();                                         //--- Redraw
      return;                                                //--- Exit
   }
   string font = "Arial";                                    //--- Font
   int fontSize = 10;                                        //--- Size
   int padding = 10;                                         //--- Padding
   int maxWidth = 510 - 2 * padding;                         //--- Max width
   string wrappedLines[];                                    //--- Wrapped lines
   WrapText(displayText, font, fontSize, maxWidth, wrappedLines, 0); //--- Wrap text
   TextSetFont(font, -fontSize * 10, 0);                     //--- Set font
   uint wid, hei;                                            //--- Size vars
   TextGetSize("A", wid, hei);                               //--- Get height
   int lineHeight = (int)hei;                                //--- Line height
   int responseHeight = 300;                                 //--- Response height
   int maxVisibleLines = (responseHeight - 2 * padding) / lineHeight; //--- Max lines
   int numLines = ArraySize(wrappedLines);                   //--- Num lines
   int startLine = MathMax(0, numLines - maxVisibleLines);   //--- Start line
   int textX = 20 + padding;                                 //--- Text x
   int textY = 70 + padding;                                 //--- Text y
   color currentColor = clrWhite;                            //--- Current color
   for (int i = startLine; i < numLines; i++) {              //--- Loop lines
      string line = wrappedLines[i];                         //--- Get line
      if (StringFind(line, "You: ") == 0) {                  //--- User color
         currentColor = clrGray;                             //--- Set gray
      } else if (StringFind(line, "AI: ") == 0) {            //--- AI color
         currentColor = clrBlue;                             //--- Set blue
      }
      string objName = "ChatGPT_ResponseLine_" + IntegerToString(i - startLine); //--- Name
      createLabel(objName, textX, textY, line, currentColor, fontSize, font, CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER); //--- Create label
      textY += lineHeight;                                   //--- Next y
   }
   ChartRedraw();                                            //--- Redraw
}

Здесь реализуется функция UpdateResponseDisplay для динамической отрисовки истории переписки. Сначала с помощью ObjectsTotal подсчитываются все графические объекты и выполняется обратный перебор. ObjectName получает имя каждого объекта, а ObjectDelete удаляет объекты с префиксами "ChatGPT_ResponseLine_" или "ChatGPT_MessageBg_", очищая предыдущие элементы отображения. Затем проверяется, пуста ли conversationHistory. Если да, вызывается createLabel для отображения сообщения-заполнителя в координатах (30, 80) серым цветом и шрифтом Arial размером 10 пунктов, предлагающего пользователю ввести сообщение; после перерисовки функция завершает работу.

Если conversationHistory содержит текст, задаются шрифт Arial, размер 10 и отступ 10. Затем maxWidth вычисляется как 510 минус удвоенный отступ, а WrapText разбивает текст на wrappedLines. Далее TextSetFont и TextGetSize определяют высоту строчки, maxVisibleLines вычисляется по высоте области ответов в 300 пикселей, а MathMax задает начальную строку для отображения последних строк. Массив wrappedLines перебирается начиная с startLine: для строк с префиксом "You:" currentColor задается серым, а для "AI:" – синим. Вызывается createLabel для отображения каждой строчки по координатам textX, textY; для каждой метки textY увеличивается на lineHeight, а имена создаются по шаблону "ChatGPT_ResponseLine_0". Наконец, вызывается ChartRedraw для обновления графика и четкого визуального отображения переписки. При вызове этой функции во время инициализации получается следующий результат.

UPDATED DISPLAY WITH INITIAL MESSAGE

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

//+------------------------------------------------------------------+
//| Escapes string for JSON                                          |
//+------------------------------------------------------------------+
string JsonEscape(string value) {                            //--- JSON escape
   StringReplace(value, "\\", "\\\\");                       //--- Escape backslash
   StringReplace(value, "\"", "\\\"");                       //--- Escape quote
   StringReplace(value, "\n", "\\n");                        //--- Escape newline
   StringReplace(value, "\r", "\\r");                        //--- Escape carriage
   StringReplace(value, "\t", "\\t");                        //--- Escape tab
   StringReplace(value, "\f", "\\f");                        //--- Escape form feed
   for(int i = 0; i < StringLen(value); i++) {               //--- Loop chars
      ushort charCode = StringGetCharacter(value, i);        //--- Get char
      if(charCode < 32 || charCode == 127) {                 //--- Control chars
         string hex = StringFormat("\\u%04x", charCode);     //--- Hex escape
         string before = StringSubstr(value, 0, i);          //--- Before part
         string after = StringSubstr(value, i + 1);          //--- After part
         value = before + hex + after;                       //--- Replace
         i += 5;                                             //--- Skip added
      }
   }
   return value;                                             //--- Return escaped
}
//+------------------------------------------------------------------+
//| Logs char array as hex for debugging                             |
//+------------------------------------------------------------------+
string LogCharArray(char &data[]) {                          //--- Log char array
   string result = "";                                       //--- Result string
   for(int i = 0; i < ArraySize(data); i++) {                //--- Loop array
      result += StringFormat("%02X ", data[i]);              //--- Append hex
   }
   return result;                                            //--- Return
}

Перед реализацией основной логики обработки ответов создаются вспомогательные функции для экранирования JSON-строк и отладки, обеспечивающие надежное взаимодействие с API. Сначала реализуется функция JsonEscape, принимающая строку и экранирующая специальные символы для соответствия JSON. StringReplace преобразует обратную косую черту в "\\", кавычку в "\"", символ переноса строки в "\n", возврат каретки в "\r", табуляцию в "\t" и перевод страницы в "\f". Затем StringGetCharacter перебирает каждый символ, проверяя управляющие символы с кодом ASCII менее 32 либо равным 127, и заменяет их Unicode-последовательностью "\uXXXX" через StringFormat. Строка обновляется с помощью StringSubstr путем объединения частей до и после экранированного символа; индекс корректируется для пропуска добавленных символов. Функция возвращает экранированную строку.

Затем разрабатывается функция LogCharArray, преобразующая массив символов в шестнадцатеричную строку для отладки. Она инициализирует пустую строку result, перебирает массив с помощью ArraySize, добавляет шестнадцатеричное значение каждого символа через StringFormat в формате "%02X" и возвращает отформатированную строку. Эти функции обеспечат правильное форматирование промптов для запросов к API OpenAI и поддержку отладки при проверке данных API. Теперь их можно использовать для создания основной функции получения ответов.

//+------------------------------------------------------------------+
//| Gets ChatGPT response via API                                    |
//+------------------------------------------------------------------+
string GetChatGPTResponse(string prompt) {                   //--- Get AI response
   string escapedPrompt = JsonEscape(prompt);                //--- Escape prompt
   string requestData = "{\"model\":\"" + OpenAI_Model + "\",\"messages\":[{\"role\":\"user\",\"content\":\"" + escapedPrompt + "\"}],\"max_tokens\":500}"; //--- Build JSON
   FileWriteString(logFileHandle, "Request Data: " + requestData + "\n"); //--- Log data
   char postData[];                                          //--- Post array
   int dataLen = StringToCharArray(requestData, postData, 0, WHOLE_ARRAY, CP_UTF8); //--- To char array
   ArrayResize(postData, dataLen - 1);                       //--- Remove terminator
   FileWriteString(logFileHandle, "Raw Post Data (Hex): " + LogCharArray(postData) + "\n"); //--- Log hex
   string headers = "Authorization: Bearer " + OpenAI_API_Key + "\r\n" + //--- Build headers
                    "Content-Type: application/json; charset=UTF-8\r\n" +
                    "Content-Length: " + IntegerToString(dataLen - 1) + "\r\n\r\n";
   FileWriteString(logFileHandle, "Request Headers: " + headers + "\n"); //--- Log headers
   char result[];                                            //--- Result array
   string resultHeaders;                                     //--- Result headers
   int res = WebRequest("POST", OpenAI_Endpoint, headers, 10000, postData, result, resultHeaders); //--- Send request
   if(res != 200) {                                          //--- Check status
      string response = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8); //--- To string
      string errMsg = "API request failed: HTTP Code " + IntegerToString(res) + ", Error: " + IntegerToString(GetLastError()) + ", Response: " + response; //--- Error msg
      Print(errMsg);                                         //--- Print
      FileWriteString(logFileHandle, errMsg + "\n");          //--- Log
      FileWriteString(logFileHandle, "Raw Response Data (Hex): " + LogCharArray(result) + "\n"); //--- Log hex
      return errMsg;                                         //--- Return error
   }
   string response = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8); //--- To string
   FileWriteString(logFileHandle, "API Response: " + response + "\n"); //--- Log response
   JsonValue jsonObject;                                     //--- JSON object
   int index = 0;                                            //--- Index
   char charArray[];                                         //--- Char array
   int arrayLength = StringToCharArray(response, charArray, 0, WHOLE_ARRAY, CP_UTF8); //--- To char
   if(!jsonObject.DeserializeFromArray(charArray, arrayLength, index)) { //--- Deserialize
      string errMsg = "Error: Failed to parse API response JSON: " + response; //--- Error
      Print(errMsg);                                         //--- Print
      FileWriteString(logFileHandle, errMsg + "\n");         //--- Log
      return errMsg;                                         //--- Return
   }
   JsonValue *error = jsonObject.FindChildByKey("error");    //--- Find error
   if(error != NULL) {                                       //--- If error
      string errMsg = "API Error: " + error["message"].ToString(); //--- Get message
      Print(errMsg);                                         //--- Print
      FileWriteString(logFileHandle, errMsg + "\n");         //--- Log
      return errMsg;                                         //--- Return
   }
   string content = jsonObject["choices"][0]["message"]["content"].ToString(); //--- Get content
   if(StringLen(content) > 0) {                              //--- If content
      StringReplace(content, "\\n", "\n");                   //--- Replace escapes
      StringTrimLeft(content);                               //--- Trim left
      StringTrimRight(content);                              //--- Trim right
      return content;                                        //--- Return
   }
   string errMsg = "Error: No content in API response: " + response; //--- Error
   Print(errMsg);                                            //--- Print
   FileWriteString(logFileHandle, errMsg + "\n");            //--- Log
   return errMsg;                                            //--- Return
}

Основная функциональность программы завершается реализацией функции GetChatGPTResponse для взаимодействия с API ChatGPT от OpenAI. Сначала JsonEscape экранирует входной промпт, обеспечивая совместимость с JSON. Затем создается JSON-строка requestData, содержащая значение OpenAI_Model, массив сообщений пользователя с экранированным промптом и max_tokens со значением 500; эта строка записывается в logFileHandle с помощью FileWriteString. Затем requestData преобразуется в символьный массив postData с помощью StringToCharArray в кодировке UTF-8. ArrayResize изменяет его размер, удаляя завершающий нулевой символ; шестнадцатеричное представление фиксируется функцией LogCharArray.

Далее формируются HTTP-заголовки с OpenAI_API_Key, типом и длиной содержимого, после чего они записываются в лог. POST-запрос к OpenAI_Endpoint отправляется через WebRequest с таймаутом 10 секунд; ответ сохраняется в result. Если код ответа не равен 200, result преобразуется в строку с помощью CharArrayToString; ошибка фиксируется через Print и FileWriteString, после чего возвращается сообщение об ошибке. В противном случае ответ после преобразования в массив символов разбирается в объект JsonValue с помощью DeserializeFromArray. При ошибке разбора сообщение об ошибке фиксируется в логе и возвращается. С помощью FindChildByKey проверяется наличие ключа error; при его наличии сообщение об ошибке фиксируется в логе и возвращается.

Наконец, ответ ИИ извлекается из jsonObject['choices'][0]['message']['content'] с помощью ToString. Экранированные символы переноса строки заменяются через StringReplace, пробельные символы удаляются функциями StringTrimLeft и StringTrimRight. Если содержимое не пустое, оно возвращается; в противном случае ошибка фиксируется в журнале терминала и возвращается. Эта функция позволит программе отправлять запросы ChatGPT и обрабатывать ответы для отображения. Теперь необходимо отправить первое сообщение. Необходимо реализовать функцию обработчика событий OnChartEvent, чтобы отслеживать действия пользователя и реагировать на них.

//+------------------------------------------------------------------+
//| Chart event handler                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Event handler
   if(id == CHARTEVENT_OBJECT_CLICK && sparam == "ChatGPT_SubmitButton") { //--- Button click
      string prompt = (string)ObjectGetString(0, "ChatGPT_InputEdit", OBJPROP_TEXT); //--- Get input
      if(StringLen(prompt) > 0) {                            //--- If not empty
         string response = GetChatGPTResponse(prompt);       //--- Get AI response
         Print("User: " + prompt);                           //--- Print user
         Print("AI: " + response);                           //--- Print AI
         conversationHistory += "You: " + prompt + "\nAI: " + response + "\n\n"; //--- Append history
         ObjectSetString(0, "ChatGPT_InputEdit", OBJPROP_TEXT, ""); //--- Clear input
         UpdateResponseDisplay();                            //--- Update display
         FileWriteString(logFileHandle, "Prompt: " + prompt + " | Response: " + response + " | Time: " + TimeToString(TimeCurrent()) + "\n"); //--- Log
         ChartRedraw();                                      //--- Redraw
      }
   }
}

Здесь повышается интерактивность программы за счет реализации функции OnChartEvent для обработки действий пользователя на графике. Сначала проверяется, равно ли id значению CHARTEVENT_OBJECT_CLICK и равно ли sparam значению "ChatGPT_SubmitButton", что указывает на нажатие кнопки отправки. Если условие выполняется, ввод пользователя извлекается из текстового поля ChatGPT_InputEdit с помощью ObjectGetString и OBJPROP_TEXT и сохраняется в переменную prompt.

Затем, если prompt не пустой, что проверяется с помощью StringLen, вызывается GetChatGPTResponse для получения ответа ИИ. Промпт пользователя и ответ ИИ записываются в лог и добавляются в conversationHistory с метками "You:" и "AI:", разделенными символами переноса строки. Поле ввода очищается через ObjectSetString пустой строкой; UpdateResponseDisplay обновляет отображение графика; взаимодействие с отметкой времени фиксируется через FileWriteString в logFileHandle и TimeToString; затем график обновляется. Эта реализация позволяет программе обрабатывать промпты и интерактивно отображать ответы ИИ, завершая основную функциональность панели управления. После компиляции получается следующий результат.

OUR FIRST PROMPT; HELLO WORLD

На изображении видно, что модель ИИ успешно интегрирована в программу. Откроем файл лога и проверим записанные данные. Файл находится в папке общих файлов, как показано ниже. Щелкните правой кнопкой мыши и следуйте указаниям, чтобы открыть файл. См. ниже.

LOG FILE

При открытии файла получается следующий результат.

LOG FILE OUTCOME

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {                                               //--- Initialization
   button_darker_bg = DarkenColor(button_original_bg);       //--- Set darker button color
   logFileHandle = FileOpen(LogFileName, FILE_READ | FILE_WRITE | FILE_TXT); //--- Open log
   if(logFileHandle == INVALID_HANDLE) {                     //--- Check handle
      Print("Failed to open log file: ", GetLastError());    //--- Print error
      return(INIT_FAILED);                                   //--- Fail init
   }
   FileSeek(logFileHandle, 0, SEEK_END);                     //--- Seek end
   FileWriteString(logFileHandle, "EA Initialized at " + TimeToString(TimeCurrent()) + "\n"); //--- Log init
   CreateDashboard();                                        //--- Create UI
   UpdateResponseDisplay();                                  //--- Update display
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);         //--- Enable mouse events
   return(INIT_SUCCEEDED);                                   //--- Success
}

Для включения событий движения мыши на графике с помощью ChartSetInteger соответствующему свойству присваивается true. Для наглядности функция выделена. Затем мы переходим к функции OnChartEvent  и добавляем логику обработки движения мыши.

//+------------------------------------------------------------------+
//| Chart event handler                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Event handler
   if(id == CHARTEVENT_OBJECT_CLICK && sparam == "ChatGPT_SubmitButton") { //--- Button click
      string prompt = (string)ObjectGetString(0, "ChatGPT_InputEdit", OBJPROP_TEXT); //--- Get input
      if(StringLen(prompt) > 0) {                            //--- If not empty
         string response = GetChatGPTResponse(prompt);       //--- Get AI response
         Print("User: " + prompt);                           //--- Print user
         Print("AI: " + response);                           //--- Print AI
         conversationHistory += "You: " + prompt + "\nAI: " + response + "\n\n"; //--- Append history
         ObjectSetString(0, "ChatGPT_InputEdit", OBJPROP_TEXT, ""); //--- Clear input
         UpdateResponseDisplay();                            //--- Update display
         FileWriteString(logFileHandle, "Prompt: " + prompt + " | Response: " + response + " | Time: " + TimeToString(TimeCurrent()) + "\n"); //--- Log
         ChartRedraw();                                      //--- Redraw
      }
   } else if(id == CHARTEVENT_MOUSE_MOVE) {                  //--- Mouse move
      int mouseX = (int)lparam;                              //--- X coord
      int mouseY = (int)dparam;                              //--- Y coord
      bool isOver = (mouseX >= 430 && mouseX <= 530 && mouseY >= 20 && mouseY <= 60); //--- Check hover
      if(isOver && !button_hover) {                          //--- Enter hover
         ObjectSetInteger(0, "ChatGPT_SubmitButton", OBJPROP_BGCOLOR, button_darker_bg); //--- Darken
         button_hover = true;                                //--- Set flag
         ChartRedraw();                                      //--- Redraw
      } else if(!isOver && button_hover) {                   //--- Exit hover
         ObjectSetInteger(0, "ChatGPT_SubmitButton", OBJPROP_BGCOLOR, button_original_bg); //--- Restore
         button_hover = false;                               //--- Clear flag
         ChartRedraw();                                      //--- Redraw
      }
   }
}

В функции OnChartEvent реализация завершается добавлением эффектов наведения для пользовательского интерфейса. В блоке else if проверяется, равно ли id значению CHARTEVENT_MOUSE_MOVE, указывающему на движение мыши. Для получения координат lparam приводится к mouseX, а dparam – к mouseY. Затем проверяется, находится ли мышь над кнопкой отправки: mouseX должно быть между 430 и 530, а mouseY – между 20 и 60. Результат сохраняется в переменную isOver.

Если isOver равно true, а button_hover равно false, то есть мышь вошла в область кнопки, ObjectSetInteger задает цвет фона button_darker_bg для объекта ChatGPT_SubmitButton, чтобы создать эффект наведения. Затем button_hover присваивается true, а график обновляется. И наоборот, если isOver равно false, а button_hover равно true, то есть мышь покинула область кнопки, ObjectSetInteger восстанавливает фон кнопки button_original_bg. Затем button_hover принимает значение false, а ChartRedraw обновляет график. При наведении на кнопку получается следующий результат.

MOUSE HOVER BUTTON EFFECT

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


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

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

COMPLETE TEST GIF


Заключение

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

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

Прикрепленные файлы |
a._ChatGPT_AI_EA.mq5 (116.72 KB)
Нейросети в трейдинге: Двухуровневая адаптация торговой политики (Банк навыков) Нейросети в трейдинге: Двухуровневая адаптация торговой политики (Банк навыков)
Статья описывает переход от одного навыка к управляемому банку, где опыт хранится как векторные латентные коррекции и извлекается по контексту. Разбираем фильтры сходства, подтверждение Candidate, правила вытеснения с учётом Utility и защитные периоды. Реализация на MQL5 и OpenCL выполняет ключевые вычисления на GPU, что даёт воспроизводимый pipeline retrieval–update без смешения конфликтующих режимов рынка.
Создание HTML-дашборда для Тестера стратегий и анализа проп-фирменных испытаний в MQL5 Создание HTML-дашборда для Тестера стратегий и анализа проп-фирменных испытаний в MQL5
В этой статье показано, как создать многоразовый модуль оценки проп-фирм для советников MQL5 и экспортировать результаты на HTML-дашборд. Модуль отслеживает баланс и капитал во время тестирования стратегий на исторических данных, имитирует одиночные или скользящие серии испытаний, проверяет целевую прибыль, дневную и общую просадку, а также минимальное количество торговых дней, после чего выводит как итоговый отчет, так и отчет в HTML-формате, который можно открыть в браузере.
Изучение стандартной библиотеки MQL5 (Часть 12): Мультитаймфреймный дашборд на основе составного показателя Изучение стандартной библиотеки MQL5 (Часть 12): Мультитаймфреймный дашборд на основе составного показателя
В статье реализован CMultiTimeframeMatrix, переиспользуемый дашборд, который сопоставляет символы с таймфреймами и отображает числовой показатель с цветовой кодировкой. Показатель объединяет тренд и импульс с вычетом волатильности, обновляется по таймеру и учитывает ограничения производительности. Вы узнаете, как создавать пользовательский интерфейс с помощью CAppDialog/CLabel, вычислять метрики с помощью CMatrixDouble и встраивать компонент в простой советник для получения целостной картины рынка в реальном времени.
Изучение стандартной библиотеки MQL5 (Часть 11): Как создать матричный индикатор структуры рынка в MQL5 Изучение стандартной библиотеки MQL5 (Часть 11): Как создать матричный индикатор структуры рынка в MQL5
Научитесь проектировать индикатор в MQL5, который преобразует тренд, импульс и волатильность в единый сырой показатель, используя matrix.mqh (ALGLIB). В статье рассматривается осциллятор с отдельным окном для проверки основной математики, а затем индикатор для основного графика, который отображает стрелки сигналов на покупку и продажу без перерисовки, когда показатель пересекает заданные пользователем пороговые значения. Дополнительный фильтр на основе долгосрочной EMA, минимальный интервал в барах между сигналами и встроенные оповещения делают инструмент практичным для торговли в реальном времени.