English
preview
MQL5でのAI搭載取引システムの構築(第3回):スクロール対応の単一スレッド型チャットUIへのアップグレード

MQL5でのAI搭載取引システムの構築(第3回):スクロール対応の単一スレッド型チャットUIへのアップグレード

MetaTrader 5トレーディングシステム |
14 0
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

前回の記事(第2回)では、MetaQuotes Language 5 (MQL5)でChatGPTを活用したインタラクティブプログラムとユーザーインターフェース(UI)を構築しました。このツールにより、OpenAIのAPIにプロンプトを送信し、その応答をチャート上で即座に確認することが可能でした。第3回となる今回はこれをさらに進化させ、スクロール可能なチャット形式のダッシュボードを導入します。これにより、タイムスタンプや滑らかな動的スクロール、豊富な会話履歴を備えたマルチターン会話を楽しめる環境を実現します。以下に詳しく説明します。

  1. アップグレード版ChatGPTプログラムのフレームワークの理解
  2. MQL5での実装
  3. ChatGPTプログラムのテスト
  4. 結論

この記事を読み終える頃には、カスタマイズ可能な形で、AIへの問い合わせを対話形式でおこなえる強化されたMQL5プログラムが手に入ります。


アップグレード版ChatGPTプログラムのフレームワークの理解

アップグレード版ChatGPTプログラムフレームワークは、スクロール可能なチャット指向のUIを取り入れることで、AI駆動型の取引インターフェースを強化しています。このUIはマルチターン会話、タイムスタンプ、動的なメッセージ処理をサポートし、セッションをまたいだクエリの文脈を保持できるようにします。これにより、履歴を参照して過去のAI応答を活用しながら取引戦略を改善できる、シームレスな会話体験を提供します。過去のやり取りの洞察を失わずに戦略を精緻化するには、AIが参照できる単一の会話に集中することが有効であると判断しました。これにより、必要に応じてプロンプトを修正したり、改善したりすることが容易になります。

私たちのアプローチは、スクロール可能なテキスト、ホバーエフェクト、APIリクエスト用のメッセージ構築機能を備えた、単一スレッド型チャット指向のダッシュボードを構築することです。このUIは会話の長さやスクロールバーの表示設定など、ユーザーの好みに応じて適応します。さらに、マルチターンのクエリに対応するために履歴を解析するロジックを実装し、タイムスタンプを追加して明瞭性を高め、会話履歴のクリアや新規チャットの開始といった機能もサポートします。これにより、取引判断を補助する継続的なAIサポートを提供する、新しいアップグレード版インターフェースを備えたツールが完成します。以下に、今回達成を目指すアップグレード版UIのロードマップを示します。

アップグレード版UIロードマップ


MQL5での実装

MQL5でアップグレード版プログラムを実装するにあたり、まず入力パラメータのセクションを変更し、スクロールバー表示モード用の新しい入力を追加します。これにより、必要に応じてホバー時にスクロールバーを表示するか、常に表示させるかを選択できるようになります。そのための列挙型を追加し、さらに応答トークンの上限を3000に引き上げます。これにより、十分に内容の充実した会話が可能になりますが、必要に応じてさらに増やすこともできます。

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

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

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

アップグレードの実装は、まずスクロール動作およびAPI設定を制御する構成パラメータと定数を定義することから始めます。まず、ENUM_SCROLLBAR_MODE列挙型を作成し、必要に応じてスクロールバーを常に表示するSCROLL_DYNAMIC_ALWAYS、必要なときにホバーで表示するSCROLL_DYNAMIC_HOVER、スクロールバーを表示せずホイール操作のみでスクロールするSCROLL_WHEEL_ONLYのオプションを用意します。ユーザーの好みに合わせ、入力パラメータScrollbarModeの初期値はSCROLL_DYNAMIC_HOVERに設定します。

次に、UI内で一貫して参照できるよう、スクロールバーオブジェクト名の定数としてSCROLL_LEADER、SCROLL_UP_REC、SCROLL_UP_LABEL、SCROLL_DOWN_REC、SCROLL_DOWN_LABEL、SCROLL_SLIDERを定義します。さらに、表示される応答テキストの長さを制限するため、入力パラメータMaxResponseLengthを3000に設定します。これにより、より長い会話を扱うことが可能になります。最後に、JSONクラスを修正してdouble型の値も処理できるようにします。これは小規模な機能拡張に過ぎません。

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

