You are currently viewing the resource titled "Building ChatGPT AI-Driven EA/BOT in MQL5: Slick UI Animations, Speed Metrics & Smart Response Tools". This page provides detailed information about the resource, including its content, attached files, and recent discussions. Feel free to explore, download available files if logged in, and join the community conversation below!
Banner Image

Introduction

Previously, we created an article for trading using ChatGPT AI integration. In this episode, we craft a refined user interface (UI) incorporating animations, timing metrics, and response management utilities. This approach boosts user engagement through visual loading indicators amid API calls, detailed timing insights for efficiency evaluation, and convenient regenerate and export controls for handling AI results. Our discussion includes these key areas:


  1. Exploring the Upgraded UI Elements
  2. Execution within MQL5
  3. Backtesting Procedures
  4. Final Thoughts
Ultimately, you'll gain a ready-to-use MQL5 script for sophisticated AI-based trading experiences, primed for personalization—time to get started!

Exploring the Upgraded UI Elements

The upgraded user interface components prioritize enhancing engagement in the AI-driven trading platform, integrating loading animations for visual cues amid API request setup and processing intervals, alongside response timing indicators in seconds to highlight operational performance. We incorporate response oversight utilities, like regenerate buttons to resend the prior prompt for renewed AI generation and export buttons to archive outputs in text files, facilitating straightforward analysis or dissemination.

Our objective is to construct these elements in a modular fashion, augmenting established UI parts with animation sequences for progressing dot patterns, time computations through tick metrics, and event listeners for button activations to initiate re-queries or file storages. We'll advance sidebar state oversight via flexible resizing and component realignment. The blueprint features user-action-driven conditional displays, promoting fluid refreshes to interfaces and scroll alignments while preserving essential AI capabilities. Concisely, presented here is a graphical depiction of our aims.


Execution within MQL5

To execute these enhancements, we'll begin by establishing the fresh object constants we intend to introduce. Our initial focus will be on the UI Components file, as it serves as the repository for our utilities. Below outlines the rationale we employ to accomplish this.

string REGEN_ICON_FONT = "Webdings";
string EXPORT_ICON_FONT = "Wingdings 3";
#define REGEN_ICON CharToString('q')  // Circular arrow (spin/regenerate)
#define EXPORT_ICON CharToString('7') // Proxy for save/export
#define ICON_SIZE 16
#define ICON_SPACING 5
color REGEN_COLOR = clrGreen;
color EXPORT_COLOR = clrBlack;

Within the global scope of the UI module, we establish "REGEN_ICON_FONT" as Webdings and "EXPORT_ICON_FONT" as Wingdings 3, designating these font families to display unique characters as symbolic icons. Employing preprocessor directives, we assign "REGEN_ICON" to the 'q' character transformed through CharToString, yielding a looped arrow emblematic of regeneration, while "EXPORT_ICON" utilizes '7' as a stand-in for a storage or export indicator. Feel free to select alternatives from the provided table and adjust accordingly for personalization.

We've highlighted the preferred options for use. Next, we define "ICON_SIZE" at 16 to ensure uniform icon scaling, "ICON_SPACING" at 5 for appropriate separations between elements, "REGEN_COLOR" in green for the regeneration symbol, and "EXPORT_COLOR" in black for the export indicator. You're welcome to tweak these for better aesthetic alignment. Following that, we'll integrate these items into the computation of line heights.

void ComputeLinesAndHeight(const string &font, const int fontSize, const int timestampFontSize,
                           const int adjustedLineHeight, const int adjustedTimestampHeight,
                           const int messageMargin, const int maxTextWidth,
                           const string &msgRoles[], const string &msgContents[], const string &msgTimestamps[],
                           const int numMessages, int &totalHeight_out, int &totalLines_out,
                           string &allLines_out[], string &lineRoles_out[], int &lineHeights_out[]) {
   ArrayResize(allLines_out, 0);
   ArrayResize(lineRoles_out, 0);
   ArrayResize(lineHeights_out, 0);
   totalLines_out = 0;
   totalHeight_out = 0;
   for (int m = 0; m < numMessages; m++) {
      string wrappedLines[];
      WrapText(msgContents[m], font, fontSize, maxTextWidth, wrappedLines);
      int numLines = ArraySize(wrappedLines);
      int currSize = ArraySize(allLines_out);
      ArrayResize(allLines_out, currSize + numLines + 1);
      ArrayResize(lineRoles_out, currSize + numLines + 1);
      ArrayResize(lineHeights_out, currSize + numLines + 1);
      for (int l = 0; l < numLines; l++) {
         allLines_out[currSize + l] = wrappedLines[l];
         lineRoles_out[currSize + l] = msgRoles[m];
         lineHeights_out[currSize + l] = adjustedLineHeight;
         totalHeight_out += adjustedLineHeight;
      }
      allLines_out[currSize + numLines] = msgTimestamps[m];
      lineRoles_out[currSize + numLines] = msgRoles[m] + "_timestamp";
      lineHeights_out[currSize + numLines] = adjustedTimestampHeight;
      totalHeight_out += adjustedTimestampHeight;
      totalLines_out += numLines + 1;
      if (m < numMessages - 1) {
         totalHeight_out += messageMargin;
      } else if (m == numMessages - 1 && numMessages > 0) {
         if (totalHeight_out > 0) totalHeight_out -= messageMargin; // Adjust if last
      }
   }
   // Add buffer below loading messages (Preparing/Thinking) to ensure space for timestamp
   if (numMessages > 0 && StringFind(msgRoles[numMessages - 1], "AI") >= 0 && 
       (StringFind(msgContents[numMessages - 1], "Preparing the Request") >= 0 || 
        StringFind(msgContents[numMessages - 1], "Thinking...") >= 0)) {
      totalHeight_out += 30;  // Extra space below thinking timestamp during wait
   }
   // Add padding if last message is AI and contains time note
   if (numMessages > 0 && StringFind(msgRoles[numMessages - 1], "AI") >= 0 && StringFind(msgContents[numMessages - 1], "(Response in ") >= 0) {
      totalHeight_out += 30; // Dedicated space for time note line + icons
   }
}

