English Deutsch
preview
MQL5でのAI搭載取引システムの構築(第2回):ChatGPT統合型アプリケーションのUI開発

MQL5でのAI搭載取引システムの構築(第2回):ChatGPT統合型アプリケーションのUI開発

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

はじめに

前回の記事(第1回)では、MetaQuotes Language 5 (MQL5)でJSON (JavaScript Object Notation)解析フレームワークをクラスとして開発しました。このフレームワークにより、AI(人工知能)API(アプリケーションプログラミングインターフェース)とのデータやり取りに必要なJSONのシリアライズとデシリアライズが可能になりました。第2回では、このJSONフレームワークを活用して、ChatGPTを統合したユーザーインターフェース付きプログラムを作成します。本プログラムでは、OpenAIのAPIにプロンプトを送信し、その応答をMetaTrader 5のチャート上に表示することで、インタラクティブなAI駆動型の取引インサイトを提供します。本記事では以下のトピックを扱います。

  1. ChatGPT AIプログラムフレームワークの理解
  2. OpenAI APIアクセスとMetaTrader 5設定の準備
  3. MQL5での実装
  4. ChatGPTプログラムのテスト
  5. 結論

この記事を通して、ChatGPTを統合したインタラクティブなMQL5プログラムを完成させ、取引戦略の強化に活用できるようになります。それでは、早速始めましょう。


ChatGPT AIプログラムフレームワークの理解

本記事で作成を目指すChatGPT AIプログラムは、AIモデルの機能をMQL5に統合することで、プロンプトを送信し、titlehttps://www.metatrader5.com/titleMetaTrader 5 のチャート上で直接AIからのインサイトを受け取ることを可能にします。これは前回の記事で構築したJSON解析の基盤を活用したものです。本プログラムは、クエリを入力してAIの応答を確認できるユーザーフレンドリーなインターフェースを提供し、市場の分析や戦略提案が可能なAI駆動型取引システムへの実用的な一歩となります。

プログラムの設計としては、入力フィールド、送信ボタン、応答表示エリアを備えたダッシュボードを作成し、AIにクエリを送信して整形された応答を確認できるようにします。また、すべてのやり取りをログに記録し、デバッグを容易にします。AI APIとの通信、応答の処理、表示を適切におこなうようプログラムを設定することで、将来的にはAIのインサイトに基づく自動取引機能を拡張する土台を整えます。以下は、目指すプログラムのイメージです。

プログラムフレームワーク

APIとMetaTrader 5の設定に進みましょう。


OpenAI APIアクセスとMetaTrader 5設定の準備

ChatGPT AIプログラムがOpenAI APIと通信できるようにするためには、有効なAPIキーを取得し、MetaTrader 5 (MT5)でHTTPリクエストを許可する設定が必要です。本セッションでは、OpenAI APIキーの取得方法、curlによる動作確認、そしてMT5側でAPIエンドポイント(URL (Uniform Resource Locator))を許可するための設定手順について説明します。

OpenAI APIキーの取得

OpenAIアカウントの作成:platform.openai.comにアクセスし、アカウントを作成またはログインします。APIセクションにアクセスして、「API Keys」ダッシュボードから新しいAPIキーを生成します。生成されたキー(例:「sk-」で始まる文字列)はプログラム設定で使用するため、安全に保管してください。

初回ロード画面

最初のランディングページ

秘密鍵の生成

API秘密鍵生成

APIキーの保護:APIキーはOpenAIサービスへのアクセス権限を持つため、厳重に管理してください。漏洩した場合はOpenAI側で無効化される可能性があります。

curlによるAPIキーの動作確認