デシリアライズ関数では、これまでのバージョンで考慮していなかったdouble型の値を処理するようにします。特定の処理箇所は、理解しやすいようにハイライトしています。次に、ヘッダ、フッタ、ボタン、スクロールバーを備えたより複雑なダッシュボードをサポートするために、UIレイアウト、スクロール、ホバー状態、カラーに関する新しいグローバル変数を追加する必要があります。

bool clear_hover = false;
bool new_chat_hover = false;

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

ここでは、アップグレード版プログラムのために、動的なホバー効果やレイアウト管理をサポートするグローバル変数とカラースキームをさらに初期化します。クリアボタンと新規チャットボタンのホバーフラグとしてclear_hoverとnew_chat_hoverをfalseに設定し、元の背景色としてclear_original_bgをclrLightCoral、new_chat_original_bgをclrLightBlueに設定します。ホバー状態用に暗めの色clear_darker_bgとnew_chat_darker_bgも定義します。次に、ダッシュボードの寸法を設定します。g_mainXを10、g_mainYを30、g_mainWidthを550、g_mainHeightを0(後で計算)に設定し、パディング値としてg_paddingを10、g_sidePaddingを6、g_textPaddingを10とします。ヘッダの高さg_headerHeightは40、表示部の高さg_displayHeightは280、フッタの高さg_footerHeightは50、行間g_lineSpacingは2に設定します。

最後に、スクロール関連の変数を初期化します。scroll_visibleとmouse_in_displayをfalseに、scroll_posを0、prev_scroll_posを-1に設定し、スライダーの高さslider_heightを20にします。ドラッグ状態movingStateSliderはfalse、スライダー操作位置mlbDownX_Slider、mlbDownY_Slider、mlbDown_YD_Sliderを0に設定します。高さトラッカーg_total_heightとg_visible_heightも0に初期化します。スクロールバーは動的に更新されるため、表示更新の前に定義する必要があります。次に、スクロールバー関連の関数を定義しましょう。

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

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

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

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

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

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

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

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

レスポンシブで適応型のチャット指向インターフェースを実現するために、getFontSizeByDPI関数では、TerminalInfoIntegerを用いてTERMINAL_SCREEN_DPIから画面DPIを取得し、標準DPI(96)を基準として基本フォントサイズをスケーリングします。また、MathMaxを使って最小サイズを8に制限し、表示環境に関わらずテキストの可読性を維持します。CreateScrollbar関数では、スクロールバーの位置(displayX、displayY)と寸法を計算し、ライトグレーの背景色(C'220,220,220')でリーダー矩形SCROLL_LEADERを作成します。上下ボタンSCROLL_UP_REC、SCROLL_DOWN_RECはclrGainsboroで描画し、ラベルSCROLL_UP_LABEL、SCROLL_DOWN_LABELにはWebdingsの矢印(0x35、0x36)を用い、createLabelおよびcreateRecLabelでDPI調整済みのフォントサイズを適用します。失敗時にはFileWriteStringでログに記録します。

DeleteScrollbar関数は、ObjectDeleteを用いてスクロールバー関連のすべてのオブジェクトを削除し、クリーンアップをおこないます。CalculateSliderHeight関数では、表示中のテキスト比率に基づきスライダーの高さを計算し、最小20ピクセルを保証します。これにより、会話が長くなってもスライダーが小さすぎて操作できなくなることを防ぎます。UpdateSliderPosition関数は、scroll_posとmax_scrollから算出したスクロール比率を用いてスライダーのy位置を調整し、範囲内に制限、更新をログに記録します。UpdateButtonColors関数では、スクロールボタン(最上部と最下部)を無効時にclrSilver、有効時にclrDimGrayで表示し、変更内容をログに記録します。ScrollUpおよびScrollDown関数は、scroll_posを30ピクセル増減させ、UpdateResponseDisplayを呼び出し、スクロールバーが表示されていれば更新し、操作内容をログに記録します。これにより、テキストサイズに応じて動的にスクロール可能なチャットUIが実現されます。

