Создание торговых систем с искусственным интеллектом на MQL5 (Часть 7): Дальнейшая модульная реорганизация и автоматизация торговли
Введение
В предыдущей статье (часть 6) мы доработали торговую систему с ИИ на языке MetaQuotes Language 5 (MQL5): добавили функционал для удаления чатов во всплывающих окнах, поиск с фильтрацией результатов и прокруткой, динамическое развертывание боковой панели и улучшенное управление историей чатов. В части 7 сосредоточимся на дальнейшей модульной реорганизации и автоматизации торговли. В этом обновлении компоненты пользовательского интерфейса (UI) вынесены в отдельный подключаемый файл, что делает код чище и структурированнее. Добавлены автоматическая проверка сигналов на новых барах и автоматическое исполнение сделок по сигналам BUY/SELL, сгенерированным ИИ, на основе разобранных JSON-ответов с уровнями входа, SL и TP. Паттерны, такие как поглощение или дивергенции, отображаются стрелками, линиями и метками; также добавлено управление температурой ответов ИИ. Мы рассмотрим следующие темы:
- Преимущества модульной реорганизации и автоматизированной торговли в системе с ИИ
- Реализация на MQL5
- Тестирование на исторических данных
- Заключение
В результате вы получите более модульную систему с ИИ и органично встроенной автоматизированной торговлей, готовую к работе на реальном счете.
Преимущества модульной реорганизации и автоматизированной торговли в системе с ИИ
Модульная реорганизация в торговых системах с ИИ разделяет сложный код на независимые повторно используемые модули: обработчики интерфейса, обработку данных и логические функции. Их можно разрабатывать, тестировать и масштабировать по отдельности. В результате код становится чище и структурированнее, его проще сопровождать, ошибок становится меньше, а новые возможности можно добавлять быстрее без нарушения работы всей системы – принцип, который вам к этому моменту уже хорошо знаком. Автоматизированная торговля развивает этот подход: ИИ не только анализирует данные, но и напрямую исполняет торговые приказы. Система разбирает сигналы с директивами на покупку или продажу и уровнями входа, SL и TP, применяет параметры риска и работает круглосуточно без эмоциональной предвзятости. Это повышает стабильность и эффективность работы на реальном рынке. Вместе эти решения повышают эффективность системы. Модульная архитектура упрощает интеграцию новых возможностей, например динамического интерфейса или автоматических сигналов, а автоматизация позволяет сосредоточиться на доработке стратегии.
Важный параметр ответов ИИ – температура, которая определяет степень вариативности. Низкие значения, например 0,0, дают детерминированные структурированные ответы, такие как строгий JSON для торговых сигналов с точными директивами BUY/SELL/NONE и ценами. Более высокие значения, например 1,0, подходят для обычных чатов, где вариативность помогает в решении задач. Некоторые промпты используются только для инициирования сделок без развернутых ответов, что упрощает исполнение. В предыдущих частях мы уже отправляли данные и извлекали результаты из ответов. Нам нужны прямые, не искаженные дополнительными пояснениями ответы, например "BUY SIGNAL".
Для этого в зависимости от задачи будут попеременно использоваться разные значения температуры, чтобы разделять отфильтрованные и неотфильтрованные ответы. Такая настройка обеспечит обработку разных типов запросов: более фактических и строгих для автоматизации для автоматизации или более свободных и вариативных для аналитических задач. Ниже приведены уровни температуры больших языковых моделей (LLM) и варианты их применения.
| Значение температуры | Диапазон | Характеристики | Области применения |
|---|---|---|---|
| Низкое | 0.1-0.5 | Высокая точность и надежная работа | Задачи, не требующие творческого подхода, включая обобщение данных |
| Среднее | 0.5-1.0 | Сочетает творческий подход и надежность; обычно устанавливается по умолчанию | Типичные задачи, которым полезна определенная гибкость: программирование и профессиональное написание текстов. |
| Высокое | 1.0-2.0 | Очень творческий результат, но с повышенным риском появления вымышленной информации. | Творческие задачи: мозговые штурмы, написание художественных текстов и генерация изображений. |
Для этого компоненты интерфейса, включая полосы прокрутки и всплывающие окна, будут вынесены в подключаемый файл. Также будут добавлены динамическое развертывание боковой панели, удаление чатов в боковой панели, большом окне истории и окне поиска, поиск с фильтрацией и прокруткой, автоматическая проверка сигналов на новых барах и разбор JSON-ответов ИИ с директивами BUY/SELL/NONE и уровнями входа, SL и TP. Паттерны, включая поглощение и дивергенцию, будут отображаться стрелками, линиями и метками; будут добавлены автоматизированная торговля с заданным размером лота и магическим числом, настройка температуры ответов ИИ и сохранение чатов. Ниже схематично показаны поставленные цели.