APIキーが正しく機能しているか確認するため、curlを使用してAPIリクエストを実行します。

  • Curlのインストール:curlがシステムにインストールされていることを確認します(Linux/macOSでは多くの環境に標準搭載されています。Windowsでは必要に応じてダウンロードします)。
  • テストリクエストの実行:コマンドライン(Windowsではコマンドプロンプト)を開き、以下のコマンドを実行します。<YOUR_API_KEY>は実際のOpenAI APIキーに置き換えてください。

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

  • 応答の確認:HTTP 200が返ってきた場合は成功で、choices配列の中にAIの応答を含むJSONオブジェクトが返されます(例:“content”:“This is a test response”)。errorフィールド(例:APIキーが無効、クォータ超過など)が含まれる場合は、キーの有効性やOpenAIアカウントの請求状態を確認してください。ログを出力して動作を確認できます。成功時は次のような結果が表示されます。

CMDでのAPI CURLテスト

この手順が難しい場合には、次のように簡単なテストファイルを作成し、ブラウザで実行する方法もあります。

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

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

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

これをHTMLファイル(例:api_test.html)として保存し、Webブラウザで開いてキーを追加します。次のインターフェースが表示されます。

プログラムインターフェース

[Test API]をクリックすると、次のような応答が返されます。

テスト応答

MetaTrader 5でのAPI通信設定

OpenAIのエンドポイントへHTTPリクエストを送信するために、取引ターミナルで以下の設定をおこないます。

  • Webリクエストを有効にする:MT5を開き、[ツール]>[オプション]>[エキスパートアドバイザ]に進み、[WebRequestを許可するURLリスト]を有効にします。
  • APIエンドポイントを追加する:[WebRequestを許可するURLリスト]フィールドにhttps://api.openai.comを追加して、OpenAIの APIへのリクエストを許可します。これにより、WebRequest機能によるAPI通信がMT5のセキュリティ設定によってブロックされずに実行できます。

完全な視覚化は次のとおりです。

MT5 WEBリクエストリンクエンドポイント

この設定により、プログラムはプロンプトを送信し、AI応答を受信できるようになります。次はプログラムの構築に進みます。


MQL5での実装

MQL5でプログラムを作成するには、まずMetaEditorを開き、ナビゲータに移動して、インジケーターフォルダを見つけ、[新規]タブをクリックして、表示される手順に従ってファイルを作成します。ファイルが作成されたら、コーディング環境で、まずプログラム全体で使用するグローバル変数入力をいくつか宣言する必要があります。

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

//--- Input parameters
input string OpenAI_Model = "gpt-3.5-turbo";                                 // OpenAI Model
input string OpenAI_Endpoint = "https://api.openai.com/v1/chat/completions"; // OpenAI API Endpoint
input int MaxResponseLength = 500;                                           // Max length of ChatGPT response to display
input string LogFileName = "ChatGPT_EA_Log.txt";                             // Log file name
//--- Hardcoded API key (confirmed valid via curl test)
//--- Keep your API key private, for us, we added some parts so you can get the actual thing you should be having
string OpenAI_API_Key = "sk-proj-vjKCf ... <YOUR FULL ACTUAL API KEY HERE> ... jY..79n...";

プログラムの実装を開始するにあたり、まずOpenAI API への接続を構成するための入力パラメータを初期設定します。最初に、入力パラメータ「OpenAI_Model」を文字列として定義し、「gpt-3.5-turbo」を設定します。これにより、APIリクエストで使用するChatGPTモデルを指定でき、必要に応じてモデルを切り替える柔軟性を持たせています。ここを変更することで、任意のモデルを使用できます。次に、OpenAI_Endpointを「https://api.openai.com/v1/chat/completions」に設定し、OpenAI APIへHTTP POSTリクエストを送信するためのURLを定義します。

さらに、MaxResponseLengthを整数値500に設定し、MetaTrader 5のチャート上で表示されるChatGPTの応答長を制限することで、出力が扱いやすいようにします。最後に、ログファイル名を指定するためLogFileNameを「ChatGPT_EA_Log.txt」とし、APIとのやり取りやエラーを記録できるようにします。また、認証のためのOpenAI_API_Keyには、有効で確認済みのAPIキー(「sk-proj-」で始まるもの)をソースコード内に直接記述し、プログラムの基盤となる構成を整えます。ログ出力およびボタン表示のために、以下の変数も追加します。

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

