
MQL5で取引管理者パネルを作成する(第2回):応答性と迅速なメッセージングの強化
内容
はじめに
メッセージの遅延により、重要な市場シグナルを見逃してしまう状況を想像してみてください。これは、ペースの速い取引環境において、トレーダーが貴重な機会や利益を失う原因となり得ます。この文脈では、熟練した管理者の洞察は市場シグナルと同様に重要です。アルゴリズムシステムは迅速かつ感情的に知的であるものの、システムのパフォーマンスを継続的に監視し、重要な判断を下す熟練トレーダーの鋭い監視に取って代わることはできません。
クリックするメッセージと入力するメッセージの比較
上の画像では、パネルをドラッグしようとすると、チャートが移動し、最小化ボタンが反応しないという問題がある
アルゴリズム取引が金融市場を席巻する中、取引システムのユーザー(トレーダー)と、人間の管理者(取引システムの背後にいる人物)との効率的なコミュニケーションが不可欠になっています。従来、管理パネルのメッセージングインターフェイスでは、クイックメッセージングやパネル自体のクリックとドラッグといったリアルタイムタスクに必要な応答性が限られており、これは迅速な対応が求められる管理者にとって大きな課題となっていました。
管理パネル:以前の管理パネルを対象とした改善
この記事の目的は、MQL5を活用して管理者メッセージングインターフェイスの応答性を向上させ、これらのコミュニケーション障壁を打破することです。加えて、迅速な取引決定と運用効率を支えるためにクイックメッセージングがいかに重要かについても説明します。
本ディスカッションでは、MQL5を利用して取引プラットフォーム内でメッセージ応答性を向上させる方法を探り、主要な実装手順を紹介することで、MQL5プログラミングが持つ可能性をより深く理解できるようにします。共に現代の取引ニーズに応える、より効果的なメッセージングインターフェイスを構築していきます。このディスカッションを通して、次の質問に答えながら議論を進めていきます。
- GUIの応答性とは何か。
- クイックメッセージとは何か。
応答性
MQL5では、GUI(グラフィカルユーザーインターフェイス)の応答性は、ボタンのクリック、スライダーの移動、パネルのサイズ変更などのユーザー操作に対して、インターフェイスがどれだけ迅速かつスムーズに反応するかを指します。応答性の高いGUIは、ユーザーに即時のフィードバックを提供し、インターフェイスが直感的で使いやすいと感じられるようにします。これは、タイムリーなアクションが重要になる可能性がある取引アプリケーションでは特に重要です。
応答性を高めるには、多くの場合、コードを最適化してGUI関連関数の実行時間を短縮し、チャートに描画されるオブジェクトの数を最小限に抑え、可能な場合は非同期処理を利用する必要があります。これにより、メインスレッドがユーザー入力に継続的に応答し、重要な取引アクティビティ中もシームレスなユーザーエクスペリエンスを実現できます。
MQL5 GUIの応答性に関する重要なポイントを以下に詳述します。
- 即時フィードバック:インターフェイスは、ボタンのクリックやテキストの入力といったユーザー操作に即座に応答する必要があります。ユーザー操作とシステム応答の間に顕著な遅延が発生しないようにすることが重要です。
- スムーズなパフォーマンス:複数のGUI要素や複雑なロジックがある場合でも、インターフェイスは遅延やフリーズなしにスムーズに動作する必要があります。これには、CPU負荷を最小限に抑え、ユーザーコマンドを迅速に実行するための効率的なコーディング手法が含まれます。
- 動的な更新:GUIは、インターフェイス全体を再描画することなく、要素を動的に更新できる必要があります。たとえば、新しい価格レベルに達した場合、対応する要素(ラベルや線など)はちらつくことなくスムーズに更新されるべきです。
- スケーラビリティ:インターフェイスは、サイズや解像度の変更に適切に対応する必要があります。たとえば、ユーザーがパネルのサイズを変更した際に、コンテンツが自動的に調整され、使いやすさが維持されることが求められます。
- エラー処理:GUIはエラーを適切に管理し、何か問題が発生した場合にクラッシュしたり応答しなくなったりすることなく、ユーザーに明確で即時のフィードバックを提供する必要があります。
クイックメッセージ
1回のクリックまたは最小限の操作で送信できる、事前に定義されたよく使用されるメッセージを指します。これらのメッセージは通常、頻繁な通信ニーズを満たすように事前に構成されており、ユーザーは手動で入力することなく、標準メッセージをすばやく返信または送信できます。
クイックメッセージの使用例
- 標準的な応答:クイックメッセージは、取引シグナルの確認、アクションの確認、特定のイベントに関するチームへの通知など、標準的な返信やコマンドに使用できます。
- エラー通知:取引中にエラーが発生した場合、「無効な信号」や「エラーが検出されました」などの簡単なメッセージをすぐに送信できます。
- ルーチンコマンド:クイックメッセージには、「すべてのポジションをクローズする」や「EAをアクティブにする」など、取引操作で頻繁に使用されるルーチンコマンドや指示を含めることができます。
例
自動取引システム用のクイックメッセージを備えたMQL5の管理パネルには、以下のような機能が含まれます。
- 監視を開始する:市場状況の監視を開始するための簡単なメッセージ
- 監視を停止する:監視を停止するための簡単なメッセージ
- 無効なシグナル:無効な取引シグナルが検出されたことをユーザーに通知するメッセージ
これらのメッセージはそれぞれパネル上のボタンに結び付けられ、ボタンをクリックすることで、事前定義されたメッセージが即座に送信されます。これにより、時間が節約され、一貫したコミュニケーションが確保されます。
MQL5での実装
MQL5では、特定の事前記述されたテキスト文字列にリンクされたボタンやドロップダウンメニューを作成することで、メッセージングパネルにクイックメッセージを実装できます。ユーザーがこれらのボタンの1つをクリックすると、対応するメッセージがTelegram、電子メール、または他のメッセージングAPIなど、目的の通信チャネルを介して自動的に送信されます。「はじめに」セクションのアニメーション画像では、パネルがチャートの一部を覆ってしまうという欠点が明確に示されています。この欠点は、チャート分析においてより広い視野が必要な場合に煩わしく感じられることがあります。この問題を解決するために、次の2つのセグメントに分割して進めます。
- パネルのコントロールボタンを論理的に配置する。
- 繰り返し機能を活用してクイックメッセージボタンをコーディングする。
読者が第1回を読み終えることで、これまでの進捗と今後の方向性について理解しているものと仮定します。
1. パネルコントロールボタンを論理的に配置する
最小化、最大化、閉じるのボタン
- ボタン宣言
まず、先に進む前にボタンを宣言する方法を示します。
///Global variables
CButton minimizeButton;
CButton maximizeButton;
CButton closeButton;
- 最小化ボタン
最小化ボタンはCButtonクラスを使用して作成し、チャート上の座標(375, -22)に配置、サイズは(30, 22)ピクセルに設定しました。ボタンには、ウィンドウを最小化するための一般的な記号であるアンダースコア「_」が表示されます。adminPanel.Add(minimizeButton)を使用して、このボタンを管理パネルに追加しました。ボタンの目的は、管理パネルを完全に閉じることなく、一時的に非表示にする機能を提供することです。OnMinimizeButtonClick()関数では、管理パネルを非表示にし、最小化、最大化、閉じるボタンのみが表示されるように設定しました。これにより、重要なコントロールにアクセスしつつ、ウィンドウの最小化をシミュレートできます。
// Create the minimize button if (!minimizeButton.Create(chart_id, "MinimizeButton", 0, 375, -22, 405, 0)) { Print("Failed to create minimize button"); return INIT_FAILED; } minimizeButton.Text("_"); adminPanel.Add(minimizeButton); // Function to handle minimize button click void OnMinimizeButtonClick() { minimized = true; // Hide the full admin panel adminPanel.Hide(); minimizeButton.Show(); maximizeButton.Show(); closeButton.Show(); }
- 最大化ボタン
最大化ボタンには同じCButtonクラスを使用し、最小化ボタンの隣の座標(405, -22)に配置しました。ボタンにはウィンドウを最大化または復元するための一般的な記号である[ ]が表示されます。このボタンはadminPanel.Add(maximizeButton)を使用して管理パネルに追加しました。ユーザーは、このボタンを使って最小化された管理パネルをフルサイズに戻すことができます。OnMaximizeButtonClick()関数内では、このボタンがクリックされると管理パネルが元のサイズに復元され、最小化、最大化、閉じるボタンは非表示になります。これにより、最小化されたウィンドウを最大化する動作をシミュレートします。
// Create the maximize button if (!maximizeButton.Create(chart_id, "MaximizeButton", 0, 405, -22, 435, 0)) { Print("Failed to create maximize button"); return INIT_FAILED; } maximizeButton.Text("[ ]"); adminPanel.Add(maximizeButton); // Function to handle maximize button click void OnMaximizeButtonClick() { if (minimized) { minimizeButton.Hide(); maximizeButton.Hide(); closeButton.Hide(); adminPanel.Show(); } }
- 閉じるボタン
閉じるボタンは、他の2つのボタンと同様の方法で作成され、(435, -22)の位置に配置されました。このボタンには、ウィンドウを閉じるための一般的な記号である「X」が表示されます。ボタンはadminPanelAdd(closeButton)を使用して管理パネルに追加されます。ユーザーがこのボタンをクリックすると、OnCloseButtonClick()関数を通じてExpertRemove()が呼び出され、チャートからエキスパートアドバイザー(EA) を完全に削除できます。これにより、ユーザーはパネルの使用を終了する際に、簡単に管理パネルを閉じ、EAを停止することができます。
// Create the close button if (!closeButton.Create(chart_id, "CloseButton", 0, 435, -22, 465, 0)) { Print("Failed to create close button"); return INIT_FAILED; } closeButton.Text("X"); adminPanel.Add(closeButton); // Function to handle close button click void OnCloseButtonClick() { ExpertRemove(); // Completely remove the EA Print("Admin Panel closed."); }2. ループ(繰り返し)機能を使用してクイックメッセージボタンをコーディングする
複数のクイックメッセージボタンインターフェイス
クイックメッセージの入力
- メッセージをカスタマイズできるように、入力変数 (QuickMessage1~QuickMessage8)を使用しました。これにより、ユーザーはEAの設定から直接各クイックメッセージのテキストを変更でき、コアコードを変更する必要がありません。この柔軟性により、さまざまな取引シナリオやユーザーのニーズに合わせてメッセージを簡単に調整できます。さらに、ボタンの配置は動的に設定されており、ループのパラメータやquickMessages配列を変更することで、ボタンの数、サイズ、位置を調整できます。この構造により、管理パネルは多様なニーズに適応可能で、堅牢でユーザーフレンドリーなインターフェイスを提供します。
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input string QuickMessage1 = "Updates"; input string QuickMessage2 = "Close all"; input string QuickMessage3 = "In deep profits"; input string QuickMessage4 = "Hold position"; input string QuickMessage5 = "Swing Entry"; input string QuickMessage6 = "Scalp Entry"; input string QuickMessage7 = "Book profit"; input string QuickMessage8 = "Invalid Signal"; input string InputChatId = "Enter Chat ID from Telegram bot API"; // User's Telegram chat ID input string InputBotToken = "Enter BOT TOKEN from your Telegram bot"; // User's Telegram bot token
ループ実装
- CButtonオブジェクトの配列(quickMessageButtons[8])を作成し、ループで初期化することで、複数のクイックメッセージボタンを生成しました。このループは、定義済みのメッセージを含むquickMessages配列を反復処理し、各反復でボタンを作成し、quickMessagesからラベルを割り当て、インデックスに基づいてボタンを動的に配置します。この繰り返し処理の本質は、各ボタンの作成と設定のプロセスをループ構造で繰り返すことにより、一貫性と効率性が保たれる点です。MQL5では、このアプローチを使用することで、類似した特性を持つ複数のボタンを効率的に作成し、冗長性を最小限に抑え、コードを簡素化してエラーを減らすことができます。
// Array of predefined quick messages string quickMessages[8] = { QuickMessage1, QuickMessage2, QuickMessage3, QuickMessage4, QuickMessage5, QuickMessage6, QuickMessage7, QuickMessage8 }; // Coordinates and dimensions for the buttons int startX = 5, startY = 160, width = 222, height = 65, spacing = 5; // Loop to create and configure quick message buttons for (int i = 0; i < 8; i++) { if (!quickMessageButtons[i].Create(chart_id, "QuickMessageButton" + IntegerToString(i + 1), 0, startX + (i % 2) * (width + spacing), startY + (i / 2) * (height + spacing), startX + (i % 2) * (width + spacing) + width, startY + (i / 2) * (height + spacing) + height)) { Print("Failed to create quick message button ", i + 1); return INIT_FAILED; } quickMessageButtons[i].Text(quickMessages[i]); adminPanel.Add(quickMessageButtons[i]); }
メッセージの長さの管理
- 文字数カウンタの実装では、入力ボックスに入力された文字数を追跡し、ラベルを更新して現在の長さと最大許容メッセージ長を表示する仕組みを採用しています。OnInputChange関数は、入力テキストが変更されるたびにトリガーされ、テキストを取得してStringLenを使用してその長さを計算します。そして、charCounterラベルは「current_length/MAX_MESSAGE_LENGTH」の形式で更新されます。これにより、ユーザーはメッセージ作成中に残りの文字数を確認でき、許容制限を超えないようにすることができます。オプションとして、最大文字数は600に設定されています。
// Maximum number of characters allowed in a message int MAX_MESSAGE_LENGTH = 600; // Function to update the character counter void OnInputChange() { string text = inputBox.Text(); int currentLength = StringLen(text); charCounter.Text(IntegerToString(currentLength) + "/" + IntegerToString(MAX_MESSAGE_LENGTH)); }
完全に統合されたプログラムはこちらです。
//+------------------------------------------------------------------+ //| Admin Panel.mq5 | //| Copyright 2024, Clemence Benjamin | //| https://www.mql5.com/ja/users/billionaire2024/seller | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, Clemence Benjamin" #property link "https://www.mql5.com/ja/users/billionaire2024/seller" #property description "A responsive Admin Panel. Send messages to your telegram clients without leaving MT5" #property version "1.09" #include <Trade\Trade.mqh> #include <Controls\Dialog.mqh> #include <Controls\Button.mqh> #include <Controls\Edit.mqh> #include <Controls\Label.mqh> // Use CLabel for displaying text //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input string QuickMessage1 = "Updates"; input string QuickMessage2 = "Close all"; input string QuickMessage3 = "In deep profits"; input string QuickMessage4 = "Hold position"; input string QuickMessage5 = "Swing Entry"; input string QuickMessage6 = "Scalp Entry"; input string QuickMessage7 = "Book profit"; input string QuickMessage8 = "Invalid Signal"; input string InputChatId = "Enter Chat ID from Telegram bot API"; // User's Telegram chat ID input string InputBotToken = "Enter BOT TOKEN from your Telegram bot"; // User's Telegram bot token //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ CDialog adminPanel; CButton sendButton; CButton clearButton; CButton minimizeButton; CButton maximizeButton; CButton closeButton; CButton quickMessageButtons[8]; CEdit inputBox; CLabel charCounter; // Use CLabel for the character counter bool minimized = false; int MAX_MESSAGE_LENGTH = 600; // Maximum number of characters //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { long chart_id = ChartID(); // Create the dialog if (!adminPanel.Create(chart_id, "Admin Panel", 0, 30, 30, 500, 500)) { Print("Failed to create dialog"); return INIT_FAILED; } // Create the input box if (!inputBox.Create(chart_id, "InputBox", 0, 5, 5, 460, 75)) { Print("Failed to create input box"); return INIT_FAILED; } adminPanel.Add(inputBox); // Create the clear button for the input box if (!clearButton.Create(chart_id, "ClearButton", 0, 180, 75, 270, 105)) { Print("Failed to create clear button"); return INIT_FAILED; } clearButton.Text("Clear"); adminPanel.Add(clearButton); // Create the send button for custom messages if (!sendButton.Create(chart_id, "SendButton", 0, 270, 75, 460, 105)) { Print("Failed to create send button"); return INIT_FAILED; } sendButton.Text("Send Message"); adminPanel.Add(sendButton); // Create the character counter label if (!charCounter.Create(chart_id, "CharCounter", 0, 380, 110, 460, 130)) { Print("Failed to create character counter label"); return INIT_FAILED; } charCounter.Text("0/" + IntegerToString(MAX_MESSAGE_LENGTH)); adminPanel.Add(charCounter); // Create the quick message buttons string quickMessages[8] = { QuickMessage1, QuickMessage2, QuickMessage3, QuickMessage4, QuickMessage5, QuickMessage6, QuickMessage7, QuickMessage8 }; int startX = 5, startY = 160, width = 222, height = 65, spacing = 5; for (int i = 0; i < 8; i++) { if (!quickMessageButtons[i].Create(chart_id, "QuickMessageButton" + IntegerToString(i + 1), 0, startX + (i % 2) * (width + spacing), startY + (i / 2) * (height + spacing), startX + (i % 2) * (width + spacing) + width, startY + (i / 2) * (height + spacing) + height)) { Print("Failed to create quick message button ", i + 1); return INIT_FAILED; } quickMessageButtons[i].Text(quickMessages[i]); adminPanel.Add(quickMessageButtons[i]); } adminPanel.Show(); // Create the minimize button if (!minimizeButton.Create(chart_id, "MinimizeButton", 0, 375, -22, 405, 0)) { Print("Failed to create minimize button"); return INIT_FAILED; } minimizeButton.Text("_"); adminPanel.Add(minimizeButton); // Create the maximize button if (!maximizeButton.Create(chart_id, "MaximizeButton", 0, 405, -22, 435, 0)) { Print("Failed to create maximize button"); return INIT_FAILED; } maximizeButton.Text("[ ]"); adminPanel.Add(maximizeButton); // Create the close button if (!closeButton.Create(chart_id, "CloseButton", 0, 435, -22, 465, 0)) { Print("Failed to create close button"); return INIT_FAILED; } closeButton.Text("X"); adminPanel.Add(closeButton); adminPanel.Show(); // Enable chart events ChartSetInteger(ChartID(), CHART_EVENT_OBJECT_CREATE, true); ChartSetInteger(ChartID(), CHART_EVENT_OBJECT_DELETE, true); ChartSetInteger(ChartID(), CHART_EVENT_MOUSE_WHEEL, true); ChartRedraw(); Print("Initialization complete"); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { adminPanel.Destroy(); Print("Deinitialization complete"); } //+------------------------------------------------------------------+ //| Expert event handling function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { // Handle different types of events switch (id) { case CHARTEVENT_OBJECT_CLICK: if (sparam == "SendButton") { OnSendButtonClick(); } else if (sparam == "ClearButton") { OnClearButtonClick(); } else if (sparam == "MinimizeButton") { OnMinimizeButtonClick(); } else if (sparam == "MaximizeButton") { OnMaximizeButtonClick(); } else if (sparam == "CloseButton") { OnCloseButtonClick(); } else if (StringFind(sparam, "QuickMessageButton") >= 0) { int index = StringToInteger(StringSubstr(sparam, StringLen("QuickMessageButton"))); OnQuickMessageButtonClick(index - 1); } break; case CHARTEVENT_OBJECT_CHANGE: if (sparam == "InputBox") { OnInputChange(); } break; default: break; } } //+------------------------------------------------------------------+ //| Function to handle custom message send button click | //+------------------------------------------------------------------+ void OnSendButtonClick() { string message = inputBox.Text(); if (message != "") { if (SendMessageToTelegram(message)) Print("Custom message sent: ", message); else Print("Failed to send custom message."); } else { Print("No message entered."); } } //+------------------------------------------------------------------+ //| Function to handle clear button click | //+------------------------------------------------------------------+ void OnClearButtonClick() { inputBox.Text(""); // Clear the text in the input box OnInputChange(); // Update the character counter Print("Input box cleared."); } //+------------------------------------------------------------------+ //| Function to handle quick message button click | //+------------------------------------------------------------------+ void OnQuickMessageButtonClick(int index) { string quickMessages[8] = { QuickMessage1, QuickMessage2, QuickMessage3, QuickMessage4, QuickMessage5, QuickMessage6, QuickMessage7, QuickMessage8 }; string message = quickMessages[index]; if (SendMessageToTelegram(message)) Print("Quick Message Button Clicked - Quick message sent: ", message); else Print("Failed to send quick message."); } //+------------------------------------------------------------------+ //| Function to update the character counter | //+------------------------------------------------------------------+ void OnInputChange() { string text = inputBox.Text(); int currentLength = StringLen(text); charCounter.Text(IntegerToString(currentLength) + "/" + IntegerToString(MAX_MESSAGE_LENGTH)); } //+------------------------------------------------------------------+ //| Function to handle minimize button click | //+------------------------------------------------------------------+ void OnMinimizeButtonClick() { minimized = true; // Hide the full admin panel adminPanel.Hide(); minimizeButton.Show(); maximizeButton.Show(); closeButton.Show(); } //+------------------------------------------------------------------+ //| Function to handle maximize button click | //+------------------------------------------------------------------+ void OnMaximizeButtonClick() { if (minimized) { minimizeButton.Hide(); maximizeButton.Hide(); closeButton.Hide(); adminPanel.Show(); } } //+------------------------------------------------------------------+ //| Function to handle close button click | //+------------------------------------------------------------------+ void OnCloseButtonClick() { ExpertRemove(); // Completely remove the EA Print("Admin Panel closed."); } //+------------------------------------------------------------------+ //| Function to send the message to Telegram | //+------------------------------------------------------------------+ bool SendMessageToTelegram(string message) { // Use the input values for bot token and chat ID string botToken = InputBotToken; string chatId = InputChatId; string url = "https://api.telegram.org/bot" + botToken + "/sendMessage"; char post_data[]; // Prepare the message data string jsonMessage = "{\"chat_id\":\"" + chatId + "\", \"text\":\"" + message + "\"}"; // Resize the character array to fit the JSON payload ArrayResize(post_data, StringToCharArray(jsonMessage, post_data)); int timeout = 5000; char result[]; string responseHeaders; // Make the WebRequest int res = WebRequest("POST", url, "Content-Type: application/json\r\n", timeout, post_data, result, responseHeaders); if (res == 200) // HTTP 200 OK { Print("Message sent successfully: ", message); return true; } else { Print("Failed to send message. HTTP code: ", res, " Error code: ", GetLastError()); Print("Response: ", CharArrayToString(result)); return false; } }
高度で応答性の高い管理パネルをテストします。
ここで、管理パネルを起動しましたが、欠点はほとんどなく、正常に動作していました。下の画像をご覧ください。
ボラティリティ150指数:管理パネルのテスト
Telegramの統合は正常に機能し、メッセージはクリックするだけで届きます。
Telegramクイックメッセージ受信
結論
結論として、管理者パネルのEAに応答性とクイックメッセージ機能を統合したことで、実用性とユーザーエクスペリエンスが大幅に向上しました。新たに追加された最小化、最大化、閉じるボタンにより、シームレスで直感的なインターフェイスが実現し、ユーザーはパネルの表示や操作を簡単に管理できるようになりました。この機能により、パネルは必要に応じてフル表示やコンパクト表示に柔軟に対応し、ユーザーのニーズに応えることができます。また、クイックメッセージ機能の実装により、ユーザーはMetaTrader 5環境を離れることなく、定義済みのメッセージを即座にTelegramクライアントに送信できるようになり、コミュニケーションの効率がさらに向上しました。この機能は、特にタイミングが重要な取引シナリオにおいて非常に役立ちます。カスタムメッセージの送信機能とクイックメッセージボタンの利便性が組み合わさることで、取引活動の中断を最小限に抑えながら、効果的なコミュニケーションを維持できます。
全体として、これらの機能強化により、管理パネルはトレーダーや管理者にとってより強力なツールとなり、効率性と柔軟性が向上しました。この進化は、アルゴリズム取引における現実的な課題に対応するソリューションを提供するという当社の取り組みを反映しており、ユーザーが取引業務をよりコントロールしやすく、便利に管理できるようにしています。
まだ改善の余地はありますが、本日はここまでとします。ソースファイルを以下に添付したので、開発と取引を引き続きお楽しみください。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/15418





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索