Реализация на MQL5
Для реализации обновлений сначала вынесем компоненты UI в подключаемый файл "AI UI COMPONENTS.mqh". Такое имя согласуется с именами остальных подключаемых файлов и упрощает поиск. Вместе с ними перенесем необходимые директивы include, входные параметры и константы, чтобы не нарушить структуру проекта. Фрагмент заголовка будет выглядеть так.
//+------------------------------------------------------------------+ //| AI UI COMPONENTS.mqh | //| 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 strict #include "AI CREATE OBJECTS FNS.mqh" enum ENUM_SCROLLBAR_MODE { SCROLL_DYNAMIC_ALWAYS, SCROLL_DYNAMIC_HOVER, SCROLL_WHEEL_ONLY }; input ENUM_SCROLLBAR_MODE ScrollbarMode = SCROLL_DYNAMIC_HOVER; //---
Теперь разделим объявления ресурсов: в основном файле останутся сами ресурсы, а в подключаемом файле – их определения. Это избавит от необходимости объявлять все переменные как external. Их можно объявить и так, но это приведет к ненужному дублированию. Теперь переменные распределены между файлами следующим образом.
// MAIN FILE //+------------------------------------------------------------------+ //| AI ChatGPT EA Part 7.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 #property icon "1. Forex Algo-Trader.ico" #resource "AI MQL5.bmp" #resource "AI LOGO.bmp" #resource "AI NEW CHAT.bmp" #resource "AI CLEAR.bmp" #resource "AI HISTORY.bmp" #include "AI JSON FILE.mqh" #include "AI UI COMPONENTS.mqh" // INCLUDE FILE #define resourceImg "::AI MQL5.bmp" #define resourceImgLogo "::AI LOGO.bmp" #define resourceNewChat "::AI NEW CHAT.bmp" #define resourceClear "::AI CLEAR.bmp" #define resourceHistory "::AI HISTORY.bmp"
В файл элементов UI потребуется внести изменения. Сначала добавим дополнительную кнопку "Get Signal", по клику на которую можно будет получать обновленные результаты для реальной торговли. Также переработаем кнопки, чтобы сделать их независимыми. Тогда кнопки в нижней части интерфейса смогут иметь разные размеры и свойства в зависимости от того, используются ли компактные значки или стандартный текст, поскольку текущая компоновка перегружена.
int chartButtonW = 50; int signalButtonW = 110; int sendButtonW = 50; // Smaller for icon-only color signal_button_bg = clrLightBlue; color signal_button_darker_bg; bool signal_hover = false;
Здесь объявляются глобальные переменные для размеров кнопок интерфейса и эффектов наведения: chartButtonW получает значение 50 вместо 140 и задает ширину кнопки графика, signalButtonW получает значение 110 для кнопки сигнала, а sendButtonW – 50 вместо 140 для компактной кнопки отправки только со значком. Для обычного фона кнопки сигнала signal_button_bg задается светло-голубой цвет. Для состояния наведения инициализируется signal_button_darker_bg, значение которого будет вычислено позднее, а signal_hover получает значение false для отслеживания взаимодействия мыши с кнопкой. Еще одно изменение – уменьшение смещения по оси Y с 30 до 20 пикселей, чтобы кнопка располагалась немного выше. При необходимости можно оставить значение по умолчанию. Нам показалось, что в интерфейсе есть свободное пространство, которое можно использовать.
// BEFORE int g_mainY = 30; // AFTER int g_mainY = 20;
Теперь можно обновить функцию создания панели, чтобы изменения применились к новым и измененным компонентам.
void CreateDashboard() { objCount = 0; g_mainHeight = g_headerHeight + 2 * g_padding + g_displayHeight + g_footerHeight; int displayX = g_mainContentX + g_sidePadding; int displayY = g_mainY + g_headerHeight + g_padding; int displayW = g_mainWidth - 2 * g_sidePadding; int footerY = displayY + g_displayHeight + g_padding; int promptY = footerY + g_margin; int buttonsY = promptY + g_promptHeight + g_margin; int chartX = g_mainContentX + g_sidePadding; int sendX = g_mainContentX + g_mainWidth - g_sidePadding - sendButtonW; dashboardObjects[objCount++] = "ChatGPT_MainContainer"; createRecLabel("ChatGPT_MainContainer", g_mainContentX, g_mainY, g_mainWidth, g_mainHeight, clrWhite, 1, clrLightGray); dashboardObjects[objCount++] = "ChatGPT_HeaderBg"; createRecLabel("ChatGPT_HeaderBg", g_mainContentX, g_mainY, g_mainWidth, g_headerHeight, clrWhiteSmoke, 0, clrNONE); string logo_resource = (StringLen(g_scaled_image_resource) > 0) ? g_scaled_image_resource : resourceImg; dashboardObjects[objCount++] = "ChatGPT_HeaderLogo"; createBitmapLabel("ChatGPT_HeaderLogo", g_mainContentX + g_sidePadding, g_mainY + (g_headerHeight - 40)/2, 104, 40, logo_resource, clrWhite, CORNER_LEFT_UPPER); string title = "ChatGPT AI EA"; string titleFont = "Arial Rounded MT Bold"; int titleSize = 14; TextSetFont(titleFont, titleSize); uint titleWid, titleHei; TextGetSize(title, titleWid, titleHei); int titleY = g_mainY + (g_headerHeight - (int)titleHei) / 2 - 4; int titleX = g_mainContentX + g_sidePadding + 104 + 5; dashboardObjects[objCount++] = "ChatGPT_TitleLabel"; createLabel("ChatGPT_TitleLabel", titleX, titleY, title, clrDarkSlateGray, titleSize, titleFont, CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER); string dateStr = TimeToString(TimeTradeServer(), TIME_MINUTES); string dateFont = "Arial"; int dateSize = 12; TextSetFont(dateFont, dateSize); uint dateWid, dateHei; TextGetSize(dateStr, dateWid, dateHei); int dateX = g_mainContentX + g_mainWidth / 2 - (int)(dateWid / 2) + 20; int dateY = g_mainY + (g_headerHeight - (int)dateHei) / 2 - 4; dashboardObjects[objCount++] = "ChatGPT_DateLabel"; createLabel("ChatGPT_DateLabel", dateX, dateY, dateStr, clrSlateGray, dateSize, dateFont, CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER); int closeWidth = 32; int closeX = g_mainContentX + g_mainWidth - closeWidth - g_sidePadding; int closeY = g_mainY + 4; dashboardObjects[objCount++] = "ChatGPT_CloseButton"; createButton("ChatGPT_CloseButton", closeX, closeY, closeWidth, g_headerHeight - 8, "r", clrRed, 20, close_original_bg, clrWhiteSmoke, "Webdings"); dashboardObjects[objCount++] = "ChatGPT_ResponseBg"; createRecLabel("ChatGPT_ResponseBg", displayX, displayY, displayW, g_displayHeight, clrWhite, 1, clrGainsboro, BORDER_FLAT, STYLE_SOLID); dashboardObjects[objCount++] = "ChatGPT_FooterBg"; createRecLabel("ChatGPT_FooterBg", g_mainContentX, footerY, g_mainWidth, g_footerHeight, clrGainsboro, 0, clrNONE); dashboardObjects[objCount++] = "ChatGPT_PromptBg"; createRecLabel("ChatGPT_PromptBg", displayX, promptY, displayW, g_promptHeight, g_promptBg, 1, g_promptBg, BORDER_FLAT, STYLE_SOLID); int editY = promptY + g_promptHeight - g_editHeight - 5; int editX = displayX + g_textPadding; g_editW = displayW - 2 * g_textPadding; dashboardObjects[objCount++] = "ChatGPT_PromptEdit"; createEdit("ChatGPT_PromptEdit", editX, editY, g_editW, g_editHeight, "", clrBlack, 13, DarkenColor(g_promptBg,0.93), DarkenColor(g_promptBg,0.87),"Calibri"); ObjectSetInteger(0, "ChatGPT_PromptEdit", OBJPROP_BORDER_TYPE, BORDER_FLAT); dashboardObjects[objCount++] = "ChatGPT_GetChartButton"; createButton("ChatGPT_GetChartButton", chartX, buttonsY, chartButtonW, g_buttonHeight, "?", clrBlack, 25, chart_button_bg, clrDarkGreen, "Wingdings"); int signalX = chartX + chartButtonW + 10; createButton("ChatGPT_GetSignalButton", signalX, buttonsY, signalButtonW, g_buttonHeight, "Get Signal", clrBlack, 11, signal_button_bg, clrDarkBlue); dashboardObjects[objCount++] = "ChatGPT_GetSignalButton"; dashboardObjects[objCount++] = "ChatGPT_SendPromptButton"; createButton("ChatGPT_SendPromptButton", sendX, buttonsY, sendButtonW, g_buttonHeight, "@", clrWhite, 23, button_original_bg, clrDarkBlue, "Wingdings 3"); ChartRedraw(); }
В функции CreateDashboard, которая формирует основные элементы интерфейса чата с ИИ – заголовки, области отображения, запросы и кнопки, – обновляется ширина кнопок. Для кнопки закрытия задается ширина 32; координата X рассчитывается как координата основной области плюс ее ширина минус ширина кнопки и боковой отступ, а координата Y – как g_mainY + 4. Кнопка создается с красным символом r размером 20, фоном и рамкой цвета white smoke, шрифтом Webdings. Так убирается малозаметная надпись "Close", использовавшаяся ранее. Конкретные изменения выделены для наглядности.
Затем создаются три кнопки: кнопка получения данных графика с черным символом ? размером 25, светло-зеленым фоном, темно-зеленой рамкой и шрифтом Wingdings; кнопка сигнала с текстом "Get Signal" черного цвета размером 11, светло-голубым фоном и темно-синей рамкой; кнопка отправки запроса с белым символом @ размером 23, фоном royal blue, темно-синей рамкой и шрифтом Wingdings 3. Через ChartRedraw перерисовывается график, чтобы отобразить все элементы. В таблице ниже показаны символьные шрифты и соответствующие значения; при необходимости их можно подобрать и заменить.

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