変数にはコメントを追加し、内容が自明となるようにしました。次におこなうべきことは、インターフェースを作成するために使用するヘルパー関数を定義することです。ただし、複数のファイルを作成したくないため、その前に前の部分で使用したJSONロジックを必ずコピー&ペーストしておく必要があります。プログラムがより複雑になった段階で、別ファイルを作成しインクルードする方法を取ることも可能ですが、現時点では処理をシンプルで分かりやすい形に保つため、すべて1つのファイルで進めていきます。

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

ここでは、トレーダーがチャート上で操作できる主要なグラフィカル要素を作成します。まず、createRecLabel関数を実装し、ObjectCreateを使用して指定した座標(xDistance, yDistance)とサイズ(xSize, ySize)に矩形ラベル(OBJ_RECTANGLE_LABEL)を描画します。この際、背景色(OBJPROP_BGCOLOR)、枠線の幅、色(OBJPROP_COLOR)、枠線タイプ(OBJPROP_BORDER_TYPEをBORDER_FLAT)、スタイル(OBJPROP_STYLEをSTYLE_SOLID)、コーナー位置(OBJPROP_CORNERをCORNER_LEFT_UPPER)などのプロパティをObjectSetIntegerで設定します。また、選択不可で非背景表示としたうえで、ChartRedrawでチャートを更新し、作成に失敗した場合はPrint関数でエラーを記録し、falseを返します。

次に、createButton関数を実装します。この関数では、同様の座標とサイズを指定してボタン(OBJ_BUTTON)を作成し、テキスト(OBJPROP_TEXT)、テキストカラー(OBJPROP_COLOR)、フォントサイズ、フォント名(Arial Rounded MT Bold)、背景色、枠線色、コーナー位置などをObjectSetStringおよびObjectSetIntegerで設定します。必要に応じて背景フラグ(isBack)を指定でき、選択不可かつ押下状態が残らないように設定します。チャート更新をおこない、作成に失敗した場合はエラーをログ出力してfalseを返します。

続いて、createEdit関数を実装します。この関数は編集可能なテキストフィールド(OBJ_EDIT)を作成し、テキスト、アライメント(OBJPROP_ALIGNをALIGN_LEFT)、読み取り専用設定、その他の外観プロパティをObjectSetStringとObjectSetIntegerで設定します。作成後にチャートを更新し、エラー発生時にはログを出力してfalseを返します。最後に、createLabel関数を追加します。この関数はテキストラベル(OBJ_LABEL)を作成し、テキスト、色、フォントサイズ、フォント名、コーナー位置、およびアンカー(OBJPROP_ANCHORをANCHOR_LEFT_UPPER)を設定します。選択不可として表示し、チャートを更新します。これらの関数により、本プログラムのダッシュボードに必要なグラフィカルな基盤が整い、AIとのやり取りの入力および表示が可能になります。また、ホバー時に暗くなるホバー可能ボタンも実装したいため、そのための関数も用意します。

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

ここでは、DarkenColor関数を実装します。この関数はカラー値と任意の係数(デフォルトは0.8)を受け取り、明度を下げた色を生成します。具体的には、ビット演算を用いて赤成分(colorValue & 0xFF)、緑成分((colorValue >> 8) & 0xFF)、青成分((colorValue >> 16) & 0xFF)をそれぞれ抽出し、各成分に係数を掛けて暗くした値を計算します。その後、「red | (green << 8) | (blue << 16)」のようにビットシフトを組み合わせて再構成し、新しい暗色カラーを返します。これで、ダッシュボードの作成準備が整います。

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

ここでは、チャート上にユーザーインターフェースを構築するためのCreateDashboard関数を実装し、AIとの対話がおこなえるようにします。まず、createEditを呼び出し、座標(20,20)、サイズ400×40ピクセルで編集可能なテキストフィールドChatGPT_InputEditを作成します。このフィールドは黒色テキスト、12ポイントのArialフォント、WhiteSmoke背景、ダークグレーの枠線、左寄せ、読み取り専用ではない設定とし、プロンプトを入力できるようにします。次に、createButtonを使用して、座標(430,20)、サイズ100×40ピクセルでChatGPT_SubmitButtonを追加します。テキストは「Send」、白色テキスト、12ポイントのArialフォント、背景色はロイヤルブルー(button_original_bg)、枠線はダークブルーとし、チャート左上付近に配置してAPIリクエストを送信するためのボタンとします。