In the "ComputeLinesAndHeight" routine, for every message, we incorporate its timestamp as an extra line featuring a "_timestamp" appended role and modified timestamp elevation, boosting the overall height and line tally, followed by inserting a message margin unless it's the final message—in which case we deduct it to prevent unnecessary trailing space. We introduce an additional buffer of 30 units if the concluding message originates from the AI and features "Preparing the Request" or "Thinking..." to allocate room beneath during loading phases, plus another 30 if it contains "(Response in " to provide cushioning below timing annotations with icons. We've emphasized the particular modifications for better visibility. Next, we'll revise the rendering function for response visualization, thereby integrating the novel auxiliary icons as well.

void UpdateResponseDisplay() {
   if (showing_small_history_popup || showing_big_history_popup || showing_search_popup) return;
   int total = ObjectsTotal(0, 0, -1);
   for (int j = total - 1; j >= 0; j--) {
      string name = ObjectName(0, j, 0, -1);
      if (StringFind(name, "ChatGPT_ResponseLine_") == 0 ||
          StringFind(name, "ChatGPT_MessageBg_") == 0 ||
          StringFind(name, "ChatGPT_MessageText_") == 0 ||
          StringFind(name, "ChatGPT_Timestamp_") == 0 ||
          StringFind(name, "ChatGPT_RegenIcon") == 0 ||
          StringFind(name, "ChatGPT_ExportIcon") == 0) {
         ObjectDelete(0, name);
      }
   }
   string displayText = conversationHistory;
   int textX = g_mainContentX + g_sidePadding + g_textPadding;
   int textY = g_mainY + g_headerHeight + g_padding + g_textPadding;
   int fullMaxWidth = g_mainWidth - 2 * g_sidePadding - 2 * g_textPadding;
   if (displayText == "") {
      string objName = "ChatGPT_ResponseLine_0";
      createLabel(objName, textX, textY, "Type your prompt here and click Send to chat with the AI.", clrGray, 10, "Arial", CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER);
      g_total_height = 0;
      g_visible_height = g_displayHeight - 2 * g_textPadding;
      if (scroll_visible) {
         DeleteScrollbar();
         scroll_visible = false;
      }
      ChartRedraw();
      return;
   }
   string parts[];
   int numParts = StringSplit(displayText, '\n', parts);
   string msgRoles[];
   string msgContents[];
   string msgTimestamps[];
   string currentRole = "";
   string currentContent = "";
   string currentTimestamp = "";
   for (int p = 0; p < numParts; p++) {
      string line = parts[p];
      string trimmed = line;
      StringTrimLeft(trimmed);
      StringTrimRight(trimmed);
      if (StringLen(trimmed) == 0) {
         if (currentRole != "") currentContent += "\n";
         continue;
      }
      if (StringFind(trimmed, "You: ") == 0) {
         if (currentRole != "") {
            int size = ArraySize(msgRoles);
            ArrayResize(msgRoles, size + 1);
            ArrayResize(msgContents, size + 1);
            ArrayResize(msgTimestamps, size + 1);
            msgRoles[size] = currentRole;
            msgContents[size] = currentContent;
            msgTimestamps[size] = currentTimestamp;
         }
         currentRole = "User";
         currentContent = StringSubstr(line, StringFind(line, "You: ") + 5);
         currentTimestamp = "";
         continue;
      } else if (StringFind(trimmed, "AI: ") == 0) {
         if (currentRole != "") {
            int size = ArraySize(msgRoles);
            ArrayResize(msgRoles, size + 1);
            ArrayResize(msgContents, size + 1);
            ArrayResize(msgTimestamps, size + 1);
            msgRoles[size] = currentRole;
            msgContents[size] = currentContent;
            msgTimestamps[size] = currentTimestamp;
         }
         currentRole = "AI";
         currentContent = StringSubstr(line, StringFind(line, "AI: ") + 4);
         currentTimestamp = "";
         continue;
      } else if (IsTimestamp(trimmed)) {
         currentTimestamp = trimmed;
         int size = ArraySize(msgRoles);
         ArrayResize(msgRoles, size + 1);
         ArrayResize(msgContents, size + 1);
         ArrayResize(msgTimestamps, size + 1);
         msgRoles[size] = currentRole;
         msgContents[size] = currentContent;
         msgTimestamps[size] = currentTimestamp;
         currentRole = "";
         currentContent = "";
         currentTimestamp = "";
      } else {
         if (currentRole != "") {
            currentContent += "\n" + line;
         }
      }
   }
   if (currentRole != "") {
      int size = ArraySize(msgRoles);
      ArrayResize(msgRoles, size + 1);
      ArrayResize(msgContents, size + 1);
      ArrayResize(msgTimestamps, size + 1);
      msgRoles[size] = currentRole;
      msgContents[size] = currentContent;
      msgTimestamps[size] = currentTimestamp;
   }
   int numMessages = ArraySize(msgRoles);
   if (numMessages == 0) {
      string objName = "ChatGPT_ResponseLine_0";
      createLabel(objName, textX, textY, "Type your prompt here and click Send to chat with the AI.", clrGray, 10, "Arial", CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER);
      g_total_height = 0;
      g_visible_height = g_displayHeight - 2 * g_textPadding;
      if (scroll_visible) {
         DeleteScrollbar();
         scroll_visible = false;
      }
      ChartRedraw();
      return;
   }
   string font = "Arial";
   int fontSize = 10;
   int timestampFontSize = 8;
   int lineHeight = TextGetHeight("A", font, fontSize);
   int timestampHeight = TextGetHeight("A", font, timestampFontSize);
   int adjustedLineHeight = lineHeight + g_lineSpacing;
   int adjustedTimestampHeight = timestampHeight + g_lineSpacing;
   int messageMargin = 25;  // Increased for extra space
   int visibleHeight = g_displayHeight - 2 * g_textPadding;
   g_visible_height = visibleHeight;
   string tentativeAllLines[];
   string tentativeLineRoles[];
   int tentativeLineHeights[];
   int tentativeTotalHeight, tentativeTotalLines;
   ComputeLinesAndHeight(font, fontSize, timestampFontSize, adjustedLineHeight, adjustedTimestampHeight,
                         messageMargin, fullMaxWidth, msgRoles, msgContents, msgTimestamps, numMessages,
                         tentativeTotalHeight, tentativeTotalLines, tentativeAllLines, tentativeLineRoles, tentativeLineHeights);
   bool need_scroll = tentativeTotalHeight > visibleHeight;
   bool should_show_scrollbar = false;
   int reserved_width = 0;
   if (ScrollbarMode != SCROLL_WHEEL_ONLY) {
      should_show_scrollbar = need_scroll && (ScrollbarMode == SCROLL_DYNAMIC_ALWAYS || (ScrollbarMode == SCROLL_DYNAMIC_HOVER && mouse_in_display));
      if (should_show_scrollbar) {
         reserved_width = 16;
      }
   }
   string allLines[];
   string lineRoles[];
   int lineHeights[];
   int totalHeight, totalLines;
   if (reserved_width > 0) {
      ComputeLinesAndHeight(font, fontSize, timestampFontSize, adjustedLineHeight, adjustedTimestampHeight,
                            messageMargin, fullMaxWidth - reserved_width, msgRoles, msgContents, msgTimestamps, numMessages,
                            totalHeight, totalLines, allLines, lineRoles, lineHeights);
   } else {
      totalHeight = tentativeTotalHeight;
      totalLines = tentativeTotalLines;
      ArrayCopy(allLines, tentativeAllLines);
      ArrayCopy(lineRoles, tentativeLineRoles);
      ArrayCopy(lineHeights, tentativeLineHeights);
   }
   g_total_height = totalHeight;
   bool prev_scroll_visible = scroll_visible;
   scroll_visible = should_show_scrollbar;
   if (scroll_visible != prev_scroll_visible) {
      if (scroll_visible) {
         CreateScrollbar();
      } else {
         DeleteScrollbar();
      }
   }
   int max_scroll = MathMax(0, totalHeight - visibleHeight);
   if (scroll_pos > max_scroll) scroll_pos = max_scroll;
   if (scroll_pos < 0) scroll_pos = 0;
   if (totalHeight > visibleHeight && scroll_pos == prev_scroll_pos && prev_scroll_pos == -1) {
      scroll_pos = max_scroll;
   }
   if (scroll_visible) {
      slider_height = CalculateSliderHeight();
      ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height);
      UpdateSliderPosition();
      UpdateButtonColors();
   }
   int currentY = textY - scroll_pos;
   int endY = textY + visibleHeight;
   int startLineIndex = 0;
   int currentHeight = 0;
   for (int line = 0; line < totalLines; line++) {
      if (currentHeight >= scroll_pos) {
         startLineIndex = line;
         currentY = textY + (currentHeight - scroll_pos);
         break;
      }
      currentHeight += lineHeights[line];
      if (line < totalLines - 1 && StringFind(lineRoles[line], "_timestamp") >= 0 && StringFind(lineRoles[line + 1], "_timestamp") < 0) {
         currentHeight += messageMargin;
      }
   }
   int numVisibleLines = 0;
   int visibleHeightUsed = 0;
   for (int line = startLineIndex; line < totalLines; line++) {
      int lineHeight = lineHeights[line];
      if (visibleHeightUsed + lineHeight > visibleHeight) break;
      visibleHeightUsed += lineHeight;
      numVisibleLines++;
      if (line < totalLines - 1 && StringFind(lineRoles[line], "_timestamp") >= 0 && StringFind(lineRoles[line + 1], "_timestamp") < 0) {
         if (visibleHeightUsed + messageMargin > visibleHeight) break;
         visibleHeightUsed += messageMargin;
      }
   }
   int leftX = g_mainContentX + g_sidePadding + g_textPadding;
   int rightX = g_mainContentX + g_mainWidth - g_sidePadding - g_textPadding - reserved_width;
   color userColor = clrGray;
   color aiColor = clrBlue;
   color timestampColor = clrDarkGray;
   for (int li = 0; li < numVisibleLines; li++) {
      int lineIndex = startLineIndex + li;
      if (lineIndex >= totalLines) break;
      string line = allLines[lineIndex];
      string role = lineRoles[lineIndex];
      bool isTimestamp = StringFind(role, "_timestamp") >= 0;
      int currFontSize = isTimestamp ? timestampFontSize : fontSize;
      color textCol = isTimestamp ? timestampColor : (StringFind(role, "User") >= 0 ? userColor : aiColor);
      string currFont = font;
      if (StringFind(line, "Preparing the Request") >= 0) {
         textCol = clrDodgerBlue;
         currFont = "Arial Bold";
      }
      if (StringFind(line, "Thinking...") >= 0) {
         textCol = clrRed;
         currFont = "Arial Bold";
      }
      if (StringFind(line, "(Response in ") == 0) {
         textCol = clrGray;
      }
      string display_line = line;
      if (line == " ") {
         display_line = " ";
         textCol = clrWhite;
      }
      int textX_pos = (StringFind(role, "User") >= 0) ? rightX : leftX;
      ENUM_ANCHOR_POINT textAnchor = (StringFind(role, "User") >= 0) ? ANCHOR_RIGHT_UPPER : ANCHOR_LEFT_UPPER;
      string lineName = "ChatGPT_MessageText_" + IntegerToString(lineIndex);
      if (currentY >= textY && currentY < endY) {
         createLabel(lineName, textX_pos, currentY, display_line, textCol, currFontSize, currFont, CORNER_LEFT_UPPER, textAnchor);
      }
      // Add icons if this is the time note line and it's the last AI's second-last line
      if (StringFind(line, "(Response in ") == 0 && StringFind(role, "AI") >= 0 && lineIndex == totalLines - 2) {
         // Calculate time note width for positioning
         TextSetFont(currFont, currFontSize);
         uint tw, th;
         TextGetSize(line, tw, th);
         int iconX = leftX + (int)tw + 50;  // X offset for space
         int iconY = currentY - 3;  // To raise up on Y axis
         
         // Regenerate icon
         string regenName = "ChatGPT_RegenIcon";
         createLabel(regenName, iconX, iconY, REGEN_ICON, REGEN_COLOR, ICON_SIZE, REGEN_ICON_FONT, CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER);
         ObjectSetInteger(0, regenName, OBJPROP_SELECTABLE, true);
         ObjectSetInteger(0, regenName, OBJPROP_ZORDER, 10);
         
         // Export icon
         iconX += ICON_SIZE + ICON_SPACING;
         string exportName = "ChatGPT_ExportIcon";
         createLabel(exportName, iconX, iconY, EXPORT_ICON, EXPORT_COLOR, ICON_SIZE, EXPORT_ICON_FONT, CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER);
         ObjectSetInteger(0, exportName, OBJPROP_SELECTABLE, true);
         ObjectSetInteger(0, exportName, OBJPROP_ZORDER, 10);
      }
      currentY += lineHeights[lineIndex];
      if (lineIndex < totalLines - 1 && StringFind(lineRoles[lineIndex], "_timestamp") >= 0 && StringFind(lineRoles[lineIndex + 1], "_timestamp") < 0) {
         currentY += messageMargin;
      }
   }
   ChartRedraw();
}