После изменения основной панели нужно доработать функцию, обновляющую ее позиции при изменении боковой панели. Это обеспечит корректную работу независимых кнопок.
void UpdateDashboardPositions() { ObjectSetInteger(0, "ChatGPT_DashboardBg", OBJPROP_XSIZE, g_dashboardWidth); ObjectSetInteger(0, "ChatGPT_SidebarBg", OBJPROP_XSIZE, g_sidebarWidth - 2 - 1); int diff = g_mainContentX - (g_dashboardX + (sidebarExpanded ? contractedSidebarWidth : expandedSidebarWidth)); for (int i = 0; i < objCount; i++) { string obj = dashboardObjects[i]; if (ObjectFind(0, obj) >= 0) { long curX = ObjectGetInteger(0, obj, OBJPROP_XDISTANCE); ObjectSetInteger(0, obj, OBJPROP_XDISTANCE, curX + diff); } } uint date_wid, date_hei; string dateStr = TimeToString(TimeTradeServer(), TIME_MINUTES); TextSetFont("Arial", 12); TextGetSize(dateStr, date_wid, date_hei); int dateX = g_mainContentX + g_mainWidth / 2 - (int)(date_wid / 2) + 20; int dateY = (int)ObjectGetInteger(0, "ChatGPT_DateLabel", OBJPROP_YDISTANCE); ObjectSetInteger(0, "ChatGPT_DateLabel", OBJPROP_XDISTANCE, dateX); int closeWidth = 32; int closeX = g_mainContentX + g_mainWidth - closeWidth - g_sidePadding; ObjectSetInteger(0, "ChatGPT_CloseButton", OBJPROP_XDISTANCE, closeX); int promptW = g_mainWidth - 2 * g_sidePadding; int editX = g_mainContentX + g_sidePadding + g_textPadding; g_editW = promptW - 2 * g_textPadding; ObjectSetInteger(0, "ChatGPT_PromptEdit", OBJPROP_XDISTANCE, editX); ObjectSetInteger(0, "ChatGPT_PromptEdit", OBJPROP_XSIZE, g_editW); int chartX = g_mainContentX + g_sidePadding; int sendX = g_mainContentX + g_mainWidth - g_sidePadding - sendButtonW; ObjectSetInteger(0, "ChatGPT_GetChartButton", OBJPROP_XDISTANCE, chartX); ObjectSetInteger(0, "ChatGPT_SendPromptButton", OBJPROP_XDISTANCE, sendX); int signalX = chartX + chartButtonW + 10; ObjectSetInteger(0, "ChatGPT_GetSignalButton", OBJPROP_XDISTANCE, signalX); int displayX = g_mainContentX + g_sidePadding; int displayW = g_mainWidth - 2 * g_sidePadding; ObjectSetInteger(0, "ChatGPT_ResponseBg", OBJPROP_XDISTANCE, displayX); ObjectSetInteger(0, "ChatGPT_ResponseBg", OBJPROP_XSIZE, displayW); ObjectSetInteger(0, "ChatGPT_PromptBg", OBJPROP_XDISTANCE, displayX); ObjectSetInteger(0, "ChatGPT_PromptBg", OBJPROP_XSIZE, displayW); int footerY = g_mainY + g_headerHeight + g_padding + g_displayHeight + g_padding; ObjectSetInteger(0, "ChatGPT_FooterBg", OBJPROP_XDISTANCE, g_mainContentX); ObjectSetInteger(0, "ChatGPT_FooterBg", OBJPROP_XSIZE, g_mainWidth); if (ObjectFind(0, "ChatGPT_PromptPlaceholder") >= 0) { int labelY = (int)ObjectGetInteger(0, "ChatGPT_PromptPlaceholder", OBJPROP_YDISTANCE); ObjectSetInteger(0, "ChatGPT_PromptPlaceholder", OBJPROP_XDISTANCE, editX + 2); } if (scroll_visible) { int displayY = g_mainY + g_headerHeight + g_padding; int scrollbar_x = displayX + displayW - 16; int button_size = 16; ObjectSetInteger(0, SCROLL_LEADER, OBJPROP_XDISTANCE, scrollbar_x); ObjectSetInteger(0, SCROLL_UP_REC, OBJPROP_XDISTANCE, scrollbar_x); ObjectSetInteger(0, SCROLL_UP_LABEL, OBJPROP_XDISTANCE, scrollbar_x + 2); ObjectSetInteger(0, SCROLL_DOWN_REC, OBJPROP_XDISTANCE, scrollbar_x); ObjectSetInteger(0, SCROLL_DOWN_LABEL, OBJPROP_XDISTANCE, scrollbar_x + 2); ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_XDISTANCE, scrollbar_x); UpdateSliderPosition(); } if (p_scroll_visible) { int promptX = g_mainContentX + g_sidePadding; int promptY = footerY + g_margin; int promptW = g_mainWidth - 2 * g_sidePadding; int scrollbar_x = promptX + promptW - 16; int button_size = 16; ObjectSetInteger(0, P_SCROLL_LEADER, OBJPROP_XDISTANCE, scrollbar_x); ObjectSetInteger(0, P_SCROLL_UP_REC, OBJPROP_XDISTANCE, scrollbar_x); ObjectSetInteger(0, P_SCROLL_UP_LABEL, OBJPROP_XDISTANCE, scrollbar_x + 2); ObjectSetInteger(0, P_SCROLL_DOWN_REC, OBJPROP_XDISTANCE, scrollbar_x); ObjectSetInteger(0, P_SCROLL_DOWN_LABEL, OBJPROP_XDISTANCE, scrollbar_x + 2); ObjectSetInteger(0, P_SCROLL_SLIDER, OBJPROP_XDISTANCE, scrollbar_x); UpdatePromptSliderPosition(); } }
В функции UpdateDashboardPositions координата X кнопки получения данных графика задается переменной chartX, кнопки отправки запроса – sendX, а кнопки сигнала – выражением signalX = chartX + chartButtonW + 10. Для фона ответа, фона запроса и нижней панели обновляются координата X и размер в соответствии с новыми значениями displayX и displayW = g_mainWidth - 2 * g_sidePadding. Для нижней панели координата X задается g_mainContentX, а ширина – g_mainWidth. Если существует метка-заполнитель запроса, считывается ее координата Y, а координата X задается выражением editX + 2. Остальная логика не меняется. На этом обновление компонентов интерфейса завершено. Остальные важные изменения для автоматизации торговли вносятся в основной файл, как показано ниже.
input bool DeleteLogsOnChatDelete = true; // Clear logs when deleting chats? input bool AutoTrade = true; // Automatically open trades on AI signals input double LotSize = 0.01; // Lot size for auto-trades input int MagicNumber = 12345; // Magic number for trades input bool EnableAutoSignal = false; // Enable automatic signal check on new closed bar CTrade obj_Trade;
В основном файле сначала определяются входные параметры для управления журналированием и торговыми операциями: DeleteLogsOnChatDelete – логический параметр со значением true по умолчанию для дополнительного удаления связанных записей журнала вместе с чатом. AutoTrade со значением true включает автоматическое исполнение торговых приказов по сигналам ИИ; LotSize со значением 0,01 задает фиксированный объем сделки. MagicNumber со значением 12345 служит для уникальной идентификации сделок; это произвольное значение. EnableAutoSignal со значением false включает или выключает автоматическую проверку сигналов на каждом новом закрытом баре. Затем obj_Trade объявляется как глобальный экземпляр класса CTrade из подключенной торговой библиотеки. Он используется для операций с торговыми приказами, например для открытия покупок и продаж с заданным объемом, уровнями и комментариями. Теперь окно входных параметров выглядит так.