続いて、createRecLabelを呼び出し、座標(20,70)、サイズ510×300ピクセルで応答背景「ChatGPT_ResponseBg」を作成します。背景色は白、枠線は2ピクセルのライトグレー、フラットな枠線タイプ、ソリッドスタイルとし、AIの応答を表示するための明確な領域を確保します。最後に、ChartRedrawを呼び出してチャートを更新し、すべてのUI要素が表示されるようにします。この関数をOnInitイベントハンドラから呼び出すことで、作成したUIを確認できます。

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

OnInit関数では、まずDarkenColorをbutton_original_bgに対して呼び出し、button_darker_bgとなる暗めの色を計算します。これにより、送信ボタンにホバー時の暗転効果を適用できるようにします。続いて、LogFileNameで指定されたログファイルをFileOpenで開き、読み書きおよびテキストモードのフラグを指定してハンドルをlogFileHandleに保存します。ハンドルが無効であった場合、エラーログを出力し、INIT_FAILEDを返して初期化に失敗したことを示します。これにより、バックグラウンドで発生する処理の記録を確実におこなえるようになります。

次に、FileSeekを使用してログファイルの末尾へ移動し、FileWriteStringで初期化メッセージを書き込みます。この際、時刻情報はTimeToStringを利用して付加します。その後、CreateDashboardを呼び出してユーザーインターフェースを構築し、最後にINIT_SUCCEEDEDを返して初期化が成功したことを示します。OnDeinit関数では、ObjectsDeleteAllを呼び出し、「ChatGPT_」で始まるすべてのチャートオブジェクトを削除してクリーンアップをおこないます。また、logFileHandleが有効な場合はFileCloseでログファイルを閉じます。プログラムがチャートにアタッチされている間はログファイルを開いたまま保持するため、実行中にそのファイルを別途開くことはできません。送受信ごとにファイルを閉じる方式もありますが、アクティブにやり取りしている場合には都合が悪く、この方式がより実用的です。プログラムを実行すると、下のような結果が得られます。

UIが作成された

インターフェースを作成できたので、次は会話内容を通常のチャットのように表示する方法を考える必要があります。しかし、MQL5にはこれを直接実現する手段がありません。ラベル関数には、1つのラベルにつき最大63文字しか表示できないという制限があります。そこで必要となるのは、テキストを複数のラベルに分割して表示するためのテキストラッピング機構です。つまり、会話テキストを解析して複数のラベルに分割し、各ラベルの文字数を上限以内に収めるように管理する仕組みを実装します。以下に処理ロジックを示します。

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

ここでは、チャート上のテキストを読みやすく表示するため、WrapText関数を実装します。これはAIの応答を視認しやすく整形する目的で使用します。まず、1行あたりの長さ制限としてmaxCharsを60に設定します(63文字に上限を合わせることも可能です)。次に、出力配列wrappedLinesをArrayResizeでクリアし、指定されたフォントとフォントサイズ(-fontSize * 10でスケール)をTextSetFontで設定します。続いて、入力テキストを段落ごとに処理するため、改行文字を区切りにStringSplitで分割し、空ではない各段落について処理をおこないます。それぞれの段落に対し、スペースを区切りとして再度StringSplitを使って単語単位に分割し、currentLineを空の状態で初期化します。