We commence by verifying if any overlay such as compact history, expanded history, or search panel is active, exiting prematurely if detected to prevent refreshing the primary response view, consistent with prior behavior. Next, we iterate across all chart objects to remove those linked to earlier response lines, message backdrops, texts, timestamps, and newly added regenerate and export icons via the ObjectDelete method, thereby clearing the space. The remaining code stays unchanged, with modifications spotlighted and annotated for transparency. Nonetheless, we'll elaborate starting from the segment featuring significant revisions for the incorporated icons.

Initially, we determine "numVisibleLines" through cumulative visible heights, ensuring they stay within "visibleHeight" limits and accounting for post-timestamp margins. We establish left and right x coordinates, along with color selections for user, AI, and timestamp elements. Subsequently, we cycle through visible lines: retrieve each line and its role, check for timestamps to configure size, color, and font, and refine the display line where feasible. We assign position and anchor according to the role. If the line falls inside y boundaries, we generate a label using "createLabel." For the timing notation in the final AI message (at lineIndex totalLines-2), we gauge width via TextGetSize, then derive "iconX" and "iconY." We produce the regenerate icon label "ChatGPT_RegenIcon" employing "REGEN_ICON," its color, size, and font, plus the export icon "ChatGPT_ExportIcon" with comparable spacing, while configuring selectable status and z-order. We advance "currentY" by the line height, appending a margin post-timestamps except for the concluding one. Lastly, we invoke ChartRedraw to update the view. At this point, we possess a fully revised UI components module. The only task left is invoking the appropriate functions in the primary file to enact the updates. Within the main file, we initiate by declaring the animation constants at the outset, in global scope, for simplified oversight.