После подготовки торговой логики при инициализации программы можно задать магическое число для идентификации сделок.
int OnInit() { obj_Trade.SetExpertMagicNumber(MagicNumber); //--- OTHER INIT LOGIC REMAINS THE SAME }
В обработчике событий OnInit задается магическое число советника, чтобы уникально идентифицировать все его сделки. Теперь определим новые функции для получения данных графика, на основе которых ИИ будет формировать заданные торговые сигналы. Сначала добавим функцию получения данных графика. Сохранять эти данные не требуется, поскольку они используются только один раз.
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ string GetChartDataString() { string symbol = Symbol(); ENUM_TIMEFRAMES tf = (ENUM_TIMEFRAMES)_Period; string timeframe = PeriodToString(tf); long visibleBarsLong = ChartGetInteger(0, CHART_VISIBLE_BARS); int visibleBars = (int)visibleBarsLong; MqlRates rates[]; int copied = CopyRates(symbol, tf, 1, MaxChartBars, rates); if (copied != MaxChartBars) { Print("Failed to copy rates: ", GetLastError()); return ""; } ArraySetAsSeries(rates, true); string data = "Chart Details: Symbol=" + symbol + ", Timeframe=" + timeframe + ", Visible Bars=" + IntegerToString(visibleBars) + "\n"; data += "Recent Bars Data (Bar 1 is latest COMPLETE bar):\n"; for (int i = 0; i < copied; i++) { data += "Bar " + IntegerToString(i + 1) + ": Date=" + TimeToString(rates[i].time, TIME_DATE | TIME_MINUTES) + ", Open=" + DoubleToString(rates[i].open, _Digits) + ", High=" + DoubleToString(rates[i].high, _Digits) + ", Low=" + DoubleToString(rates[i].low, _Digits) + ", Close=" + DoubleToString(rates[i].close, _Digits) + ", Volume=" + IntegerToString((int)rates[i].tick_volume) + "\n"; } return data; }
Здесь определяется функция GetChartDataString, формирующая строку с текущими параметрами графика и данными последних баров. Эту строку можно добавлять к ИИ-промптам, чтобы ответы учитывали контекст. Сначала функция Symbol возвращает торговый символ, который сохраняется в переменную symbol. Затем значение _Period приводится к типу ENUM_TIMEFRAMES и сохраняется в tf, а PeriodToString преобразует его в строку таймфрейма, например "H1", в переменной timeframe. Ниже приведен фрагмент этой функции.
string PeriodToString(ENUM_TIMEFRAMES period) { switch(period) { case PERIOD_M1: return "M1"; case PERIOD_M5: return "M5"; case PERIOD_M15: return "M15"; case PERIOD_M30: return "M30"; case PERIOD_H1: return "H1"; case PERIOD_H4: return "H4"; case PERIOD_D1: return "D1"; case PERIOD_W1: return "W1"; case PERIOD_MN1: return "MN1"; default: return IntegerToString(period); } }
Количество видимых баров на графике через ChartGetInteger и свойство CHART_VISIBLE_BARS сохраняется в visibleBarsLong, после чего приводится к типу int и записывается в visibleBars. Объявляется массив rates[] типа MqlRates, в который через CopyRates копируются последние завершенные бары в количестве MaxChartBars, начиная со смещения 1. Если copied не равно MaxChartBars, ошибка фиксируется через Print и GetLastError, а функция возвращает пустую строку.
Через ArraySetAsSeries массиву задается значение true, поэтому индекс 0 соответствует самому новому бару. Формирование строки данных начинается с "Chart Details: Symbol=...", содержащей символ, таймфрейм и число видимых баров. Затем добавляется строка "Recent Bars Data (Bar 1 is latest COMPLETE bar):". Далее цикл обрабатывает бары из copied: для каждого i добавляются "Bar " и i + 1, дата и время с точностью до минут через TimeToString, цены открытия, максимума, минимума и закрытия с точностью _Digits через DoubleToString, а также тиковый объем tick_volume как целое число. Сформированная строка data возвращается для использования в запросах. Для отправки этих данных нужно предварительно определить значение температуры, поскольку теперь требуются разные типы ответов. Ниже приведена логика для этой задачи.
// Added temperature param, default to 1.0 (balanced creativity) string GetChatGPTResponse(string prompt, double temperature = 1.0) { string messages = BuildMessagesFromHistory(prompt); Print("Messages JSON: " + messages); FileWrite(logFileHandle, "Messages JSON: " + messages); // Added temperature to request string requestData = "{\"model\":\"" + OpenAI_Model + "\",\"messages\":" + messages + ",\"max_tokens\":" + IntegerToString(MaxResponseLength) + ",\"temperature\":" + DoubleToString(temperature, 2) + "}"; Print("Request data (temp=" + DoubleToString(temperature, 2) + "): " + requestData); FileWrite(logFileHandle, "Request data (temp=" + DoubleToString(temperature, 2) + "): " + requestData); char postData[]; //--- NEXT LOGIC REMAINS SIMILAR }
В функцию GetChatGPTResponse добавляется опциональный параметр температуры со значением 1,0 по умолчанию. Он управляет вариативностью ответа ИИ: низкие значения подходят для детерминированных результатов, например структурированного JSON, а высокие – для более разнообразных ответов. Через BuildMessagesFromHistory и переданный запрос формируется массив сообщений из истории. JSON-сообщения выводятся и записываются в журнал. Затем формируются данные запроса в виде JSON-строки: модель из OpenAI_Model, сообщения, максимальное число токенов из MaxResponseLength и значение температуры, форматируемое DoubleToString с двумя знаками после запятой.
Данные запроса и значение температуры выводятся и записываются в журнал. Затем через StringToCharArray они преобразуются в символьный массив postData; завершающий нулевой символ исключается изменением размера массива. Дальнейшая логика WebRequest для отправки POST-запроса к API остается прежней. Также упростим добавление данных графика к запросам, устранив лишние операции ввода-вывода для более быстрого добавления данных.
void GetAndAppendChartData() { string data = GetChartDataString(); if (StringLen(data) == 0) return; Print("Chart data appended to prompt: \n" + data); FileWrite(logFileHandle, "Chart data appended to prompt: \n" + data); if (StringLen(currentPrompt) > 0) { currentPrompt += "\n"; } currentPrompt += data; DeletePlaceholder(); UpdatePromptDisplay(); p_scroll_pos = MathMax(0, p_total_height - p_visible_height); if (p_scroll_visible) { UpdatePromptSliderPosition(); UpdatePromptButtonColors(); } ChartRedraw(); }
В функцию добавляется фрагмент кода для прямого добавления данных. После доработки функций можно получать ответы и исполнять сделки на их основе. Сначала добавим визуализацию, чтобы было видно, какой торговый сигнал получен.
// Prefix for signal objects (easy to clean up) #define SIGNAL_OBJ_PREFIX "AI_Signal_" // Function to delete previous signal objects void DeleteSignalObjects() { int total = ObjectsTotal(0, 0, OBJ_ARROW | OBJ_HLINE | OBJ_TEXT | OBJ_RECTANGLE | OBJ_TREND); for (int i = total - 1; i >= 0; i--) { string name = ObjectName(0, i, 0, OBJ_ARROW | OBJ_HLINE | OBJ_TEXT | OBJ_RECTANGLE | OBJ_TREND); if (StringFind(name, SIGNAL_OBJ_PREFIX) == 0) { ObjectDelete(0, name); } } ChartRedraw(); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void VisualizeSignal(string signal, string reason, int triggerBar, double entry, double sl, double tp) { // DeleteSignalObjects(); // Comment out to retain objects // Unique ID for this signal to avoid name conflicts static int sigId = 0; sigId++; string prefix = SIGNAL_OBJ_PREFIX + IntegerToString(sigId) + "_"; // Get rates: Start from COMPLETE bars (shift=triggerBar, e.g. 1=last closed) MqlRates rates[]; int barsToCopy = 5; // Increased for multi-bar patterns like Three Soldiers if (CopyRates(Symbol(), Period(), triggerBar, barsToCopy, rates) < barsToCopy) { Print("Failed to get rates for complete bar ", triggerBar); return; } ArraySetAsSeries(rates, true); // rates[0] = trigger bar (complete), rates[1] = previous complete datetime triggerTime = rates[0].time; datetime prevTime = rates[1].time; double triggerHigh = rates[0].high, triggerLow = rates[0].low; double prevHigh = rates[1].high, prevLow = rates[1].low; double triggerOpen = rates[0].open, triggerClose = rates[0].close; double prevOpen = rates[1].open, prevClose = rates[1].close; bool isBuy = (StringFind(signal, "BUY") >= 0); int arrowCode = isBuy ? 233 : 234; color arrowColor = isBuy ? clrGreen : clrRed; double arrowPrice = isBuy ? triggerLow : triggerHigh; int arrowAnchor = isBuy ? ANCHOR_TOP : ANCHOR_BOTTOM; string arrowName = prefix + "Arrow_" + TimeToString(triggerTime); ObjectCreate(0, arrowName, OBJ_ARROW, 0, triggerTime, arrowPrice); ObjectSetInteger(0, arrowName, OBJPROP_ARROWCODE, arrowCode); ObjectSetInteger(0, arrowName, OBJPROP_COLOR, arrowColor); ObjectSetInteger(0, arrowName, OBJPROP_ANCHOR, arrowAnchor); ObjectSetInteger(0, arrowName, OBJPROP_BACK, false); string lowerReason = reason; StringToLower(lowerReason); string patternLabel = reason + " (Bar " + IntegerToString(triggerBar) + ")"; color labelColor = clrBlack; if (StringFind(lowerReason, "engulfing") >= 0) { bool isBullish = (StringFind(lowerReason, "bullish") >= 0) || (StringFind(signal, "BUY") >= 0); color engulfColor = isBullish ? clrDarkBlue : clrDarkRed; ObjectCreate(0, prefix + "EngulfRect", OBJ_RECTANGLE, 0, prevTime, prevLow, triggerTime + (PeriodSeconds() / 2), triggerHigh); ObjectSetInteger(0, prefix + "EngulfRect", OBJPROP_COLOR, engulfColor); ObjectSetInteger(0, prefix + "EngulfRect", OBJPROP_FILL, true); ObjectSetInteger(0, prefix + "EngulfRect", OBJPROP_BACK, true); ObjectSetInteger(0, prefix + "EngulfRect", OBJPROP_WIDTH, 1); patternLabel = (isBullish ? "Bullish " : "Bearish ") + "Engulfing (Bar " + IntegerToString(triggerBar) + ")"; labelColor = clrBlue; } else if (StringFind(lowerReason, "pin bar") >= 0 || StringFind(lowerReason, "hammer") >= 0 || StringFind(lowerReason, "shooting star") >= 0) { color pinColor = isBuy ? clrGreen : clrRed; ObjectCreate(0, prefix + "PinLine", OBJ_TREND, 0, triggerTime, triggerOpen, triggerTime + PeriodSeconds(), triggerClose); ObjectSetInteger(0, prefix + "PinLine", OBJPROP_COLOR, pinColor); ObjectSetInteger(0, prefix + "PinLine", OBJPROP_WIDTH, 2); ObjectSetInteger(0, prefix + "PinLine", OBJPROP_RAY_RIGHT, false); string patName = StringFind(lowerReason, "hammer") >= 0 ? "Hammer" : StringFind(lowerReason, "shooting star") >= 0 ? "Shooting Star" : "Pin Bar"; patternLabel = patName + " (Bar " + IntegerToString(triggerBar) + ")"; labelColor = pinColor; } else if (StringFind(lowerReason, "inside bar") >= 0) { color insideColor = clrYellow; ObjectCreate(0, prefix + "InsideRect", OBJ_RECTANGLE, 0, triggerTime, triggerLow, triggerTime + PeriodSeconds(), triggerHigh); ObjectSetInteger(0, prefix + "InsideRect", OBJPROP_COLOR, insideColor); ObjectSetInteger(0, prefix + "InsideRect", OBJPROP_FILL, false); ObjectSetInteger(0, prefix + "InsideRect", OBJPROP_BACK, true); ObjectSetInteger(0, prefix + "InsideRect", OBJPROP_STYLE, STYLE_DASH); patternLabel = "Inside Bar (Bar " + IntegerToString(triggerBar) + ")"; labelColor = clrOrange; } else if (StringFind(lowerReason, "doji") >= 0) { color dojiColor = clrMagenta; ObjectCreate(0, prefix + "DojiMark", OBJ_TEXT, 0, triggerTime, (triggerHigh + triggerLow)/2); ObjectSetString(0, prefix + "DojiMark", OBJPROP_FONT, "Wingdings"); ObjectSetInteger(0, prefix + "DojiMark", OBJPROP_FONTSIZE, 12); ObjectSetString(0, prefix + "DojiMark", OBJPROP_TEXT, CharToString((uchar)108)); // Cross symbol ObjectSetInteger(0, prefix + "DojiMark", OBJPROP_COLOR, dojiColor); patternLabel = "Doji (Bar " + IntegerToString(triggerBar) + ")"; labelColor = dojiColor; } else if (StringFind(lowerReason, "three white soldiers") >= 0 || StringFind(lowerReason, "three black crows") >= 0) { bool isBullish = StringFind(lowerReason, "white soldiers") >= 0; color soldiersColor = isBullish ? clrLimeGreen : clrCrimson; datetime startTime = rates[2].time; // N-2 ObjectCreate(0, prefix + "SoldiersRect", OBJ_RECTANGLE, 0, startTime, rates[2].low, triggerTime + PeriodSeconds(), rates[0].high); ObjectSetInteger(0, prefix + "SoldiersRect", OBJPROP_COLOR, soldiersColor); ObjectSetInteger(0, prefix + "SoldiersRect", OBJPROP_FILL, true); ObjectSetInteger(0, prefix + "SoldiersRect", OBJPROP_BACK, true); ObjectSetInteger(0, prefix + "SoldiersRect", OBJPROP_WIDTH, 1); patternLabel = (isBullish ? "Three White Soldiers" : "Three Black Crows") + " (Bars " + IntegerToString(triggerBar - 2) + "-" + IntegerToString(triggerBar) + ")"; labelColor = soldiersColor; } else if (StringFind(lowerReason, "double bottom") >= 0 || StringFind(lowerReason, "double top") >= 0) { bool isBottom = StringFind(lowerReason, "bottom") >= 0; double level = isBottom ? MathMin(rates[1].low, triggerLow) : MathMax(rates[1].high, triggerHigh); color doubleColor = isBottom ? clrAqua : clrFuchsia; ObjectCreate(0, prefix + "DoubleLine", OBJ_TREND, 0, rates[1].time, level, triggerTime + PeriodSeconds() * 3, level); ObjectSetInteger(0, prefix + "DoubleLine", OBJPROP_COLOR, doubleColor); ObjectSetInteger(0, prefix + "DoubleLine", OBJPROP_STYLE, STYLE_DOT); ObjectSetInteger(0, prefix + "DoubleLine", OBJPROP_WIDTH, 2); ObjectSetInteger(0, prefix + "DoubleLine", OBJPROP_RAY_RIGHT, true); patternLabel = (isBottom ? "Double Bottom" : "Double Top") + " (Bar " + IntegerToString(triggerBar) + ") at " + DoubleToString(level, _Digits); labelColor = doubleColor; } else if (StringFind(lowerReason, "breakout") >= 0) { double breakLevel = isBuy ? MathMax(prevHigh, triggerHigh) : MathMin(prevLow, triggerLow); ObjectCreate(0, prefix + "BreakLine", OBJ_TREND, 0, prevTime, breakLevel, triggerTime + PeriodSeconds() * 5, breakLevel); ObjectSetInteger(0, prefix + "BreakLine", OBJPROP_COLOR, clrGray); ObjectSetInteger(0, prefix + "BreakLine", OBJPROP_STYLE, STYLE_DOT); ObjectSetInteger(0, prefix + "BreakLine", OBJPROP_WIDTH, 2); ObjectSetInteger(0, prefix + "BreakLine", OBJPROP_RAY_RIGHT, true); patternLabel = "Breakout (Bar " + IntegerToString(triggerBar) + ") at " + DoubleToString(breakLevel, _Digits); labelColor = clrBlue; } else if (StringFind(lowerReason, "divergence") >= 0 || StringFind(lowerReason, "rsi") >= 0) { int rsiHandle = iRSI(Symbol(), Period(), 14, PRICE_CLOSE); double rsiValue[1]; if (CopyBuffer(rsiHandle, 0, triggerBar, 1, rsiValue) == 1) { patternLabel = "RSI Divergence (Bar " + IntegerToString(triggerBar) + ", RSI=" + IntegerToString((int)rsiValue[0]) + ")"; } labelColor = clrDarkBlue; IndicatorRelease(rsiHandle); } else { // Fallback: Simple highlight + short label ObjectCreate(0, prefix + "Highlight", OBJ_RECTANGLE, 0, triggerTime, triggerLow, triggerTime + PeriodSeconds(), triggerHigh); ObjectSetInteger(0, prefix + "Highlight", OBJPROP_COLOR, clrLightGray); ObjectSetInteger(0, prefix + "Highlight", OBJPROP_FILL, true); ObjectSetInteger(0, prefix + "Highlight", OBJPROP_BACK, true); patternLabel = reason + " (Bar " + IntegerToString(triggerBar) + ")"; labelColor = clrBlack; } datetime labelTime = triggerTime; double labelPrice = arrowPrice; int direction = isBuy ? 1 : -1; ENUM_ANCHOR_POINT labelAnchor = (direction > 0) ? ANCHOR_LEFT : ANCHOR_LEFT; double labelAngle = (direction > 0) ? -90 : 90; string labelText = " " + patternLabel; CreateTextLabel(prefix + "PatternLabel", labelTime, labelPrice, labelText, labelColor, labelAnchor, labelAngle); Print("Anchored viz drawn: Arrow at ", (isBuy ? "low" : "high"), ", Label: ", patternLabel); ChartRedraw(); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CreateHLine(string name, double price, color clr, ENUM_LINE_STYLE style, string tooltip) { ObjectCreate(0, name, OBJ_HLINE, 0, 0, price); ObjectSetInteger(0, name, OBJPROP_COLOR, clr); ObjectSetInteger(0, name, OBJPROP_STYLE, style); ObjectSetInteger(0, name, OBJPROP_WIDTH, 1); ObjectSetString(0, name, OBJPROP_TEXT, tooltip); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CreateTextLabel(string name, datetime time, double price, string text, color clr, ENUM_ANCHOR_POINT anchor, double angle = 0) { ObjectCreate(0, name, OBJ_TEXT, 0, time, price); ObjectSetString(0, name, OBJPROP_TEXT, text); ObjectSetInteger(0, name, OBJPROP_COLOR, clr); ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 10); ObjectSetString(0, name, OBJPROP_FONT, "Arial Bold"); ObjectSetInteger(0, name, OBJPROP_ANCHOR, anchor); ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); ObjectSetDouble(0, name, OBJPROP_ANGLE, angle); }
Для визуализации сначала задается константа SIGNAL_OBJ_PREFIX со значением "AI_Signal_". Она помогает идентифицировать объекты графика, связанные с сигналами, и удалять их при очистке. Затем реализуется функция DeleteSignalObjects для удаления ранее созданных объектов сигналов с графика. При необходимости ее можно использовать, однако в данном примере объекты сохраняются. Через ObjectsTotal определяется число объектов типов "стрелка", "горизонтальная линия", "текст", "прямоугольник" и "трендовая линия". В обратном цикле через ObjectName получается имя каждого объекта. Если StringFind возвращает 0, то есть имя начинается с нужного префикса, объект удаляется через ObjectDelete; затем график перерисовывается.
Затем определяется функция VisualizeSignal, которая отображает на графике элементы, соответствующие паттерну торгового сигнала ИИ. Вызов DeleteSignalObjects комментируется, чтобы при необходимости сохранять предыдущие объекты. Статическая переменная sigId создает уникальный префикс, например "AI_Signal_1_". Через CopyRates копируются бары в количестве barsToCopy (в нашем случае – пять), начиная с triggerBar. При ошибке функция завершает работу; затем массив переводится в режим серии, поэтому индекс 0 соответствует триггерному бару. По наличию "BUY" в строке сигнала определяется направление покупки. Задаются код стрелки – 233 для покупки или 234 для продажи, цвет – зеленый или красный, цена – минимум бара для покупки или максимум для продажи, а также точка привязки – сверху или снизу. Затем через ObjectCreate создается объект OBJ_ARROW, для которого задаются код, цвет, точка привязки и размещение на переднем плане.
Через StringToLower значение reason приводится к нижнему регистру и сохраняется в lowerReason. Метка patternLabel формируется из reason и номера бара. Для паттерна "engulfing" по reason или сигналу определяется бычий либо медвежий вариант. Для бычьего поглощения задается темно-синий цвет, для медвежьего – темно-красный. Через ObjectCreate создается OBJ_RECTANGLE, выделяющий два бара: от времени предыдущего бара на минимуме до момента, отстоящего на половину бара вперед, на максимуме. Затем задаются заливка, фоновый режим и ширина 1, после чего обновляется метка.
Для "pin bar", "hammer" или "shooting star" задается зеленый цвет для покупки либо красный для продажи. От цены открытия до цены закрытия создается трендовая линия шириной 2 без правого луча, а метка получает название паттерна. Для "inside bar" задается желтый пунктирный контур прямоугольника без заливки. Для "doji" в середине бара размещается крест шрифта Wingdings цвета magenta. Для "three white soldiers" или "three black crows" создается прямоугольник с заливкой поверх трех баров цвета lime green либо crimson соответственно. Для "double bottom" или "double top" на соответствующем уровне проводится пунктирная горизонтальная линия цвета aqua или fuchsia с продлением вправо. Для "breakout" создается серая пунктирная горизонтальная линия с продлением вправо. Для "divergence" или "RSI" через iRSI и CopyBuffer извлекается значение RSI на триггерном баре. Затем метка обновляется этим значением, а хэндл индикатора освобождается. В остальных случаях на триггерном баре создается светло-серый прямоугольник с заливкой.
Метка паттерна размещается по времени триггерного бара и цене стрелки, с левой точкой привязки и углом -90 для покупки либо 90 для продажи. Затем вызывается CreateTextLabel с черным цветом текста, если иной цвет не задан. Реализуется функция CreateHLine для создания горизонтальной линии: объект OBJ_HLINE создается по заданной цене, после чего ему задаются цвет, стиль, ширина 1 и текст подсказки. Определяется функция CreateTextLabel для создания пользовательского текста: объект OBJ_TEXT создается по времени и цене, затем задаются текст, его цвет, шрифт Arial Bold размером 10, точка привязки, значение false для возможности выделения и угол поворота. Для открытия сделок также определим отдельную функцию.
void OpenTrade(string signal, double entry, double sl, double tp) { ENUM_ORDER_TYPE type = (StringFind(signal, "BUY") >= 0) ? ORDER_TYPE_BUY : (StringFind(signal, "SELL") >= 0) ? ORDER_TYPE_SELL : (ENUM_ORDER_TYPE)-1; if (type == (ENUM_ORDER_TYPE)-1) return; double price = 0; // Market order string comment = "AI Signal: " + signal; int result = obj_Trade.PositionOpen(Symbol(), type, LotSize, price, sl, tp, comment); if (result == -1) { Print("Trade open failed: ", obj_Trade.ResultRetcodeDescription()); Alert("Failed to open trade."); } else { Print("Trade opened: Ticket #", obj_Trade.ResultOrder()); } }
Определяется функция OpenTrade для автоматического исполнения рыночных приказов по разобранным сигналам ИИ. Она обрабатывает приказы на покупку и продажу с заданными параметрами риска. Сначала определяется тип приказа. Если StringFind находит в строке сигнала "BUY", переменной type присваивается ORDER_TYPE_BUY; для "SELL" задается ORDER_TYPE_SELL. По умолчанию устанавливается недопустимое значение перечисления -1; если не найден ни один вариант, функция завершается досрочно. Для рыночного исполнения цена задается равной 0, а комментарий формируется из "AI Signal: " и строки сигнала. Вызывается obj_Trade.PositionOpen с символом, типом, LotSize, ценой 0, sl, tp и комментарием; результат сохраняется. Если получено значение -1, ошибка фиксируется в журнале вместе с описанием кода возврата и выводится предупреждение "Failed to open trade.". При успехе в журнал записывается "Trade opened: Ticket #" и номер приказа из obj_Trade.ResultOrder. Теперь эту логику можно использовать для открытия позиций по обнаруженным сигналам, как показано ниже.
void GetTradeSignal(bool isAuto = false) { string modeStr = isAuto ? " (Auto-mode)" : " (Manual)"; Print("Starting signal analysis" + modeStr); string chartData = GetChartDataString(); if (StringLen(chartData) == 0) { Alert("Failed to get chart data."); return; } string prompt = "You are a JSON-only trading analyst. Respond with EXACTLY one valid JSON object matching this schema. NO OTHER TEXT:\n" "{\n" " \"signal\": \"BUY\", \"SELL\", or \"NONE\",\n" " \"reason\": \"exact pattern e.g. 'bullish engulfing' (only if matches definition)\",\n" " \"trigger_bar\": integer 1-10 (1=most recent COMPLETE bar (Bar 1 in data), 2=older; estimate only if exact match)\n" " \"entry\": decimal price (5 digits),\n" " \"sl\": decimal price,\n" " \"tp\": decimal price\n" "}\n" "Patterns must match exactly (prioritize strongest signals; if multiple, choose one):\n" "[Your existing patterns list here...]\n" "If no exact match, use 'NONE'. Example bullish engulfing on bar 1: {\"signal\":\"BUY\",\"reason\":\"bullish engulfing\",\"trigger_bar\":1,\"entry\":1.10100,\"sl\":1.09900,\"tp\":1.10500}\n" "Data (Bar 1=recent complete):\n" "Remember: Bar 1 is the most recent (rightmost on chart), Bar 10 is oldest (leftmost). Analyze patterns from recent (Bar 1) backward to older bars.\n" + chartData; string response = GetChatGPTResponse(prompt, 0.0); // Strict for JSON Print("Raw AI Response: ", response); // Extract JSON substring (handle extra text like "Here's: {JSON}") int jsonStart = StringFind(response, "{"); int jsonEnd = StringFind(response, "}", jsonStart); if (jsonStart >= 0 && jsonEnd > jsonStart) { response = StringSubstr(response, jsonStart, jsonEnd - jsonStart + 1); // Isolate { ... } } // Parse JSON (primary); fallback to text string signal = "", reason = ""; int triggerBar = 1; double entry = 0, sl = 0, tp = 0; bool parsed = false; JsonValue jsonResp; int index = 0; char charArray[]; int len = StringToCharArray(response, charArray, 0, WHOLE_ARRAY, CP_UTF8); if (jsonResp.DeserializeFromArray(charArray, len, index)) { signal = jsonResp["signal"].ToString(); reason = jsonResp["reason"].ToString(); triggerBar = (int)jsonResp["trigger_bar"].ToInteger(); entry = jsonResp["entry"].ToDouble(); sl = jsonResp["sl"].ToDouble(); tp = jsonResp["tp"].ToDouble(); parsed = true; } else { // Fallback text parse (enhanced: ignore case, trim) Print("JSON failed; text fallback."); string lines[]; StringSplit(response, '\n', lines); for (int i = 0; i < ArraySize(lines); i++) { string line = lines[i]; StringTrimLeft(line); StringTrimRight(line); // Clean StringToUpper(line); // Case-insensitive if (StringFind(line, "SIGNAL:") == 0) { signal = StringSubstr(lines[i], StringFind(lines[i], ":") + 1); // Original case } else if (StringFind(line, "REASON:") == 0) { reason = StringSubstr(lines[i], StringFind(lines[i], ":") + 1); } else if (StringFind(line, "TRIGGER_BAR:") == 0) { triggerBar = (int)StringToInteger(StringSubstr(lines[i], StringFind(lines[i], ":") + 1)); } else if (StringFind(line, "ENTRY:") == 0) { entry = StringToDouble(StringSubstr(lines[i], StringFind(lines[i], ":") + 1)); } else if (StringFind(line, "SL:") == 0) { sl = StringToDouble(StringSubstr(lines[i], StringFind(lines[i], ":") + 1)); } else if (StringFind(line, "TP:") == 0) { tp = StringToDouble(StringSubstr(lines[i], StringFind(lines[i], ":") + 1)); } } } // Validation: Allow NONE without prices; else require basics if (signal == "") { Alert("No signal detected. Raw: " + response); // Debug aid return; } if (signal != "NONE" && (entry == 0 || sl == 0 || tp == 0)) { Alert("Incomplete prices for " + signal + ". Raw: " + response); // Specific debug return; } if (triggerBar < 1 || triggerBar > 10) triggerBar = 1; // Clamp to data // Append to history string timestamp = TimeToString(TimeCurrent(), TIME_MINUTES); string fullReason = reason + " (Bar " + IntegerToString(triggerBar) + ")"; conversationHistory += "You: Get trade signal" + modeStr + "\n" + timestamp + "\nAI: " + response + "\n" + timestamp + "\n\n"; UpdateCurrentHistory(); UpdateResponseDisplay(); scroll_pos = MathMax(0, g_total_height - g_visible_height); if (scroll_visible) { UpdateSliderPosition(); UpdateButtonColors(); } ChartRedraw(); // Visualize (skip if NONE) if (signal != "NONE") { VisualizeSignal(signal, fullReason, triggerBar, entry, sl, tp); } else { Print("No signal (NONE)—skipping viz/trade."); } // Auto-trade if (AutoTrade && signal != "NONE") { OpenTrade(signal, entry, sl, tp); } }
Здесь определяется функция GetTradeSignal для запроса и обработки торгового сигнала от ИИ. Ее опциональный логический параметр isAuto по умолчанию имеет значение false для ручного вызова или true для автоматического вызова на новых барах. Для автоматического режима переменной modeStr присваивается " (Auto-mode)", иначе – " (Manual)". Затем в журнал выводится "Starting signal analysis" вместе со значением режима. Это позволит различать записи журнала и видеть, какой анализ выполняется. Ниже приведены записи журнала.

Затем вызывается GetChartDataString, формирующая данные графика в chartData. Если строка пуста, выводится предупреждение и функция завершает работу. Формируется подробный промпт, предписывающий ИИ возвращать только JSON. В нем задается точная схема: signal (BUY/SELL/NONE), reason (точный паттерн), trigger_bar (от 1 до 10, где 1 – последний завершенный бар), а также десятичные entry/sl/tp. Промпт требует точного совпадения с паттернами из списка "[Your existing patterns list here...]", предписывает возвращать NONE при отсутствии совпадения и содержит пример JSON, данные графика и напоминание о порядке баров.
Промпт передается в GetChatGPTResponse с температурой 0,0 для получения строгого JSON, после чего выводится необработанный ответ. Подстрока JSON извлекается путем поиска "{" и "}" через StringFind; если они найдены, нужная часть выделяется функцией StringSubstr. Выполняется попытка разбора JSON: jsonResp объявляется как JsonValue, ответ через StringToCharArray преобразуется в символьный массив и десериализуется DeserializeFromArray. При успехе signal и reason извлекаются как строки, triggerBar – как int через ToInteger, entry/sl/tp – как double через ToDouble, а parsed получает значение true. Если разбор JSON не удался, используется разбор текста: в журнал выводится "JSON failed; text fallback", ответ через StringSplit разбивается на строки, каждая строка очищается функциями StringTrimLeft и StringTrimRight, а ее копия приводится к верхнему регистру для сравнения. Значения после ":" извлекаются через StringSubstr и StringFind, а затем приводятся к нужным типам.
Выполняется проверка: если сигнал отсутствует, выводится предупреждение с необработанным ответом и функция завершает работу. Если сигнал не NONE, но цены отсутствуют, выводится предупреждение о неполных данных и функция также завершается. Значение triggerBar ограничивается диапазоном от 1 до 10. История пополняется следующим образом: сначала получаем временную метку с точностью до минут через TimeToString, затем добавляем строковый литерал "You: Get trade signal", значение режима, перенос строки, временную метку, перенос строки, строковый литерал "AI: ", ответ, перенос строки, временную метку для ответа и финальный перенос строки. Затем вызываются UpdateCurrentHistory и UpdateResponseDisplay; переменной scroll_pos присваивается максимальное значение – разность общей и видимой высоты – для прокрутки вниз. Если прокрутка видима, обновляются ползунок и кнопки, после чего график перерисовывается. Если сигнал не NONE, вызывается VisualizeSignal с параметрами signal, fullReason, triggerBar, entry, sl и tp. Если AutoTrade имеет значение true и сигнал не NONE, вызывается OpenTrade с нужными параметрами. Теперь эту функцию можно вызывать при необходимости. Сначала она будет вызываться кнопкой ручного запуска, а затем – при каждом тике.
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- EXISTING LOGIC else if (id == CHARTEVENT_OBJECT_CLICK && sparam == "ChatGPT_GetSignalButton") { GetTradeSignal(); } //--- REST OF EXISTING LOGIC }
В обработчике событий OnChartEvent функция GetTradeSignal вызывается при нажатии соответствующей кнопки. Теперь основная логика переносится в обработчик тиковых событий.
void OnTick() { static datetime prevBarTime = 0; // Tracks the time of the last processed bar datetime currentBarTime[1]; if (CopyTime(Symbol(), Period(), 0, 1, currentBarTime) != 1) { Print("Failed to copy current bar time: ", GetLastError()); return; } // Check if a new bar has started (previous bar closed) if (currentBarTime[0] != prevBarTime && EnableAutoSignal) { prevBarTime = currentBarTime[0]; // Optional: Log for debugging Print("New bar detected at ", TimeToString(prevBarTime, TIME_DATE | TIME_MINUTES), ". Running auto signal check..."); GetTradeSignal(true); } }
В обработчике событий OnTick статическая переменная prevBarTime отслеживает время открытия последнего обработанного бара. Объявляется массив currentBarTime, в который через CopyTime копируется время открытия текущего бара со смещением 0. Если функция возвращает не 1, ошибка фиксируется в журнале через Print и GetLastError, после чего выполняется досрочный выход. Проверка позволяет запускать анализ только один раз на бар и не перегружать систему лишними запросами и расходом токенов. Затем проверяется, отличается ли время текущего бара от prevBarTime и имеет ли EnableAutoSignal значение true. Если оба условия выполнены, prevBarTime обновляется новым временем, в журнал записывается "New bar detected at " с датой и временем через TimeToString, а также "Running auto signal check...". После этого GetTradeSignal вызывается с true для автоматического режима. Это запускает анализ ИИ только на закрытых барах и оптимизирует производительность. При включенном автоматическом режиме получаем следующий результат.

На визуализации видно, что в программу добавлены и настроены новые элементы интерфейса, а торговля автоматизирована. Поставленные задачи выполнены. Остается протестировать программу на исторических данных, и этому посвящен следующий раздел.
Тестирование на исторических данных
Мы провели тестирование; ниже представлена итоговая визуализация в виде одного изображения формата Graphics Interchange Format (GIF).

Заключение
В этой части мы продолжили модульную реорганизацию торговой системы с искусственным интеллектом на языке MQL5: вынесли компоненты UI в отдельный подключаемый файл и добавили настройку температуры для управления характером ответов ИИ. Система автоматически исполняет сделки по разобранным JSON-сигналам с уровнями входа, SL и TP, отображает паттерны поглощения и дивергенции с настраиваемыми элементами и поддерживает опциональную проверку сигналов на новых барах. Для работы на реальном счете также предусмотрены настройка объема сделки и магического числа. Эти улучшения делают помощник надежнее и удобнее, а в следующих частях позволят добавить более сложные возможности, например мониторинг нескольких символов и автоматизацию управления рисками. Оставайтесь с нами.
Приложения
| № | Имя | Тип | Описание |
|---|---|---|---|
| 1 | AI_JSON_FILE.mqh | Библиотека классов JSON | Класс для сериализации и десериализации JSON |
| 2 | AI_CREATE_OBJECTS_FNS.mqh | Библиотека функций объектов | Функции для создания объектов визуализации, таких как метки и кнопки |
| 3 | AI_UI_COMPONENTS.mqh | Библиотека компонентов пользовательского интерфейса | Файл, содержащий компоненты пользовательского интерфейса и логику их организации |
| 4 | AI_ChatGPT_EA_Part_7.mq5 | Основной файл советника | Основной советник для интеграции ИИ |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20588
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Возможности Мастера MQL5, которые вам нужно знать (Часть 86): Ускорение доступа к данным с помощью разреженной таблицы в пользовательском классе трейлинга
Изучение Стандартной библиотеки MQL5 (Часть 5): Мультисигнальный советник
Изучение Стандартной библиотеки MQL5 (Часть 6): Оптимизация сгенерированного советника
Возможности Мастера MQL5, которые вам нужно знать (Часть 85): Использование паттернов стохастического осциллятора и FrAMA с обучением на основе инференса β-VAE
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
**Искренний призыв ко всем**
Сложенными руками и с сердцем, полным уважения, я обращаюсь к каждому из вас.
Я прочитал все статьи, начиная со 2-й части и заканчивая 7-й. Сэр Аллан — настоящий эксперт, и проделанная им работа просто невероятна — показанная эффективность советника абсолютно поразительна.
Но с тяжелым сердцем должен признаться, что я не такой, как он. Возможно, это из-за моего недостаточного знания программирования или языка MQL5. Я очень старался, вкладывая всю душу в каждую попытку, но каждый раз терпел неудачу. Я просто не знаю, как заставить этот великолепный советник работать в моей системе. Процесс, описанный в части 2 — с одним файлом, который нужно скопировать, скомпилировать и запустить — был понятен. Но теперь, когда в последующих частях речь идет о нескольких файлах, я полностью растерялся и не знаю, с чего начать.
С глубочайшим почтением я хотел бы высказать две небольшие просьбы, если позволите:
1. Не мог бы сэр Аллан любезно предоставить все файлы ресурсов в формате `.bmp` в одном месте? Возможно, я их пропустил, и их объединение в одно место было бы большим подспорьем для всех.
2. Не могли бы вы предоставить простое руководство — файл `Instructions.txt` — в котором было бы точно объяснено, куда именно в MT5 нужно сохранять каждый файл и изображение? Это стало бы настоящим спасением для таких людей, как я, у которых нет технических знаний, но которые очень хотят учиться.
Я понимаю, что прошу слишком многого, и прошу прощения, если это кажется чрезмерным. Но мое рвение проистекает из глубокой необходимости. Я потерял значительный капитал на торговле, и вся моя надежда связана с поиском системы, которая действительно работает. Этот советник кажется мне тем самым лучом надежды.
Если я допустил какие-либо ошибки в своих словах, прошу прощения. Я умоляю вас всех из глубины души проявить немного милосердия к начинающему трейдеру, который переживает трудности.
Спасибо за ваше терпение и доброту. С надеждой и нетерпением жду ваших советов и поддержки.
**С искренней благодарностью и уважением,**
*Ваш коллега-трейдер*
**Искренний призыв ко всем**
Сложенными руками и с сердцем, полным уважения, я обращаюсь к каждому из вас.
Я прочитал все статьи, начиная со 2-й части и заканчивая 7-й. Сэр Аллан — настоящий эксперт, и проделанная им работа просто невероятна — показанные результаты EA абсолютно поразительны.
Но с тяжелым сердцем вынужден признать, что я не такой, как он. Возможно, это из-за моего недостаточного знания программирования или языка MQL5. Я очень старался, вкладывая душу в каждую попытку, но каждый раз терпел неудачу. Я просто не знаю, как заставить этот великолепный советник работать в моей системе. Процесс, описанный в части 2 — с одним файлом, который нужно скопировать, скомпилировать и запустить — был понятен. Но теперь, когда в последующих частях речь идет о нескольких файлах, я полностью растерялся и не знаю, с чего начать.
С глубочайшим смирением я хотел бы высказать две небольшие просьбы, если позволите:
1. Не мог бы сэр Аллан любезно предоставить все файлы ресурсов в формате `.bmp` в одном месте? Возможно, я их пропустил, и их объединение в одном месте было бы большим подспорьем для всех.
2. Не могли бы вы подготовить простое руководство — файл `Instructions.txt` — в котором было бы точно объяснено, куда именно в MT5 следует сохранять каждый файл и изображение? Это стало бы настоящим спасением для таких людей, как я, у которых нет технических знаний, но которые очень хотят учиться.
Я понимаю, что прошу слишком многого, и приношу извинения, если это кажется чрезмерным. Но мое рвение проистекает из глубокой необходимости. Я потерял значительный капитал на торговле, и вся моя надежда связана с поиском системы, которая действительно работает. Этот советник кажется мне тем самым маяком надежды.
Если я допустил какие-либо ошибки в своих словах, прошу прощения. Я умоляю вас всех из глубины души проявить немного милосердия к начинающему трейдеру, который переживает трудности.
Спасибо за ваше терпение и доброту. С надеждой и нетерпением жду ваших советов и поддержки.
**С искренней благодарностью и уважением,**
*Коллега-трейдер*
Затем зайдите в Google, введите «.png в .bmp» и найдите бесплатный конвертер.
Затем в основной рабочей папке, где находится файл mq5, поместите
с правильным именем — всё должно скомпилироваться без проблем. Примечание. Если при компиляции появляется сообщение об отсутствии файла .mqh, убедитесь, что из имён файлов удалены все символы «_», и приведите их в соответствие с тем, как они указаны в файлах .mq5 и .mqh — тогда компиляция пройдёт успешно.
Просто зайдите в Google, найдите сайт с иконками и скачайте изображение для каждого из нужных вам файлов .bmp.
Затем зайдите в Google и введите «.png в .bmp» — там вы найдёте бесплатный конвертер.
Затем в основной рабочей папке, где находится файл mq5, поместите файл
с правильным именем — всё должно скомпилироваться без проблем. Примечание. Если при компиляции появляется сообщение об отсутствии файла .mqh, убедитесь, что из имён файлов удалены все символы «_», и приведите их в соответствие с тем, как они указаны в файлах .mq5 и .mqh — после этого компиляция должна пройти успешно.
Уважаемая компания Ricks Creations
Все еще не компилируется. Я выполнил все шаги, но компиляция так и не завершилась. Пожалуйста, помогите мне
Спасибо, с уважением
Уважаемые все
Это скромная просьба: пожалуйста, ПОМОГИТЕ мне
со сложенными руками
Всем привет,
Сегодня для меня наступил очень важный и особенный момент. Один из участников нашего сообщества появился передо мной словно ангел и помог мне успешно запустить этот советник на MT5.
Я хотела бы выразить свою искреннюю благодарность г-ну Алехандро Агудело. Он настоящий гений и поддержал меня всеми возможными способами.
Пожалуйста, посмотрите на результаты его усилий.