その後、単語をループ処理し、currentLineが空でなければスペースを挟んで単語を追加したtestLineを構築し、TextGetSizeで表示幅を取得します。幅がmaxWidth以内で、かつ文字数がmaxChars以内である場合はcurrentLineを更新します。制限を超える場合は、currentLineが空でなければwrappedLinesに追加し、新たに現在の単語で行を開始します。単語自体が極端に長い場合は、文字単位でループ処理をおこない、wrappedWordとして1文字ずつ追加しながら幅を確認し、maxWidthまたはmaxCharsを超えた場合はwrappedLinesに追加し、残りの部分を新たな行として処理します。最後に、currentLineが空でない場合は最終行としてwrappedLinesに追加します。これにより、すべてのテキストがプログラムの応答表示エリアで読みやすい形式に整形されます。このロジックを用いて、ユーザーに開始方法を案内するサンプルテキストを画面に表示できるようになります。

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

ここでは、会話履歴を動的に描画するためにUpdateResponseDisplay関数を実装します。まず、ObjectsTotalを使用してチャート上のオブジェクト数を取得し、逆順でループ処理をおこない、ObjectNameで各オブジェクト名を取得します。そして、名前が「ChatGPT_ResponseLine_」または「ChatGPT_MessageBg_」で始まる場合はObjectDeleteを呼び出して削除し、以前の表示要素をクリアします。次に、conversationHistoryが空であるかを確認します。空の場合は、ユーザーにメッセージ入力を促すため、座標(30,80)にグレー、10ポイントArialフォントでプレースホルダーとなるメッセージをcreateLabelにより表示し、再描画後に処理を終了します。

conversationHistoryにテキストが含まれている場合は、フォントをArial、フォントサイズを10、paddingを10に設定し、応答領域の幅510ピクセルから左右のpadding×2を引いた値をmaxWidthとして計算します。その後、WrapTextを呼び出してテキストをwrappedLinesに分割します。続いて、TextSetFontTextGetSizeで行の高さを取得し、300ピクセルの表示領域から計算してmaxVisibleLinesを算出します。さらに、MathMaxを使用して最新の行が表示されるように開始行を決定します。続いて、wrappedLinesのstartLineから順にループ処理し、行の内容に応じて色を分けます。具体的には、「You: 」で始まる行はグレー、「AI: 」で始まる行はブルーをcurrentColorとして設定し、createLabelを呼び出して各行を(textX,textY)に表示します。表示後はtextYに行の高さを加算し、ラベル名は「ChatGPT_ResponseLine_0」のように一意の名称とします。最後にChartRedrawを呼び出してチャートを更新し、会話が視覚的に明確かつ判読しやすく表示されるようにします。初期化時にこの関数を呼び出すと、次のような結果が得られます

初期メッセージ付きの更新されたディスプレイ

画像からわかるように、初期メッセージが表示され、会話の描画準備が整っています。次に、送信したプロンプトに対する応答を取得する関数を作成しますが、その前に必要となるヘルパー関数を定義します。

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

応答処理の本格的なロジックを実装する前に、API通信を堅牢におこなうためのJSON文字列エスケープ処理とデバッグ用ユーティリティ関数を作成します。まず、JsonEscape関数を実装します。この関数は文字列入力を受け取り、JSON仕様に準拠するよう特殊文字をエスケープします。具体的には、StringReplaceを使用し、バックスラッシュを「\\」、ダブルクォートを「\"」、改行を「\n」、キャリッジリターンを「\r」、タブを「\t」、フォームフィードを「\f」へと変換します。その後、StringGetCharacterで文字を1文字ずつ取得し、制御文字(ASCII値が32未満または127の場合)であれば、StringFormatを利用して「\uXXXX」形式のUnicodeエスケープシーケンスに置き換えます。この際、StringSubstrで置換前後の文字列を連結し、追加された文字数に応じてインデックスを調整します。最終的に、エスケープ済みの文字列を返します。

次に、デバッグ用としてLogCharArray関数を実装します。この関数はchar配列を16進文字列へ変換するためのものです。まず空の結果文字列を用意し、ArraySizeで配列要素数を取得してループ処理をおこないます。そして、各文字をStringFormatの「%02X」形式で16進数に変換し、結果文字列に追加します。最終的に、整形された16進文字列を返します。これらのユーティリティ関数により、OpenAI APIへのリクエストで送信するプロンプトが正しい形式で整えられ、さらにAPIデータを検証する際のデバッグも容易になります。これらを活用して、応答を取得する基本関数を作成できるようになります。

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