さらに、複雑な会話で長い段落を扱う際には、視認性向上のため空行の処理が必要になります。ここからは、WrapText関数を強化して空行を適切に扱うロジックを紹介します。

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

この関数に初めて触れるわけではないため、最も重要なアップグレード部分に注目して確認します。WrapText関数では、1行あたり最大文字数を60文字(maxChars)に設定し、前の関数と同様にArrayResizeで出力配列wrappedLinesをクリアします。フォントとサイズはTextSetFontで設定し、入力テキストを改行文字で区切って段落ごとに分割します。この段階ではStringSplitを使用して、テキストを段落ごとに分割しています。各段落について、空の段落はwrappedLinesにスペースを追加して次の段落に進みます。

空でない段落はさらにStringSplitで単語ごとに分割し、maxWidth(offsetで調整)と文字数制限の範囲内で単語を追加して行を構築します。TextGetSizeで行幅をチェックし、制限を超えた場合は現在の行をwrappedLinesに追加し、新しい行を現在の単語で開始します。幅や文字数制限を超える長い単語は、1文字ずつ分割して処理し、超えた段階で新しい行に追加し、各セグメントをwrappedLinesに格納します。残った行も最後に出力に追加します。初期化時には、要素の色を設定し、プログラム削除時には新しい要素を削除する必要があります。

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

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

初期化が完了したところで、新しい要素を定義し、表示に追加して、どの段階まで進んだかを確認できるようにしましょう。

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

コアとなるダッシュボードレイアウト関数では、メインコンテナの高さg_mainHeightを、g_headerHeight、g_displayHeight、g_footerHeight、およびg_paddingの2倍の合計として計算します。また、表示エリアの位置displayX、displayYやフッタ位置footerYも、ダッシュボードを静的ではなく動的にしたいため、パディング値を使って決定します。メインコンテナChatGPT_MainContainerとヘッダ背景ChatGPT_HeaderBgは、白とライトグレーの色を使ってcreateRecLabelで作成します。タイトルラベルChatGPT_TitleLabelには「ChatGPT AI EA」を表示し、フォントはArial Rounded MT Bold、サイズ14で、TextGetSizeを用いて位置を調整します。日付ラベルChatGPT_DateLabelは、TimeTradeServerから取得したサーバー時刻をArialサイズ12で表示し、水平中央に配置します。

ヘッダには、[Clear]ボタンChatGPT_ClearButtonと[New Chat]ボタンChatGPT_NewChatButtonをcreateButtonで追加し、それぞれclrLightCoralとclrLightBlueの色、フォントサイズ11で設定します。チャット表示および入力セクションには、応答エリアChatGPT_ResponseBgとフッタChatGPT_FooterBgをcreateRecLabelで作成します。フッタ内には幅448の入力フィールドChatGPT_InputEditと、幅80の送信ボタンChatGPT_SubmitButtonをcreateEditとcreateButtonで中央揃えに配置し、両者の間に10ピクセルの間隔を設けます。最後に、ChartRedraw関数でチャートを再描画します。コンパイルすると、次の結果が得られます。

要素を含む最終的なUI

すべての要素を備えたインターフェースが完成したので、新しい会話履歴を表示に反映する段階に進めます。ただし、会話が表示エリア内に収まるように、メッセージ行やその高さを取得するユーティリティ関数が必要になります。また、これから組み込むタイムスタンプ行にも対応する必要があります。

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

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

ここでは、タイムスタンプの検証とメッセージ表示プロパティの計算をおこなうユーティリティ関数を実装します。IsTimestamp関数では、入力文字列の左右の空白をStringTrimLeftStringTrimRightで除去し、長さが正確に5文字であることを確認します。次に、StringGetCharacterで2文字目にコロンがあるかを検証し、StringSubstrで時間(hours)と分(minutes)を抽出、StringToIntegerで整数に変換します。時間が0~23、分が0~59の範囲に収まる場合にtrueを返すことで、会話履歴における正確なタイムスタンプ検出を保証します。別のルールを採用する場合は、適宜独自の基準を定義してください。

