MQL5でのAI搭載取引システムの構築(第3回):スクロール対応の単一スレッド型チャットUIへのアップグレード
はじめに
前回の記事(第2回)では、MetaQuotes Language 5 (MQL5)でChatGPTを活用したインタラクティブプログラムとユーザーインターフェース(UI)を構築しました。このツールにより、OpenAIのAPIにプロンプトを送信し、その応答をチャート上で即座に確認することが可能でした。第3回となる今回はこれをさらに進化させ、スクロール可能なチャット形式のダッシュボードを導入します。これにより、タイムスタンプや滑らかな動的スクロール、豊富な会話履歴を備えたマルチターン会話を楽しめる環境を実現します。以下に詳しく説明します。
この記事を読み終える頃には、カスタマイズ可能な形で、AIへの問い合わせを対話形式でおこなえる強化されたMQL5プログラムが手に入ります。
アップグレード版ChatGPTプログラムのフレームワークの理解
アップグレード版ChatGPTプログラムフレームワークは、スクロール可能なチャット指向のUIを取り入れることで、AI駆動型の取引インターフェースを強化しています。このUIはマルチターン会話、タイムスタンプ、動的なメッセージ処理をサポートし、セッションをまたいだクエリの文脈を保持できるようにします。これにより、履歴を参照して過去のAI応答を活用しながら取引戦略を改善できる、シームレスな会話体験を提供します。過去のやり取りの洞察を失わずに戦略を精緻化するには、AIが参照できる単一の会話に集中することが有効であると判断しました。これにより、必要に応じてプロンプトを修正したり、改善したりすることが容易になります。
私たちのアプローチは、スクロール可能なテキスト、ホバーエフェクト、APIリクエスト用のメッセージ構築機能を備えた、単一スレッド型チャット指向のダッシュボードを構築することです。このUIは会話の長さやスクロールバーの表示設定など、ユーザーの好みに応じて適応します。さらに、マルチターンのクエリに対応するために履歴を解析するロジックを実装し、タイムスタンプを追加して明瞭性を高め、会話履歴のクリアや新規チャットの開始といった機能もサポートします。これにより、取引判断を補助する継続的なAIサポートを提供する、新しいアップグレード版インターフェースを備えたツールが完成します。以下に、今回達成を目指すアップグレード版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 ¤tIndex) { //--- 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関数でチャートを再描画します。コンパイルすると、次の結果が得られます。

すべての要素を備えたインターフェースが完成したので、新しい会話履歴を表示に反映する段階に進めます。ただし、会話が表示エリア内に収まるように、メッセージ行やその高さを取得するユーティリティ関数が必要になります。また、これから組み込むタイムスタンプ行にも対応する必要があります。
//+------------------------------------------------------------------+ //| Check if string is a timestamp in HH:MM format | //+------------------------------------------------------------------+ bool IsTimestamp(string line) { StringTrimLeft(line); //--- Trim left whitespace StringTrimRight(line); //--- Trim right whitespace if (StringLen(line) != 5) return false; //--- Check length if (StringGetCharacter(line, 2) != ':') return false; //--- Check colon string hh = StringSubstr(line, 0, 2); //--- Extract hours string mm = StringSubstr(line, 3, 2); //--- Extract minutes int h = (int)StringToInteger(hh); //--- Convert hours to integer int m = (int)StringToInteger(mm); //--- Convert minutes to integer if (h < 0 || h > 23 || m < 0 || m > 59) return false; //--- Validate time return true; //--- Confirm valid timestamp } //+------------------------------------------------------------------+ //| Compute lines and height for messages | //+------------------------------------------------------------------+ void ComputeLinesAndHeight(const string &font, const int fontSize, const int timestampFontSize, const int adjustedLineHeight, const int adjustedTimestampHeight, const int messageMargin, const int maxTextWidth, const string &msgRoles[], const string &msgContents[], const string &msgTimestamps[], const int numMessages, int &totalHeight_out, int &totalLines_out, string &allLines_out[], string &lineRoles_out[], int &lineHeights_out[]) { ArrayResize(allLines_out, 0); //--- Clear lines array ArrayResize(lineRoles_out, 0); //--- Clear roles array ArrayResize(lineHeights_out, 0); //--- Clear heights array totalLines_out = 0; //--- Initialize total lines totalHeight_out = 0; //--- Initialize total height for (int m = 0; m < numMessages; m++) { //--- Iterate through messages string wrappedLines[]; //--- Declare wrapped lines WrapText(msgContents[m], font, fontSize, maxTextWidth, wrappedLines); //--- Wrap message content int numLines = ArraySize(wrappedLines); //--- Get number of lines int currSize = ArraySize(allLines_out); //--- Get current size ArrayResize(allLines_out, currSize + numLines + 1); //--- Resize lines array ArrayResize(lineRoles_out, currSize + numLines + 1); //--- Resize roles array ArrayResize(lineHeights_out, currSize + numLines + 1); //--- Resize heights array for (int l = 0; l < numLines; l++) { //--- Iterate through wrapped lines allLines_out[currSize + l] = wrappedLines[l]; //--- Add line lineRoles_out[currSize + l] = msgRoles[m]; //--- Add role lineHeights_out[currSize + l] = adjustedLineHeight; //--- Set line height totalHeight_out += adjustedLineHeight; //--- Update total height } allLines_out[currSize + numLines] = msgTimestamps[m]; //--- Add timestamp lineRoles_out[currSize + numLines] = msgRoles[m] + "_timestamp"; //--- Add timestamp role lineHeights_out[currSize + numLines] = adjustedTimestampHeight; //--- Set timestamp height totalHeight_out += adjustedTimestampHeight; //--- Update total height totalLines_out += numLines + 1; //--- Update total lines if (m < numMessages - 1) { //--- Check for margin totalHeight_out += messageMargin; //--- Add message margin } } }
ここでは、タイムスタンプの検証とメッセージ表示プロパティの計算をおこなうユーティリティ関数を実装します。IsTimestamp関数では、入力文字列の左右の空白をStringTrimLeftとStringTrimRightで除去し、長さが正確に5文字であることを確認します。次に、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_)をObjectsTotalとObjectDeleteで削除し、表示をリフレッシュします。会話履歴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配列を構築します。StringFindとStringSubstrを用いて、ユーザーメッセージ(「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)ビットマップ画像形式で示したものです。

結論
本記事では、MQL5に統合されたChatGPTプログラムを拡張し、スクロール可能な単一スレッド型チャット指向のUIへとアップグレードしました。動的なJSON解析、タイムスタンプ付きの会話履歴、送信、クリア、新規チャットといったインタラクティブな操作ボタンを実装することで、AIとの対話性と実用性を大きく向上させています。このシステムにより、市場分析におけるAI主導の洞察とシームレスにやり取りでき、マルチターン会話を通じて文脈を維持しながら、適応的なスクロールやホバー効果によって操作性も最適化されています。今後のバージョンでは、双方向の会話をさらに自然に扱える表示更新や、ライブデータの共有を通じた取引インサイトの取得にも対応していく予定です。どうぞご期待ください。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/19741
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
知っておくべきMQL5ウィザードのテクニック(第81回): β-VAE推論学習で一目均衡表とADX-Wilderのパターンを利用する
プライスアクション分析ツールキットの開発(第43回):ローソク足の確率とブレイクアウト
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
初心者からエキスパートへ:MQL5を使用したバックエンド操作モニター
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索