// Loading indicator constants
string PrepBase = "AI: Preparing the Request";  // Preparing Text
string LoadingPlaceholder = "AI: Thinking...";  // Thinking text
string SpinnerDots[] = {"", ".", "..", "..."};  // Cycling dots for animation
int PreAnimationCycles = 6;  // Number of cycles (~1s total)
ulong StartTimeMs = 0;  // For timing API call

In this section, we assign the "PrepBase" string to "AI: Preparing the Request" as the foundational text for the preliminary loading indicator amid API request setup. Feel free to modify it to suit your preferred startup preparation phrasing. We configure "LoadingPlaceholder" as "AI: Thinking..." for the display shown during AI response anticipation, which is also customizable. Next, we form the "SpinnerDots" string array containing an empty string, one dot, two dots, and three dots to produce rotating animation visuals added to loading texts. We define "PreAnimationCycles" at 6 to regulate animation iterations, yielding roughly 1 second overall based on pause durations. We set "StartTimeMs" as an unsigned long initialized to 0, employed to record the initial tick value for gauging API response duration. Subsequently, upon message submission, we integrate these elements to emulate the loading condition. Presented here is the revised function.

void SubmitMessage(string prompt) {
   if (StringLen(prompt) == 0) return;
   string timestamp = TimeToString(TimeCurrent(), TIME_MINUTES);
   string response = "";
   bool send_to_api = true;
   if (StringFind(prompt, "set title ") == 0) {
      string new_title = StringSubstr(prompt, 10);
      current_title = new_title;
      response = "Title set to " + new_title;
      send_to_api = false;
      UpdateCurrentHistory();
      UpdateSidebarDynamic();
   }
   // Save old height before adding prompt
   UpdateResponseDisplay();
   int old_total = g_total_height;
   conversationHistory += "You: " + prompt + "\n" + timestamp + "\n";
   // Get height after prompt
   UpdateResponseDisplay();
   int after_prompt = g_total_height;
   int prompt_height = after_prompt - old_total;
   if (send_to_api) {
      conversationHistory += PrepBase + "\n" + timestamp + "\n\n";
      // Get height after loading
      UpdateResponseDisplay();
      int after_loading = g_total_height;
      int loading_height = after_loading - after_prompt;
      int new_content_height = prompt_height + loading_height;
      // Dynamic scroll: if fits, prompt at top (higher); else, loading at bottom
      if (new_content_height <= g_visible_height) {
         scroll_pos = MathMax(0, old_total);
      } else {
         scroll_pos = MathMax(0, after_loading - g_visible_height);
      }
      if (scroll_visible) {
         UpdateSliderPosition();
         UpdateButtonColors();
      }
      ChartRedraw();
      for (int i = 0; i < PreAnimationCycles; i++) {
         // Sub-cycle for strict increasing: reset dots every 3 steps
         int subCycle = i % 3;
         string dots = "";
         for (int d = 0; d <= subCycle; d++) {
            dots += ".";
         }
         int prepPos = StringFind(conversationHistory, PrepBase, 0);
         if (prepPos >= 0) {
            int endPos = StringFind(conversationHistory, "\n\n", prepPos) + 2;
            if (endPos < 2) endPos = StringLen(conversationHistory);
            string before = StringSubstr(conversationHistory, 0, prepPos);
            string after = StringSubstr(conversationHistory, endPos);
            conversationHistory = before + PrepBase + dots + "\n" + timestamp + "\n\n" + after;
         }
         UpdateResponseDisplay();
         // Re-apply dynamic scroll after animation update (height same as loading)
         scroll_pos = (new_content_height <= g_visible_height) ? MathMax(0, old_total) : MathMax(0, g_total_height - g_visible_height);
         if (scroll_visible) {
            UpdateSliderPosition();
            UpdateButtonColors();
         }
         ChartRedraw();
         Sleep(200);
      }
      int prepPos = StringFind(conversationHistory, PrepBase, 0);
      if (prepPos >= 0) {
         int endPos = StringFind(conversationHistory, "\n\n", prepPos) + 2;
         if (endPos < 2) endPos = StringLen(conversationHistory);
         string before = StringSubstr(conversationHistory, 0, prepPos);
         string after = StringSubstr(conversationHistory, endPos);
         conversationHistory = before + LoadingPlaceholder + "\n" + timestamp + "\n\n" + after;
      } else {
         conversationHistory += LoadingPlaceholder + "\n" + timestamp + "\n\n";
      }
      UpdateResponseDisplay();
      // Re-apply dynamic scroll after placeholder
      scroll_pos = (new_content_height <= g_visible_height) ? MathMax(0, old_total) : MathMax(0, g_total_height - g_visible_height);
      if (scroll_visible) {
         UpdateSliderPosition();
         UpdateButtonColors();
      }
      ChartRedraw();
      StartTimeMs = GetTickCount();
      Print("Chat ID: " + IntegerToString(current_chat_id) + ", Title: " + current_title);
      FileWrite(logFileHandle, "Chat ID: " + IntegerToString(current_chat_id) + ", Title: " + current_title);
      Print("User: " + prompt);
      FileWrite(logFileHandle, "User: " + prompt);
      response = GetChatGPTResponse(prompt);
      Print("AI: " + response);
      FileWrite(logFileHandle, "AI: " + response);
      ulong elapsedMs = GetTickCount() - StartTimeMs;
      int elapsedSec = (int)(elapsedMs / 1000);
      string timeNote = "\n(Response in " + IntegerToString(elapsedSec) + "s)";
      int placeholderPos = StringFind(conversationHistory, LoadingPlaceholder, 0);
      if (placeholderPos >= 0) {
         int endPos = StringFind(conversationHistory, "\n\n", placeholderPos) + 2;
         if (endPos < 2) endPos = StringLen(conversationHistory);
         string before = StringSubstr(conversationHistory, 0, placeholderPos);
         string after = StringSubstr(conversationHistory, endPos);
         conversationHistory = before + "AI: " + response + timeNote + "\n" + timestamp + "\n\n" + after;
      } else {
         conversationHistory += "AI: " + response + timeNote + "\n" + timestamp + "\n\n";
      }
      if (StringFind(current_title, "Chat ") == 0) {
         current_title = StringSubstr(prompt, 0, 30);
         if (StringLen(prompt) > 30) current_title += "...";
         UpdateCurrentHistory();
         UpdateSidebarDynamic();
      }
   } else {
      conversationHistory += "AI: " + response + "\n" + timestamp + "\n\n";
   }
   UpdateCurrentHistory();
   UpdateResponseDisplay();
   // For final response: always scroll to bottom (response may be long)
   scroll_pos = MathMax(0, g_total_height - g_visible_height);
   if (scroll_visible) {
      UpdateSliderPosition();
      UpdateButtonColors();
   }
   ChartRedraw();
}