ComputeLinesAndHeight関数では、出力配列allLines_out、lineRoles_out、lineHeights_outをArrayResizeでクリアし、totalLines_outとtotalHeight_outを0に初期化します。各メッセージについては、WrapTextを使用して指定フォント・フォントサイズ・最大幅で内容を折り返します。次に、折り返し後の行とタイムスタンプを収容するために出力配列をリサイズし、各行のテキスト、役割(UserまたはAI)、高さ(内容はadjustedLineHeight、タイムスタンプはadjustedTimestampHeight)をそれぞれallLines_out、lineRoles_out、lineHeights_outに代入します。同時にtotalHeight_outとtotalLines_outを更新します。さらに、メッセージ間にはmessageMarginを追加して視覚的な区切りを設けます(最後のメッセージを除く)。これにより、タイムスタンプの検証とメッセージテキストの整理が行われ、スクロール可能なチャットインターフェース用に表示準備が整います。

これらの関数を用いることで、表示関数を更新し、履歴を役割、内容、タイムスタンプに分解して整列させ、マージンを追加し、スクロールやトリミングを処理し、スクロールバーを動的に表示できるようになります。

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

UpdateResponseDisplay関数では、まず既存のメッセージ関連オブジェクト(ChatGPT_ResponseLine_、ChatGPT_MessageBg_、ChatGPT_MessageText_、ChatGPT_Timestamp_)をObjectsTotalObjectDeleteで削除し、表示をリフレッシュします。会話履歴conversationHistoryが空の場合は、createLabelでユーザーにメッセージ入力を促すデフォルトラベルを作成し、g_total_heightを0にリセット、g_visible_heightを表示高さからパディングを引いた値に設定します。スクロールバーが表示されていればDeleteScrollbarで削除し、ChartRedrawで再描画します。履歴がある場合は、StringSplitで改行ごとに分割し、各行を「You: 」、「AI: 」、およびIsTimestampで判定したタイムスタンプに基づき、msgRoles、msgContents、msgTimestampsに解析して格納します。複数行にまたがる内容はまとめて1つのメッセージとして保存します。

次に、テキストの位置textX、textYや最大幅fullMaxWidthを計算し、フォントサイズはメッセージが10、タイムスタンプが8に設定されます。行高さはTextGetHeightにg_lineSpacingを加えて算出します。ComputeLinesAndHeightを使用して仮の行配列と高さを生成し、スクロールの必要性を判定します。スクロールバーの表示可否はScrollbarModeとmouse_in_displayに基づき決定し、表示される場合はスクロールバー分16ピクセルを確保します。必要に応じて幅を調整して行を再計算し、g_total_heightを更新、スクロールバーはCreateScrollbarまたはDeleteScrollbarで管理し、scroll_posをmax_scrollの範囲内に制限、新しいメッセージが追加された場合は下端に設定します。

表示開始行とy位置をscroll_posに基づき算出し、g_visible_height内に収まる行を描画します。各行はcreateLabelで作成し、AIメッセージは左寄せ(clrBlue)、ユーザーメッセージは右寄せ(clrGray)、タイムスタンプはclrDarkGrayで表示し、メッセージ間には12ピクセルのマージンを適用します。最後に、表示内容をFileWriteStringでログに記録し、ChartRedrawで再描画します。これにより、表示エリアには会話履歴が正しく反映されます。次のステップとして、送信ボタンを押した際にプロンプトを送信できるようにし、既存の関数を複数の関数に分割して将来のバージョンで管理しやすくします。

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

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

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

ここでは、APIとの中核的な連携処理およびメッセージ管理をおこなう関数を実装します。まず、BuildMessagesFromHistory関数を定義します。この関数では、conversationHistoryを解析してAPIリクエスト用のJSON配列を構築します。StringFindStringSubstrを用いて、ユーザーメッセージ(「You: 」)およびAIメッセージ(「AI: 」)を抽出し、タイムスタンプ行や空行はスキップします。各メッセージ内容はJsonEscapeでエスケープ処理をおこない、役割をuserまたはassistantとしてJSONオブジェクトに整形します。最後に、新しく入力されたユーザープロンプトを最終メッセージとして追加することで、マルチターン会話に対応した正しい形式の配列を生成します。