プログラムの中核となる機能を完成させるため、OpenAIのChatGPTと通信をおこなうGetChatGPTResponse関数を実装します。まず、JSON形式に適合させるためにJsonEscapeを呼び出して入力プロンプトをエスケープし、OpenAI_Model、エスケープ済みプロンプトを含むユーザーメッセージ配列、max_tokensを500としたJSON文字列requestDataを構築します。この内容はFileWriteStringを用いてlogFileHandleへログとして記録します。次に、requestDataをUTF-8としてStringToCharArrayでchar配列へ変換し、末尾に付加されるヌル終端を取り除くためにArrayResizeでサイズを調整します。その後、LogCharArray関数を使ってこの配列の16進表記をログへ出力します。

次に、OpenAI_API_Key、コンテンツタイプ、コンテンツの長さを含むHTTPヘッダーを作成してログに記録し、10秒のタイムアウトでWebRequestを使用してOpenAI_EndpointにPOSTリクエストを送信し、応答をresultに保存します。応答コードが200でない場合、resultをCharArrayToStringで文字列へ変換し、PrintとFileWriteStringでエラーをログに記録し、そのエラーメッセージを返します。それ以外の場合は、応答をchar配列へ変換したうえでDeserializeFromArrayを使用してJsonValueオブジェクトとして解析し、解析に失敗した場合はエラーをログに記録して返します。FindChildByKeyを使用して「error」キーが存在するかを確認し、存在する場合はそのエラーメッセージをログに記録して返します。

最後に、jsonObject["choices"][0]["message"]["content"]からToStringを使ってAIの返答を取得し、StringReplaceでエスケープされた改行を置き換え、StringTrimLeftStringTrimRightで前後の空白を取り除きます。内容が空でない場合はそれを返し、空であればエラーをログに記録して返します。この関数により、プログラムはChatGPTへ問い合わせをおこない、その返答を表示用に処理できるようになります。次におこなうべきことは、最初のメッセージを送信することです。そのためには、こちらがおこなう編集を監視し、処理を実行するためのOnChartEventイベントハンドラ関数を実装する必要があります。

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

ここでは、OnChartEvent関数を実装し、チャート上でのユーザー操作を処理することで、プログラムのインタラクティブ性を向上させます。まず、イベントidがCHARTEVENT_OBJECT_CLICKであり、sparamがChatGPT_SubmitButtonであるかを確認し、送信ボタンがクリックされたことを検出します。これが真の場合、ObjectGetStringOBJPROP_TEXTを取得し、テキスト入力フィールド「ChatGPT_InputEdit」からユーザー入力を取得して、prompt変数に保存します。

次に、そのpromptが空でないかをStringLenで確認し、空でなければGetChatGPTResponseを呼び出してAIの応答を取得します。そして、ユーザーのプロンプトとAIの応答をログに記録し、「You: 」と「AI: 」のラベルを付けて改行とともにconversationHistoryに追加します。続いて、ObjectSetStringで入力フィールドを空文字に設定してクリアし、UpdateResponseDisplayを呼び出してチャート上の表示を更新し、FileWriteStringTimeToStringを使用してタイムスタンプ付きでログファイルに記録し、最後にチャートを再描画します。この実装により、プログラムはプロンプトを処理し、AIの応答をインタラクティブに表示できるようになり、ダッシュボードの中核機能が完成します。コンパイルすると、次のような結果が得られます。

最初のプロンプト; HELLO WORLD

画像から分かるように、AIモデルをプログラムへ正常に統合できています。次に、ログファイルを開いて内容を確認してみましょう。ログファイルは共通ファイルフォルダにあります。右クリックして指示に従うことで開くことができます。以下のとおりです。

ログファイル

ファイルを開くと、次のような結果が得られます。

ログファイルの結果

画像から、アクティビティデータを記録できていることが確認できます。これにより、実行した操作、プロンプト、取得した応答を追跡できます。次におこなうべきことは、送信ボタンにホバー効果を追加することです。そのためには、マウス移動座標を追跡する必要があり、チャート上層でのマウスの動きを有効にする必要があります。

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