We initiate the "SubmitMessage" routine by verifying if the provided "prompt" possesses content, exiting promptly if vacant, and acquiring the present timestamp via TimeToString employing TimeCurrent alongside "TIME_MINUTES". We declare an empty "response" and configure "send_to_api" as true, subsequently examining whether "prompt" commences with "set title " through StringFind, deriving the fresh title using StringSubstr, revising "current_title", assigning "response" to an affirmation, toggling "send_to_api" to false, and invoking "UpdateCurrentHistory" plus "UpdateSidebarDynamic". We trigger "UpdateResponseDisplay" to capture the prior "g_total_height", attach the user prompt and timestamp to "conversationHistory", and invoke "UpdateResponseDisplay" once more to obtain the height post-prompt, deriving "prompt_height" from the variance.

Should "send_to_api" remain true, we affix "PrepBase" accompanied by timestamp to "conversationHistory", execute "UpdateResponseDisplay" to secure height following loading, determine "loading_height", and "new_content_height" as the aggregate of prompt and loading heights; we adjust "scroll_pos" adaptively via MathMax to retain the former total if new material accommodates "g_visible_height" or shift to the base post-loading otherwise, then if "scroll_visible" engage "UpdateSliderPosition" and "UpdateButtonColors", followed by ChartRedraw. We iterate from 0 to "PreAnimationCycles"-1, deriving "subCycle" as i modulo 3, constructing "dots" by adding periods up to subCycle+1, locating "prepPos" within "conversationHistory" using "StringFind", isolating preceding and succeeding segments via "StringSubstr", refreshing history with "PrepBase" concatenated with dots and timestamp, executing "UpdateResponseDisplay", reestablishing adaptive "scroll_pos", refreshing slider and buttons if visible, redrawing via "ChartRedraw", and pausing 200ms through "Sleep".