次に、GetChatGPTResponse関数では、BuildMessagesFromHistoryで生成したメッセージ配列に、OpenAI_ModelとMaxResponseLengthを含めたJSONリクエストを作成します。これをStringToCharArrayで文字配列に変換し、OpenAI_API_Keyを含むヘッダを設定した上で、WebRequest関数を用いてOpenAI_EndpointへPOSTリクエストを送信します。レスポンス処理では、HTTPステータスが200以外の場合をエラーとして扱い、生データやエラー内容をFileWriteStringでlogFileHandleに記録します。正常時は、JsonValue::DeserializeFromArrayでJSONレスポンスを解析し、APIエラーの有無を確認した後、choices[0][message][content]から応答テキストを抽出します。改行のアンエスケープと空白のトリミングをおこない、従来バージョンと同様の形式で結果を返します。

SubmitMessage関数では、ObjectGetStringを使ってChatGPT_InputEditからユーザー入力を取得し、空でなければGetChatGPTResponseを呼び出します。プロンプトと応答はPrintでログ出力され、TimeCurrentで取得したタイムスタンプ付きでconversationHistoryに追加されます。その後、ObjectSetStringで入力欄をクリアし、UpdateResponseDisplayで表示を更新、scroll_posを設定して最下部へスクロールし、必要に応じてスクロールバー表示も更新します。これらの操作内容もログに記録されます。この仕組みにより、AIとの会話管理、API通信、チャットUIの動的更新が一体となったシステムが構築されます。この関数は、[Send]ボタンをクリックした際に呼び出すことで動作します。

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

チャート上の操作を処理するために、イベントリスナーとしてOnChartEventイベントハンドラを使用します。イベントがボタンのクリックである場合、プロンプトを送信するための関数を呼び出します。以下は、その結果を可視化したものです。

最初のプロンプト

画像から分かるように、会話はより長く、直感的な構成になっており、ユーザーの発言は右側、AIの応答は左側に表示され、すべてにタイムスタンプが付与されています。次に残る課題は、スクロールバーのドラッグ操作や、追加したボタンのホバー状態を有効にするなど、インタラクティブな表示を確実に実装することです。以下に、そのために使用した完全なロジックを示します。

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

完全なインタラクティブ性を実現するため、OnChartEvent関数内で各UI要素の位置とサイズを計算します。表示エリア(displayX、displayY、displayW、displayH)、[Clear]ボタン(clearX、clearY、clearW、clearH)、[New Chat]ボタン(new_chat_x、new_chat_w、new_chat_h)、送信ボタン(sendX、sendY、sendW、sendH)は、グローバルなレイアウト変数を用いて算出されます。クリックイベント(CHARTEVENT_OBJECT_CLICK),では、ChatGPT_ClearButtonおよびChatGPT_NewChatButtonが押された場合にconversationHistoryをクリアし、scroll_posとprev_scroll_posをリセットし、ObjectSetStringで入力フィールドを消去した後、UpdateResponseDisplayで表示を更新します。また、スクロールボタン(SCROLL_UP_REC、SCROLL_UP_LABEL、SCROLL_DOWN_REC、SCROLL_DOWN_LABEL)がクリックされた場合は、ScrollUpまたはScrollDown関数を呼び出します。

マウス移動イベント(CHARTEVENT_MOUSE_MOVE)では、[Send]、[Clear]、[New Chat]ボタンにマウスを移動したことを検出し、マウスを移動したときに背景(button_darker_bg、clear_darker_bg、new_chat_darker_bg)をObjectSetIntegerで更新し、マウスが表示領域内にあるかどうかを確認してmouse_in_displayを切り替え、ChartSetIntegerでチャートのスクロールを更新し、SCROLL_DYNAMIC_HOVERモードで表示を更新します。

スライダーのドラッグ処理では、SCROLL_SLIDERがクリックされたことを検出してmovingStateSliderを有効にし、マウス移動量に応じてObjectSetIntegerでスライダーのy位置を更新します。スクロール比率からscroll_posを算出し、FileWriteStringでログに記録します。マウスリリース時にはドラッグ状態を解除し、スライダーの色を元に戻します。マウスホイールイベント(CHARTEVENT_MOUSE_WHEEL)では、ホイールの方向に応じてscroll_posを30ピクセル単位で調整し、表示を更新し、スクロールバーが表示されている場合はその見た目も更新します。さらに、スクロールバー上でのホバー効果も管理し、上下ボタンやスライダーの色を適切に切り替えます。各操作の後にはChartRedrawが呼び出され、即座に視覚的な更新が反映されます。これにより、プログラムはクリック、ホバー、ドラッグ、スクロールといった一連の操作をすべてサポートするようになります。最終的な結果は次のとおりです。