ChartSetInteger関数を使用して、チャートのマウスイベントを有効にするだけで済みます。分かりやすくするために、その関数を強調してあります。次に、OnChartEvent関数に移動し、マウスが動いた際のロジックを追加する必要があります。

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

OnChartEvent関数では、ユーザーインターフェースのホバー効果を追加することで実装を完了します。else-ifブロック内で、イベントidがCHARTEVENT_MOUSE_MOVEであるかを確認し、マウス移動イベントであることを判定します。そのうえで、lparamをmouseXに、dparamをmouseYにキャストして座標を取得します。次に、マウスが送信ボタン上にあるかどうかを判定するために、mouseXが430から530の間、かつmouseYが20から60の間であるかを評価し、その結果をisOverに保存します。

isOverがtrueで、かつbutton_hoverがfalse(マウスがボタン領域に入った状態)である場合、ObjectSetIntegerを呼び出してChatGPT_SubmitButtonの背景色をbutton_darker_bgへ変更し、ホバー効果を適用します。そして、button_hoverをtrueに設定し、チャートを更新します。逆に、isOverがfalseで、かつbutton_hoverがtrue(マウスがボタン領域から離れた状態)である場合は、ObjectSetIntegerを使用してボタンの背景色をbutton_original_bgへ戻し、button_hoverをfalseに設定したうえで、ChartRedrawによりチャートを更新します。この状態でボタン上でマウスをホバーすると、次のような結果が得られます。

マウスホバーボタンエフェクト

画像から分かるように、作成したボタンにホバー効果を適用できており、インタラクティブなAIダッシュボードの目標を達成しています。残っている作業は、このプログラムのバックテストをおこなうことです。バックテストについては次のセクションで扱います。


ChatGPTプログラムのテスト

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

完全なテストGIF


結論

これまでに作成してきたMQL5のChatGPT統合プログラムは、前回構築したJSON解析フレームワークを活用し、OpenAIのAPIへプロンプトを送信し、インタラクティブなダッシュボードを通して応答を表示できるものとなっています。入力フィールド、送信ボタン、応答表示エリアを備えた使いやすいインターフェースに加えて、API通信とログ記録を組み合わせることで、リアルタイムのAIによるインサイトを得られるようにしています。続くセクションでは、さらに多くの応答を扱えるように表示処理を更新し、スクロール可能にしていきます。どうぞご期待ください。 

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

添付されたファイル |
a._ChatGPT_AI_EA.mq5 (116.72 KB)
Market Sentimentインジケーターの自動化 Market Sentimentインジケーターの自動化
この記事では、市場の状況を強気、弱気、リスクオン、リスクオフ、中立(ニュートラル)に分類するMarket Sentimentカスタムインジケーターを自動化します。エキスパートアドバイザー(EA)は、現在の市場の傾向や方向性の分析プロセスを合理化しながら、一般的なセンチメントに関するリアルタイムの洞察を提供します。
ParafracおよびParafrac V2オシレーターを使用した取引戦略の開発:シングルエントリーパフォーマンスインサイト ParafracおよびParafrac V2オシレーターを使用した取引戦略の開発:シングルエントリーパフォーマンスインサイト
本記事では、ParaFracオシレーターとその後継であるV2モデルを取引ツールとして紹介し、これらを用いて構築した3種類の取引戦略を解説します。各戦略をテストおよび最適化し、それぞれの強みと弱みを明らかにします。比較分析によって両モデルの性能差を明確にしました。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
ボラティリティベースのブレイクアウトシステムの開発 ボラティリティベースのブレイクアウトシステムの開発
ボラティリティベースのブレイクアウトシステムは、市場のレンジを特定したうえで、ATRなどのボラティリティ指標によるフィルタを通過した場合に、価格がそのレンジを上方または下方へブレイクしたタイミングでエントリーする手法です。このアプローチにより、強い方向性を伴う値動きを捉えやすくなります。