We relocate "prepPos" anew, substitute with "LoadingPlaceholder" and timestamp in like manner if detected, otherwise append it, execute "UpdateResponseDisplay", reinstate adaptive "scroll_pos", refresh slider/buttons if visible, and "ChartRedraw". We assign "StartTimeMs" to GetTickCount, log and record to the file the chat ID and title using "Print" and FileWrite, log and record the user prompt, retrieve "response" from "GetChatGPTResponse", log and record the AI reply. We compute "elapsedMs" as "GetTickCount" less "StartTimeMs", "elapsedSec" as whole seconds, form "timeNote" with the response duration text, locate "placeholderPos" for "LoadingPlaceholder", replace with "AI: " concatenated with response, timeNote, timestamp if located, otherwise append, leveraging StringFind and StringSubstr functions. The remainder stays unaltered. We've accentuated the key revisions for emphasis. Notably, live animations are infeasible due to web requests obstructing interactions. Presently, we require auxiliary routines for the icons we've integrated upon their activation.

// Extract last AI response from history
string GetLastAIResponse() {
   int ai_pos = StringFind(conversationHistory, "AI: ", -1); // Search backward
   if (ai_pos < 0) return "";
   int end_pos = StringFind(conversationHistory, "\n\n", ai_pos);
   if (end_pos < 0) end_pos = StringLen(conversationHistory);
   string response = StringSubstr(conversationHistory, ai_pos + 4, end_pos - ai_pos - 4);
   StringTrimLeft(response);
   StringTrimRight(response);
   return response;
}

// Extract last user prompt from history
string GetLastUserPrompt() {
   int you_pos = StringFind(conversationHistory, "You: ", -1); // Search backward
   if (you_pos < 0) return "";
   int ts_start = StringFind(conversationHistory, "\n", you_pos + 5) + 1;
   string prompt = StringSubstr(conversationHistory, you_pos + 5, ts_start - you_pos - 6);
   StringTrimLeft(prompt);
   StringTrimRight(prompt);
   return prompt;
}

// Remove last AI block from history (AI: ... \n timestamp \n\n)
void RemoveLastAIResponse() {
   int last_nn = StringFind(conversationHistory, "\n\n", -1);
   if (last_nn >= 0) {
      int ai_pos = StringFind(conversationHistory, "AI: ", last_nn - 100); // Rough backward search
      if (ai_pos >= 0 && ai_pos < last_nn) {
         conversationHistory = StringSubstr(conversationHistory, 0, ai_pos);
      }
   }
   UpdateCurrentHistory();
}