アップグレード版と旧バージョンの比較

画像から分かるように、新しい要素を追加し、スクロール可能な会話履歴を表示し、インターフェースを完全に操作可能にすることで、当初の目的を達成できました。残っている作業は、このプログラムのバックテストをおこなうことです。バックテストについては次のセクションで扱います。


ChatGPTプログラムのテスト

テストを実施しました。以下はコンパイル後の可視化を単一のGraphics Interchange Format (GIF)ビットマップ画像形式で示したものです。

バックテストGIF


結論

本記事では、MQL5に統合されたChatGPTプログラムを拡張し、スクロール可能な単一スレッド型チャット指向のUIへとアップグレードしました。動的なJSON解析、タイムスタンプ付きの会話履歴、送信、クリア、新規チャットといったインタラクティブな操作ボタンを実装することで、AIとの対話性と実用性を大きく向上させています。このシステムにより、市場分析におけるAI主導の洞察とシームレスにやり取りでき、マルチターン会話を通じて文脈を維持しながら、適応的なスクロールやホバー効果によって操作性も最適化されています。今後のバージョンでは、双方向の会話をさらに自然に扱える表示更新や、ライブデータの共有を通じた取引インサイトの取得にも対応していく予定です。どうぞご期待ください。

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/19741

添付されたファイル |
知っておくべきMQL5ウィザードのテクニック(第81回): β-VAE推論学習で一目均衡表とADX-Wilderのパターンを利用する 知っておくべきMQL5ウィザードのテクニック(第81回): β-VAE推論学習で一目均衡表とADX-Wilderのパターンを利用する
本記事は第80回の続編です。前回は、強化学習フレームワーク下で一目均衡表とADXの組み合わせを検証しました。今回は焦点を推論学習に移します。一目均衡表とADXは前回も述べた通り補完的な指標ですが、今回は前回の記事で触れたパイプライン使用に関する結論を再検討します。推論学習には、変分オートエンコーダのβアルゴリズムを用います。また、MQL5ウィザードとの統合を目的として設計されたカスタムシグナルクラスの実装も継続します。
プライスアクション分析ツールキットの開発(第43回):ローソク足の確率とブレイクアウト プライスアクション分析ツールキットの開発(第43回):ローソク足の確率とブレイクアウト
MQL5ネイティブで開発されたCandlestick Probability EAは、ローソク足データをリアルタイムかつ銘柄別の確率情報へと変換する、軽量で実用的な分析ツールです。本EAは、バー確定時にピンバー、包み足、および十字線といったパターンを分類し、ATRを考慮したフィルタリングや、任意でブレイクアウト確認をおこないます。さらに、各パターンについて、純粋なフォロー率および出来高加重フォロー率を算出することで、特定の銘柄や時間足における典型的な結果を把握することが可能です。チャート上のマーカー、コンパクトなダッシュボード、インタラクティブな切り替え機能により、検証作業や分析対象への集中を容易にします。また、詳細なCSVログをエクスポートできるため、オフラインでの検証や追加分析にも対応しています。本EAを活用することで、確率プロフィールの構築、戦略の最適化をおこない、ローソク足パターン認識を定量的な優位性へと変換できます。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
初心者からエキスパートへ:MQL5を使用したバックエンド操作モニター 初心者からエキスパートへ:MQL5を使用したバックエンド操作モニター
取引システムの内部動作を意識せずに、既製のソリューションをそのまま利用することは一見すると安心に思えますが、開発者にとっては必ずしもそうとは限りません。いずれアップデートや動作不良、あるいは予期しないエラーが発生し、その原因がどこにあるのかを正確に突き止め、迅速に診断して解決する必要に迫られます。本記事では、取引用エキスパートアドバイザー(EA)の裏側で通常どのような操作がおこなわれているのかを明らかにするとともに、MQL5を用いてバックエンド操作を表示し、記録するための専用カスタムクラスを開発します。これにより、開発者およびトレーダーの双方が、エラーの特定、挙動の監視、EAごとの診断情報に迅速にアクセスできるようになります。