In this section, we introduce the "GetLastAIResponse" function to pull the latest AI message from "conversationHistory", employing StringFind with -1 for reverse lookup of "AI: ", identifying the termination via "\n\n" or the full string length if absent, extracting the segment post-"AI: " with StringSubstr, trimming excess spaces using StringTrimLeft and StringTrimRight, and yielding it—or an empty string if missing. We develop the "GetLastUserPrompt" function to fetch the most recent user entry, performing a backward search for "You: " via "StringFind", pinpointing the subsequent "\n" after the prompt body, slicing from post-"You: " to pre-timestamp with StringSubstr, trimming, and returning the result or empty if unavailable. We construct the "RemoveLastAIResponse" function to excise the final AI segment from "conversationHistory", detecting the trailing "\n\n" through "StringFind", then reverse-scanning up to 100 characters prior for "AI: ", cropping the history to pre-"ai_pos" using StringSubstr if properly identified, and invoking "UpdateCurrentHistory" to persist alterations. These routines will be triggered upon icon clicks, yet we must first enable detection of those interactions. Below details the approach we apply to accomplish this.

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {

//--- rest of the logic

else if (sparam == "ChatGPT_EditIcon") {
   string response = GetLastAIResponse();
   if (response != "") {
      currentPrompt = response;
      DeletePlaceholder();
      UpdatePromptDisplay();
      p_scroll_pos = MathMax(0, p_total_height - p_visible_height);
      if (p_scroll_visible) {
         UpdatePromptSliderPosition();
         UpdatePromptButtonColors();
      }
      ChartRedraw();
   }
}
else if (sparam == "ChatGPT_RegenIcon") {
   string prompt = GetLastUserPrompt();
   if (prompt != "") {
      RemoveLastAIResponse();
      SubmitMessage(prompt);  // Regenerates
   }
}
else if (sparam == "ChatGPT_ExportIcon") {
   string response = GetLastAIResponse();
   if (response != "") {
      int handle = FileOpen("LastAIResponse.txt", FILE_WRITE | FILE_TXT);
      if (handle != INVALID_HANDLE) {
         FileWrite(handle, response);
         FileClose(handle);
         Print("Exported to LastAIResponse.txt");
      }
   }
}

//--- rest of the logic

}

In this segment, we integrate click handling for the edit icon within the OnChartEvent handler. Upon "sparam" matching "ChatGPT_EditIcon", we extract the latest AI reply via "GetLastAIResponse". If non-empty, we transfer it to "currentPrompt", invoke "DeletePlaceholder", and refresh the prompt view using "UpdatePromptDisplay". Additionally, we position "p_scroll_pos" at the base through MathMax applied to 0 and "p_total_height" less "p_visible_height". Should "p_scroll_visible" hold true, we execute "UpdatePromptSliderPosition" and "UpdatePromptButtonColors" prior to redrawing.

Regarding the regenerate icon, when "sparam" aligns with "ChatGPT_RegenIcon", we obtain the most recent user input through "GetLastUserPrompt", and if populated, eliminate the prior AI reply with "RemoveLastAIResponse" before reissuing the prompt via "SubmitMessage" for a fresh generation. For "sparam" equaling "ChatGPT_ExportIcon", we acquire the last AI reply, and if available, initiate "LastAIResponse.txt" in write-text mode using FileOpen, verify "handle" differs from INVALID_HANDLE, record the reply with FileWrite, seal the file via FileClose, and output a confirmation note. Below illustrates the export operation.

The final UI looks as follows.

As illustrated in the diagram, these enhancements enable us to refine the application through the integration and fine-tuning of novel UI components, thereby fulfilling our goals. The sole pending task is program backtesting, which we'll address in the subsequent segment.


Backtesting Procedures

We've performed the evaluation, and presented here is the aggregated visual depiction within a unified Graphics Interchange Format (GIF) bitmap image structure.

As depicted in the illustration, the UI elements perform adequately, yet upon icon activation, they retrieve the initial message rather than the most recent one, contrary to our aims. Thus, we'll invert the detection sequence to resolve this. The problem stems from our presumption in the iteration, overlooking the requirement for intricate parsing to manage multi-line replies and inputs.

// Extract last user prompt from history
string GetLastUserPrompt() {
   string blocks[];
   int num_blocks = SplitOnString(conversationHistory, "\n\n", blocks);
   if (num_blocks == 0) return "";
   // Find the last You block (reverse)
   for (int i = num_blocks - 1; i >= 0; i--) {
      string block = blocks[i];
      if (StringFind(block, "You: ") == 0) {
         // Extract content after "You: " up to timestamp
         int ts_pos = StringFind(block, "\n", 5); // After "You: "
         if (ts_pos > 0) {
            string prompt = StringSubstr(block, 5, ts_pos - 5);
            StringTrimLeft(prompt);
            StringTrimRight(prompt);
            Print("DEBUG: Full history before extract prompt: " + conversationHistory);
            Print("DEBUG: Last You block: " + block);
            Print("DEBUG: Extracted last prompt: " + prompt);
            return prompt;
         }
      }
   }
   return "";
}

string GetLastAIResponse() {
   // Split entire history into lines
   string lines[];
   int num_lines = StringSplit(conversationHistory, '\n', lines);
   if (num_lines == 0) {
      Print("DEBUG: No lines in history.");
      return "";
   }
   Print("DEBUG: Total lines in history: " + IntegerToString(num_lines));
   for (int j = 0; j < num_lines; j++) {
      Print("DEBUG: History Line " + IntegerToString(j) + ": " + lines[j]);
   }
   // Find start of last AI response (reverse search for "AI: ")
   int ai_start = -1;
   for (int i = num_lines - 1; i >= 0; i--) {
      string trimmed = lines[i];
      StringTrimLeft(trimmed);
      StringTrimRight(trimmed);
      if (StringFind(trimmed, "AI: ") == 0) {
         ai_start = i;
         break;
      }
   }
   if (ai_start == -1) {
      Print("DEBUG: No AI: line found in history. Full history: " + conversationHistory);
      return "";
   }
   Print("DEBUG: Last AI starts at line " + IntegerToString(ai_start));
   string response_build = "";
   // Extract from AI: line
   string first_line = lines[ai_start];
   int prefix_pos = StringFind(first_line, "AI: ");
   if (prefix_pos >= 0) {
      first_line = StringSubstr(first_line, prefix_pos + 4);
      StringTrimLeft(first_line);
      StringTrimRight(first_line);
      if (StringLen(first_line) > 0 && StringFind(first_line, "(Response in ") != 0 && StringFind(first_line, "(Regenerated in ") != 0 && !IsTimestamp(first_line)) {
         response_build = first_line;
      }
   }
   // Collect subsequent lines until next message start (You: or AI: ) or end
   for (int j = ai_start + 1; j < num_lines; j++) {
      string orig_line = lines[j];
      string trimmed = orig_line;
      StringTrimLeft(trimmed);
      StringTrimRight(trimmed);
      // Stop if new message starts
      if (StringFind(trimmed, "You: ") == 0 || StringFind(trimmed, "AI: ") == 0) {
         break;
      }
      // Skip notes and timestamps
      if (StringFind(trimmed, "(Response in ") == 0 || StringFind(trimmed, "(Regenerated in ") == 0 || IsTimestamp(trimmed)) {
         continue;
      }
      // Add original line (preserve empties as \n)
      if (response_build != "") response_build += "\n";
      response_build += orig_line;
   }
   Print("DEBUG: Extracted last response: '" + response_build + "'");
   return response_build;
}

We've incorporated annotations on the pertinent lines for better comprehension, along with debugging statements to verify outputs. You may disable them by commenting them out if unnecessary; however, we'll retain them for future reference and deactivate them during final refinements. Additionally, we've inserted a supplementary newline in the submit routine to accommodate responses containing blanks and line breaks, as demonstrated below.

void SubmitMessage(string prompt) {

//---

   conversationHistory += "You: " + prompt + "\n" + timestamp + "\n\n";  // Add extra \n for separation

//---

}

Following assembly, we attain the subsequent conclusive and rewarding result.



Final Thoughts

To wrap up, we've refined the user interface in our MQL5-based AI trading platform by incorporating smooth loading animations during API setup and processing stages, performance timers showing response times in seconds, and handy utilities such as regenerate controls for retrying queries and export functions for archiving results to files. Paired with interactive hover highlights, resized visuals, and adaptable side panels, these additions deliver a sharper, more engaging user journey, all while preserving a modular structure for straightforward expansions. Looking ahead, we'll delve into sentiment analysis tie-ins or cross-timeframe signal validations to sharpen trading intelligence further. Keep an eye out.


Attachments


 S/N NameType  Description
 1 AI_JSON_FILE.mqh JSON Class Library Class for handling JSON serialization and deserialization
 2 AI_CREATE_OBJECTS_FNS.mqh Object Functions Library Functions for creating visualization objects like labels and buttons
 3 AI_UI_COMPONENTS.mqh User Interface Components Library File containing the User Interface components and their organization
 4 AI_BMP_FILES_ZIP Bitmap Files Zip File containing the Bitmap images
 5 AI_ChatGPT_EA_Part_8.mq5 Main Expert Advisor File Main Expert Advisor for handling AI integration


Disclaimer: The ideas and strategies presented in this resource are solely those of the author and are intended for informational and educational purposes only. They do not constitute financial advice, and past performance is not indicative of future results. All materials, including but not limited to text, images, files, and any downloadable content, are protected by copyright and intellectual property laws and are the exclusive property of Forex Algo-Trader or its licensors. Reproduction, distribution, modification, or commercial use of these materials without prior written consent from Forex Algo-Trader is strictly prohibited and may result in legal action. Users are advised to exercise extreme caution, perform thorough independent research, and consult with qualified financial professionals before implementing any trading strategies or decisions based on this resource, as trading in financial markets involves significant risk of loss.
Banner
🤖 Building a ChatGPT AI Trade Brain in MQL5
What’s the point of an AI trading assistant that talks but never acts? In this article, we transfo...
2026-05-15 16:50:14
Banner
Grid Scalper MA MT5 EA Review (2026): The Forever-Free MetaTrader 5 Robot
Grid Scalper MA MT5 EA Review (2026): The forever-free MetaTrader 5 robot that's quietly earning a 4...
2026-05-14 19:35:18
Banner
Supercharging Your TPO Market Profile With Volume Intelligence
You've got a TPO market profile that tracks where price spent its time — now imagine giving it a f...
2026-04-03 16:05:42