English Русский Deutsch
preview
MQL5からDiscordへのメッセージの送信、Discord-MetaTrader 5ボットの作成

MQL5からDiscordへのメッセージの送信、Discord-MetaTrader 5ボットの作成

MetaTrader 5統合 |
91 0
Omega J Msigwa
Omega J Msigwa

内容


はじめに

私たちはもはやインターネットの黎明期や原始的なデジタル時代にいるわけではありません。現在ではほとんどあらゆるものがインターネットを介して相互接続可能であり、実際に接続するかどうかはそれをおこなう意志と労力次第です。

API (Application Programming Interface)は、異なるソフトウェアアプリケーション同士が通信するための規則やプロトコルの集合体であり、複数のソフトウェアやアプリケーションが容易に接続できるようになりました。その結果として、今日私たちが目にしている高度に接続されたインターネットが実現しています。

提供されるAPIを利用するためには、その規則やプロトコルを遵守する必要があり、さらに(存在する場合には)API提供者が定めるセキュアなアクセス手段を取得しなければなりません。

MetaTrader5と外部アプリケーション間で通信をおこなうことは新しい取り組みではありません。既に、MQL5開発者がWebRequestを利用して情報を送受信するための信頼性あるAPIを提供しているアプリケーションはいくつも存在します。MQL5トレーダーがこうした通信に最もよく活用しているのはTelegramです。

本記事では、MQL5プログラミング言語を用いてMetaTrader5からDiscordへメッセージや取引情報(シグナル)を送信する方法について掘り下げていきます。


Discord Webhookを作成する

Discordにまだ馴染みのない方のために説明します。

Discordは無料のコミュニケーションプラットフォームで、ユーザーはテキスト、ボイス、ビデオでチャットをしたり、画面を共有したりできます。Discordは友人やコミュニティとの交流に広く利用されており、特にオンラインゲームの分野で人気ですが、読書会やハンドクラフトのサークルなどさまざまなグループでも利用されています。

Telegramと同様に、Discordもユーザー同士がコミュニケーションを取るためのプラットフォームです。両者ともにプラットフォーム外との通信を可能にするAPIを提供していますが、統合性や自動化の観点ではDiscordはTelegramよりはるかに先を進んでいます

このプラットフォームでは、柔軟なコミュニティを構築したり、さまざまな種類のアプリ(「ボット」)を管理することが可能です。

Discordにボットを作成したり、情報共有やコミュニケーションを自動化する方法は複数ありますが、最も簡単なのはコミュニティ内のチャンネルにWebhookを作成する方法です。以下に作成手順を示します。

 

図01

コミュニティから[Server Settings]に移動します。

 

図02

Server Settingsから、[Integrations]>[Webhooks]に移動します。

 

図03

[New Webhook]ボタンをクリックしてWebhookを作成します。デフォルト名「Captain Hook」のWebhookが作成されますが、名前は自由に変更できます。

 

図04

名前を変更したら、このWebhookを適用したいチャンネルを選択できます。このサーバーでは「trading signals」というチャンネルを用意しており、ここにWebhookを適用します(図01参照)

最後に、[Copy Webhook URL]を選択して、Webhook URLをコピーします。これはMQL5でWebRequestを使う際に利用できるAPIゲートウェイとなります。

 

図05



MetaTrader 5からDiscordに最初のメッセージを送信する

最初におこなわなければならないのは、MetaTrader5で許可されたURLリストにhttps://discord.com を追加することです。これを設定しなければ、この後に説明する内容は動作しません。

MetaTrader 5で、[ツール] > [オプション] > [エキスパートアドバイザ]を開き、[WebRequestを許可するURLリスト:]にチェックが入っていることを確認してください。その後、下のフォームにURLを追加します。

Webhook URLを使えば、JSON形式のデータを指定してこのAPIエンドポイントにPOSTリクエストを送信することができます。

ファイル名: Discord EA.mq5

#include <jason.mqh> //https://www.mql5.com/ja/code/13663

string discord_webhook_url = "https://discord.com/api/webhooks/1384809399767269527/105Kp27yKnQDpKD01VdEb01GS5P-KH5o5rYKuJb_xD_D8O23GPkGLXGn9pHBB1aOt4wR";
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
     
   CJAVal js(NULL, jtUNDEF); //Creating a json object
   
   string raw_json = "{\"content\": \"Hello world from MetaTrader5!\"}";
   
   bool b = js.Deserialize(raw_json); //Parse JSON string into a structured object
   
   string json;
   js.Serialize(json); //Convert the object to a valid JSON string
   
//--- Sending a Post webrequest

   char data[]; 
   ArrayResize(data, StringToCharArray(json, data, 0, StringLen(json))); //--- serialize to string
   
//--- send data

   char res_data[]; //For storing the body of the response
   string res_headers=NULL; //For storing response headers (if needed)
   
   string headers="Content-Type: application/json; charset=utf-8";
   uint timeout=10000; //10 seconds timeout for the HTTP request
   
   int ret = WebRequest("POST", 
                         discord_webhook_url, 
                         headers, 
                         timeout, 
                         data, 
                         res_data, 
                         res_headers); //Send a post request
   
   if (ret==-1)
     {
       printf("func=%s line=%d, Failed to send a webrequest. Error = %s",__FUNCTION__,__LINE__,ErrorDescription(GetLastError()));
       return false;
     }
   
//--- Check if the post request was successful or not

   if (ret==204)
     {
       if (MQLInfoInteger(MQL_DEBUG))
         Print("Message sent to discord successfully");
     }
   else
     {
       printf("Failed to send message to discord. Json response Error = %s",CharArrayToString(res_data));
     }
  
//---
   return(INIT_SUCCEEDED);
  }

Discordに送信したいすべての情報やメッセージは、常にJSON形式である必要があります。

JSONオブジェクト内のcontentキーの下にある内容は、Discordに送信されるメッセージの本文を表します。

上記のコードを実行すると、Discordに簡単なメッセージが送信されます。

うまくいきました。しかし、単純なメッセージを送信するだけのためにJSONデータやその他の処理を毎回手作業で扱うのは面倒です。そこで、すべてをクラス化して、毎回技術的な詳細を気にせずに簡単にメッセージを送信できるようにしましょう。


MQL5でDiscordクラスを作成する

標準的な機能を持つクラスにするために、APIエンドポイントや送信リクエスト全体の処理で発生したエラーを追跡できるよう、ログ機能を追加しましょう。

ファイル名:Discord.mqh

#include <errordescription.mqh>
#include <logging.mqh>
#include <jason.mqh>

class CDiscord
  {
protected:
   string m_webhook_url;
   string m_headers;
   uint m_timeout;
   CLogging logging;
   
   bool PostRequest(const string json);
   string GetFormattedJson(const string raw_json);
   
public:
                     CDiscord(const string webhook_url, const string headers="Content-Type: application/json; charset=utf-8", const uint timeout=10000);
                    ~CDiscord(void);
                     
                     bool SendMessage(const string message);
                     bool SendEmbeds(const string title, const string description_, color clr, const string footer_text, const datetime time);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CDiscord::CDiscord(const string webhook_url, string headers="Content-Type: application/json; charset=utf-8", uint timeout=10000):
 m_webhook_url(webhook_url),
 m_headers(headers),
 m_timeout(timeout)
 {
//--- Initialize the logger

   logging.Config("Discord server");
   if (!logging.init())
      return;
      
   logging.info("Initialized",MQLInfoString(MQL_PROGRAM_NAME),__LINE__);
 }

POSTリクエストの送信やテキストをJSON形式に変換する処理など、繰り返し発生する処理をクラス内にまとめます。

string CDiscord::GetFormattedJson(const string raw_json)
 {
   CJAVal js(NULL, jtUNDEF);
   bool b = js.Deserialize(raw_json);
   
   string json;
   js.Serialize(json); 
   
   return json;
 }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CDiscord::PostRequest(const string json)
 {
   char res[];
    
//--- serialize to string

   char data[]; 
   ArrayResize(data, StringToCharArray(json, data, 0, StringLen(json)));
   
//--- send data

   char res_data[];
   string res_headers=NULL;

   int ret = WebRequest("POST", m_webhook_url, m_headers, m_timeout, data, res_data, res_headers); //Send a post request
   
   if (ret==-1)
     {
       printf("func=%s line=%d, Failed to send a webrequest. Error = %s",__FUNCTION__,__LINE__,ErrorDescription(GetLastError()));
       return false;
     }
   
//--- Check if the post request was successful or not

   if (ret==204)
     {
       if (MQLInfoInteger(MQL_DEBUG))
         Print("Message sent to discord successfully");
         logging.info("Message sent to discord successfully",MQLInfoString(MQL_PROGRAM_NAME), __LINE__);
     }
   else
     {
       printf("Failed to send message to discord. Json response Error = %s",CharArrayToString(res_data));
       logging.error(CharArrayToString(res_data), MQLInfoString(MQL_PROGRAM_NAME), __LINE__);
     }

  return true;
 }

以下はDiscordにメッセージを送信するための簡単な関数です。

bool CDiscord::SendMessage(const string message)
 {
   string raw_json = StringFormat("{\"content\": \"%s\"}",message);   
   string json = GetFormattedJson(raw_json); //Deserialize & Serialize the message in JSON format
   
   return PostRequest(json);
 }

使い方は次の通りです。

ファイル名: Discord EA.mq5

#include <Discord.mqh>
CDiscord *discord;

string discord_webhook_url = "https://discord.com/api/webhooks/1384809399767269527/105Kp27yKnQDpKD01VdEb01GS5P-KH5o5rYKuJb_xD_D8O23GPkGLXGn9pHBB1aOt4wR";
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
     
    discord = new CDiscord(discord_webhook_url);  
    discord.SendMessage("Hello from MetaTrader5!");
  }

このシンプルで強力な関数は、さまざまな種類のメッセージを送信するために利用できます。titlehttps://support.discord.com/hc/en-us/articles/210298617-Markdown-Text-101-Chat-Formatting-Bold-Italic-UnderlinetitleDiscordはMarkdown形式のテキストをサポートしている ためです。

Markdown形式メッセージの例

    discord.SendMessage(
                         "# h1 header\n"
                         "## h2 header\n"
                         "### h3 header\n\n"
                         "**bold text** normal text\n"
                         "[MQL5](https://www.mql5.com)\n"
                         "![image](https://i.imgur.com/4M34hi2.png)"
                       );

以下が出力です。

Discordが提供するMarkdownテキストエディタでできることは、これだけではありません。ここでは、Discordでコードを共有する方法を見てみましょう。

    discord.SendMessage("```MQL5\n" //we put the type of language for markdown highlighters after three consecutive backticks
                        "if (CheckPointer(discord)!=POINTER_INVALID)\n"
                        "  delete discord;\n"
                        "```");
                        
    discord.SendMessage("```Python\n"
                        "while(True):\n"
                        "  break\n"
                        "```");

以下が出力です。

これは、Markdownテキストエディタの強力さを示す例でした。

メッセージに絵文字を追加する

絵文字はテキストメッセージにおいて非常に便利です。メッセージに華やかさを加え、可読性を向上させるだけでなく、ちょっとしたユーモアも演出できます。

他のコミュニケーションプラットフォームでは、絵文字は特定の数値コードから生成されますが、Discordでは独自の構文を用いて、テキストメッセージ内で直接絵文字を識別、表示できます。

絵文字の名前を2つのコロン(:)で囲む方法は簡単です。絵文字名の前後にコロンを1つずつ付けるだけです。

このようにコードをテキストメッセージに挿入すると、Discord上では絵文字として表示されます。

discord.SendMessage(":rotating_light: Trade Alert!");

以下がメッセージです。

Discordには何千もの絵文字があり、すべてを把握するのは困難です。こちらのチートシートを活用すると便利です。 私はいくつかの絵文字コードと構文をまとめた簡単なライブラリファイルを作成しました。必要に応じて、自由に絵文字コードを追加してください

ファイル名:discord emojis.mqh

#define DISCORD_EMOJI_ROCKET                   ":rocket:"               
#define DISCORD_EMOJI_CHART_UP                 ":chart_with_upwards_trend:"   
#define DISCORD_EMOJI_CHART_DOWN               ":chart_with_downwards_trend:" 
#define DISCORD_EMOJI_BAR_CHART                ":bar_chart:"            
#define DISCORD_EMOJI_BOMB                     ":bomb:"                 
#define DISCORD_EMOJI_THUMBS_UP                ":thumbsup:"             
#define DISCORD_EMOJI_THUMBS_DOWN              ":thumbsdown:"           
#define DISCORD_EMOJI_WARNING                  ":warning:"              
#define DISCORD_EMOJI_JOY                      ":joy:"                  
#define DISCORD_EMOJI_SOB                      ":sob:"                  
#define DISCORD_EMOJI_SMILE                    ":smile:"                
#define DISCORD_EMOJI_FIRE                     ":fire:"                 
#define DISCORD_EMOJI_STAR                     ":star:"                 
#define DISCORD_EMOJI_BLUSH                    ":blush:"                
#define DISCORD_EMOJI_THINKING                 ":thinking:"             
#define DISCORD_EMOJI_ROTATING_LIGHT           ":rotating_light:"
#define DISCORD_EMOJI_X                        ":x:"
#define DISCORD_EMOJI_WHITE_CHECK_MARK         ":white_check_mark:"
#define DISCORD_EMOJI_BALLOT_BOX_WITH_CHECK    ":ballot_box_with_check:"

#define DISCORD_EMOJI_HASH                     ":hash:"               
#define DISCORD_EMOJI_ASTERISK                 ":asterisk:"           

#define DISCORD_EMOJI_ZERO                     ":zero:"               
#define DISCORD_EMOJI_ONE                      ":one:"                
#define DISCORD_EMOJI_TWO                      ":two:"                
#define DISCORD_EMOJI_THREE                    ":three:"              
#define DISCORD_EMOJI_FOUR                     ":four:"               
#define DISCORD_EMOJI_FIVE                     ":five:"               
#define DISCORD_EMOJI_SIX                      ":six:"                
#define DISCORD_EMOJI_SEVEN                    ":seven:"              
#define DISCORD_EMOJI_EIGHT                    ":eight:"              
#define DISCORD_EMOJI_NINE                     ":nine:"               
#define DISCORD_EMOJI_TEN                      ":keycap_ten:"       
  
#define DISCORD_EMOJI_RED_CIRCLE               ":red_circle:"               
#define DISCORD_EMOJI_GREEN_CIRCLE             ":green_circle:"       

メッセージを送信します。

discord.SendMessage(DISCORD_EMOJI_ROTATING_LIGHT " Trade Alert! " DISCORD_EMOJI_JOY);

以下がメッセージです。

ユーザーやロールへのメンション

このシンプルなメッセージ機能では、メンションやロールを扱うことができ、効果的なメッセージ配信には欠かせません。

  • @everyone:チャンネル内のすべてのユーザーに通知します。オフラインのユーザーも含まれます。
  • @here:チャット内のすべての非アイドル状態のユーザーに通知します。オンラインユーザー全員です。
  • <@user_id>:特定のユーザーに通知します。
  • <@&role_id>:特定のロールに割り当てられたすべてのユーザーに通知します。たとえば、裁量トレーダーのロールを持つすべてのユーザーに取引シグナルを送信する場合などです。

以下が使用例です。

discord.SendMessage("@everyone This is an information about a trading account");
discord.SendMessage("@here This is a quick trading signals for all of you that are active");

以下がメッセージです。


Discordボットのアイデンティティ

Webhookの利点の一つは、デフォルトの表示名やアバターに縛られず、POSTリクエストを送信するたびにこれらの値を変更できる点です。

この機能は非常に便利です。複数のトレーディングロボットが同じWebhookを使用してコミュニティに情報を送信する場合でも、異なるアイデンティティを使うことで送信者や受信メッセージを区別することができます。

さらに、Discordクラスが呼び出されるたび(初期化されるたび)にこのアイデンティティを必須とするように設定すれば、アイデンティティをグローバルに管理することも可能です。

class CDiscord
  {
protected:

//...
//...
   
public:
                     string m_name;
                     string m_avatar_url;
                     
                     CDiscord(const string webhook_url, 
                              const string name, 
                              const string avatar_url, 
                              const string headers="Content-Type: application/json; charset=utf-8", 
                              const uint timeout=10000);
 }

これらの変数はpublicとして宣言します。ユーザーがクラスの初期化後にこれらを変更したりアクセスしたりする可能性があるためです。

こうすることで、このDiscordメッセンジャーに割り当てられたユーザー名ごとのログも簡単に追跡できます。

CDiscord::CDiscord(const string webhook_url, 
                   const string name, 
                   const string avatar_url, 
                   const string headers="Content-Type: application/json; charset=utf-8", 
                   const uint timeout=10000):
                   
 m_webhook_url(webhook_url),
 m_headers(headers),
 m_timeout(timeout),
 m_name(name),
 m_avatar_url(avatar_url)
 {
//--- Initialize the logger

   logging.Config(m_name);
   if (!logging.init())
      return;
      
   logging.info("Initialized",MQLInfoString(MQL_PROGRAM_NAME),__LINE__);
 }

次に、このアイデンティティ(名前とアバター)の情報を、JSON文字列が使用されているすべての箇所に追加する必要があります。

bool CDiscord::SendMessage(const string message)
 {
   string raw_json = StringFormat("{"
                                    "\"username\": \"%s\","
                                    "\"avatar_url\": \"%s\","
                                    "\"content\": \"%s\""
                                  "}",
                                  m_name, m_avatar_url, message);   
                                  
   string json = GetFormattedJson(raw_json); //Deserialize & Serialize the message in JSON format
   
   return PostRequest(json);
 }

SendJSON()関数内では、このアイデンティティをオプション扱いにする必要があります。ユーザーは最初から別の方法を考えている場合もあり、送信したい情報を自由に指定できる生のJSON文字列を使う関数を選択することもあるためです。

bool CDiscord::SendJSON(const string raw_json, bool use_id=true)
 {
   CJAVal js;                                 
   
   js.Deserialize(raw_json);
   
   if (use_id) //if a decides to use the ids assigned to the class constructor, we append that information to the json object
     {
       js["username"] = m_name;
       js["avatar_url"] = m_avatar_url;
     }
     
   string json;
   js.Serialize(json);
   
   return PostRequest(json);
 }

ボットに別のIDを設定しましょう。

#include <Discord.mqh>
CDiscord *discord;

input string discord_webhook_url = "https://discord.com/api/webhooks/1384809399767269527/105Kp27yKnQDpKD01VdEb01GS5P-KH5o5rYKuJb_xD_D8O23GPkGLXGn9pHBB1aOt4wR";
input string avatar_url_ = "https://imgur.com/m7sVf51.jpeg";
input string bots_name = "Signals Bot";
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
     
    discord = new CDiscord(discord_webhook_url,
                           bots_name,
                           avatar_url_);  
    
    discord.SendMessage("Same webhook but, different Identity :smile: cheers!");
            
//---
   return(INIT_SUCCEEDED);
  }

以下が結果です。


埋め込みファイルと画像ファイルの操作

前のセクションでDiscordクラスを作成する際にメッセージを添付する方法を説明しましたが、アイテムを埋め込む適切な方法があります。

これは独立したJSONリクエストで実現できます。

discord.SendJSON(
    "{"
        "\"content\": \"Here's the latest chart update:\","
        "\"embeds\": ["
            "{"
                "\"title\": \"Chart Update\","
                "\"image\": {"
                    "\"url\": \"https://imgur.com/SBsomI7.png\""
                "}"
            "}"
        "]"
    "}"
);

以下は結果です。

残念ながら、Discordはファイルの受信に対応していますが、MQL5からWebRequestを使って直接画像を送信することはできません。  現状、最も現実的な方法は、オンライン上にホストされているファイルのリンクを共有することです。

前の例ではimgur.comを使用しました。


MetaTrader 5からDiscordに取引通知を送信する

トレーダーは、MetaTrader5から外部プラットフォームへ情報を送信する機能を利用して、自分の取引活動に関するアップデートを送信することがよくあります。そのようなタスクのためにこのボット(エキスパートアドバイザー)を活用します。

取引開始の通知から始めます。

01:取引開始通知の送信

このタスクにはOnTradeTransaction関数が便利です。

以下は、最近のポジションと取引の状態を確認するための条件チェッカーです。

#define IS_TRANSACTION_POSITION_OPENED         (trans.type == TRADE_TRANSACTION_DEAL_ADD && HistoryDealSelect(trans.deal) && (ENUM_DEAL_ENTRY)HistoryDealGetInteger(trans.deal, DEAL_ENTRY) == DEAL_ENTRY_IN)
#define IS_TRANSACTION_POSITION_CLOSED         (trans.type == TRADE_TRANSACTION_DEAL_ADD && HistoryDealSelect(trans.deal) && (ENUM_DEAL_ENTRY)HistoryDealGetInteger(trans.deal, DEAL_ENTRY) == DEAL_ENTRY_OUT && ((ENUM_DEAL_REASON)HistoryDealGetInteger(trans.deal, DEAL_REASON) != DEAL_REASON_SL && (ENUM_DEAL_REASON)HistoryDealGetInteger(trans.deal, DEAL_REASON) != DEAL_REASON_TP))
#define IS_TRANSACTION_POSITION_MODIFIED       (trans.type == TRADE_TRANSACTION_POSITION)
void OnTradeTransaction(const MqlTradeTransaction& trans,
                        const MqlTradeRequest& request,
                        const MqlTradeResult& result)
  {
     ulong deal_ticket = trans.deal;
     ulong position_ticket = trans.position;

//---
     
     m_deal.Ticket(deal_ticket); //select a deal by it's ticket
     
     if (IS_TRANSACTION_POSITION_OPENED)
       {
         if (deal_ticket==0)
           return;
          
         if (!m_position.SelectByTicket(position_ticket)) //select a position by ticket
            return;
              
         if (!m_symbol.Name(m_deal.Symbol())) //select a symbol from a position
           {
             printf("line=%d Failed to select symbol %s. Error = %s",__LINE__,m_deal.Symbol(),ErrorDescription(GetLastError()));
             return;
           }
         
         string message = 
            DISCORD_EMOJI_ROTATING_LIGHT " **TRADE OPENED ALERT** \n\n"
            "- **Symbol:**"+m_position.Symbol()+"\n"
            "- **Trade Type:** "+ (m_position.PositionType()==POSITION_TYPE_BUY ? DISCORD_EMOJI_GREEN_CIRCLE: DISCORD_EMOJI_RED_CIRCLE) +" "+m_position.TypeDescription()+"\n"
            "- **Entry Price:** `"+DoubleToString(m_deal.Price(), m_symbol.Digits())+"`\n" 
            "- **Stop Loss:** `"+DoubleToString(m_position.StopLoss(), m_symbol.Digits())+"`\n"
            "- **Take Profit:** `"+DoubleToString(m_position.TakeProfit(), m_symbol.Digits())+"`\n"
            "- **Time UTC:** `"+TimeToString(TimeGMT())+"`\n"
            "- **Position Ticket:** `"+(string)position_ticket+"`\n"
            "> "DISCORD_EMOJI_WARNING" *Risk what you can afford to lose.*";
               
         discord.SendMessage(message);
       }

  //...
  //... Other lines of code
  //...     
 }

注意:

エントリープライス、ストップロス、テイクプロフィットなどの市場価格をフォーマットする際には、それぞれの数値を単一のバッククォート(`)で囲んでいます。これにより、Discord上でインラインコードブロックとして表示され、数値が視覚的に区切られ、ユーザーがコピーしやすくなります。

Discordでは以下の形式を使用します。

  • 単一のバッククォート(`)はインラインコードのフォーマットに使用します。短い数値や価格に適しています。
  • 三重バッククォート(```)は複数行のコードブロックに使用します。大きなコードや長いメッセージに適しています。

MetaTrader 5のワンクリック取引機能を使用して、手動で2つの逆方向の取引を開きました。以下がその結果です。

02:取引変更通知の送信

if (IS_TRANSACTION_POSITION_MODIFIED)
  {
    if (!m_position.SelectByTicket(position_ticket))
      {
        printf("Failed to modify a position. Erorr = %s",ErrorDescription(GetLastError()));
        return;
      }
              
    if (!m_symbol.Name(m_deal.Symbol()))
      {
        printf("line=%d Failed to select symbol %s. Error = %s",__LINE__,m_deal.Symbol(),ErrorDescription(GetLastError()));
        return;
      }
         
    string message = 
       DISCORD_EMOJI_BAR_CHART " **TRADE MODIFIED ALERT** \n\n"
       "- **Symbol:** `"+m_position.Symbol()+"`\n"
       "- **Trade Type:** "+ (m_position.PositionType()==POSITION_TYPE_BUY ? DISCORD_EMOJI_GREEN_CIRCLE: DISCORD_EMOJI_RED_CIRCLE) +" "+m_position.TypeDescription()+"\n"
       "- **Entry Price:** `"+DoubleToString(m_position.PriceOpen(), m_symbol.Digits())+"`\n" 
       "- **New Stop Loss:** `"+DoubleToString(m_position.StopLoss(), m_symbol.Digits())+"`\n"
       "- **New Take Profit:** `"+DoubleToString(m_position.TakeProfit(), m_symbol.Digits())+"`\n"
       "- **Time UTC:** `"+TimeToString(TimeGMT())+"`\n"
       "- **Position Ticket:** `"+(string)position_ticket+"`\n"
       "> "DISCORD_EMOJI_WARNING" *Risk what you can afford to lose.*";
            
    discord.SendMessage(message);      
  }

このメッセージの内容は、取引通知を送信する際のものとすべて同じです。唯一の違いは、ストップロスとテイクプロフィットの項目名が変更されている点です。

03:取引終了通知の送信

if (IS_TRANSACTION_POSITION_CLOSED)
   {         
    if (!m_symbol.Name(m_deal.Symbol()))
      {
        printf("line=%d Failed to select symbol %s. Error = %s",__LINE__,m_deal.Symbol(),ErrorDescription(GetLastError()));
        return;
      }
         
    m_symbol.RefreshRates(); //Get recent ask and bid prices
         
    long reason_integer;
    m_deal.InfoInteger(DEAL_REASON, reason_integer);
         
    string reason_text = GetDealReasonText(reason_integer);
            
    string message = 
          DISCORD_EMOJI_X " **TRADE CLOSED ALERT**\n\n"
          "- **Symbol:** `" + m_deal.Symbol() + "`\n"
          "- **Trade Type:** " + (m_position.PositionType() == POSITION_TYPE_BUY ? DISCORD_EMOJI_GREEN_CIRCLE : DISCORD_EMOJI_RED_CIRCLE) + " " + m_position.TypeDescription() + "\n"
          "- **Entry Price:** `" + DoubleToString(m_deal.Price(), m_symbol.Digits()) + "`\n"
          "- **Exit Price:** `" + (m_position.PositionType() == POSITION_TYPE_BUY ? (DoubleToString(m_symbol.Bid(), m_symbol.Digits())) : (DoubleToString(m_symbol.Ask(), m_symbol.Digits()))) + "`\n"
          "- **Profit:** " + (m_deal.Profit() >= 0 ? DISCORD_EMOJI_THUMBS_UP : DISCORD_EMOJI_THUMBS_DOWN) + " `" + DoubleToString(m_deal.Profit(), 2) + "`\n"
          "- **Close Reason:** `" + reason_text+ "`\n"
          "- **Commission:** `" + DoubleToString(m_position.Commission(), 2) + "`\n"
          "- **Swap:** `" + DoubleToString(m_position.Swap(), 2)+ "`\n"
          "- **Time (UTC):** `" + TimeToString(TimeGMT()) + "`\n"
          "- **Deal Ticket:** `" + string(deal_ticket) + "`\n"
          "> "DISCORD_EMOJI_WARNING" *Risk what you can afford to lose.*";
               
    discord.SendMessage(message);
  }

決済取引のメッセージでは、利益で決済された場合はサムズアップの絵文字を、損失で決済された場合はサムズダウンの絵文字を付けます。これは、ポジションの新規建てや変更時にポジションチケット番号を使っていたのとは異なります。決済済みのポジションはもはやポジションではなく「取引」となるため、決済では取引チケット([Deal Ticket])番号を使用します。

今回の取引は手動で決済されたため、その理由はClient (Desktop Terminal)となります。これはENUM_DEAL_REASONに従ったものです。

string GetDealReasonText(long reason)
{
   switch((int)reason)
   {
      case DEAL_REASON_CLIENT:            return "Client (Desktop Terminal)";
      case DEAL_REASON_MOBILE:            return "Mobile App";
      case DEAL_REASON_WEB:               return "Web Platform";
      case DEAL_REASON_EXPERT:            return "Expert Advisor";
      case DEAL_REASON_SL:                return "Stop Loss Hit";
      case DEAL_REASON_TP:                return "Take Profit Hit";
      case DEAL_REASON_SO:                return "Stop Out (Margin)";
      case DEAL_REASON_ROLLOVER:          return "Rollover Execution";
      case DEAL_REASON_VMARGIN:           return "Variation Margin Charged";
      case DEAL_REASON_SPLIT:             return "Stock Split Adjustment";
      case DEAL_REASON_CORPORATE_ACTION:  return "Corporate Action";
      default:                            return "Unknown Reason";
   }
}


結論

Discord Webhookは、MetaTrader 5とDiscordを統合するための良い出発点であり、これら2つの強力なプラットフォーム間でのコミュニケーションを可能にします。しかし、前述した通り、すべてのAPIにはルールと制限があります。Discord Webhook APIも例外ではなく、Webhookリクエストにはレート制限があります。

  • 1つのWebhookでは、2秒ごとに最大5件のメッセージを送信可能
  • 1つのWebhookでは、チャンネルごとに最大30件のメッセージ/分まで送信可能

この制限を超えると、Discordは「HTTP 429 (Too Many Requests)」を返します。この問題に対処するためには、MQL5コード内で送信に遅延やキューを追加する方法があります。

また、この記事全体で「Discordボット」についても触れていますが、WebhookはDiscordボットとは大きく異なります。私がMQL5とDiscord間のワークフロー全体を指して「ボット」と呼んでいるのはこのためです。

ボットとは、Discord内で動作するプログラムで、基本的なコマンドから複雑な自動化まで幅広い機能を提供します。一方、Webhookは単純なURLで、外部アプリケーションから特定のDiscordチャンネルに自動でメッセージを送信するために使われます。

以下に、Discord WebhookとDiscordボットの違いを表形式でまとめました。


Webhook ボット
機能
  • 設定されたチャンネルにのみメッセージを送信可能
  • 送信のみ可能で閲覧は不可
  • 1メッセージあたり最大10個のEmbedを送信可能

  • より柔軟で、通常ユーザーと同様の複雑なアクションが可能
  • メッセージの閲覧と送信が可能
  • 1メッセージあたりEmbedは1つのみ
カスタマイズ
  • サーバーごとに最大10個のWebhookを作成可能。各Webhookの名前やアバターをカスタマイズ可能。
  • Embed外のテキストにもハイパーリンクが可能

  • 公開ボットは事前設定された名前とアバターを持ち、ユーザーは変更不可
  • 通常メッセージ内でのハイパーリンクは不可、Embedを使用する必要あり。
負荷とセキュリティ
  • データ送信用のエンドポイントのみで、ホスティングは不要
  • Webhookに送信されたデータが信頼できるかの認証はなし
  • URLが漏洩しても問題は一時的(例:スパム)で済む
  • 必要に応じてWebhook URLを簡単に変更可能

  • 常にオンラインにする必要がある安全な環境でホストする必要があり、リソースコストが高い。
  • トークンで認証されるため、トークンが漏れると権限に応じて深刻な被害が発生する可能性あり。
  • 必要に応じてボットトークンをリセット可能

Discordボットは複雑で、MQL5プログラマーにとっては実用的ではありません。通常は、取引の進捗状況を他のトレーダーに通知する機能があれば十分だからです。ボットを作成するにはPythonのモジュールもいくつか必要になります。

とはいえ、MetaTrader 5と連携可能な本格的なDiscordボットを開発することも可能です。

MetaTrader 5用Pythonパッケージを使用すれば、Python環境から両プラットフォーム間の完全統合を実現できます。

ご一読、誠にありがとうございました。


添付ファイルの表

ファイル名 説明と使用法
Include\discord emojis.mqh Discordと互換性のある絵文字コードと構文
Include\discord.mqh DiscordプラットフォームにJSON形式でメッセージを送信するためのCDiscordクラス
Include\errordescription.mqh MetaTrader 5が生成するすべてのエラーコードの説明を含むファイル(MQL5言語用)
Include\jason.mqh JSONプロトコルのシリアル化とデシリアル化のためのライブラリ
Include\logging.mqh  CDiscordクラス(Webhook送信元)によって生成されたすべての情報とエラーをログに記録するためのライブラリ 
Experts\Discord EA.mq5  Discord Webhookをテストし、割り当てられたDiscord Webhook URLに取引アラートを送信するためのEA 

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

添付されたファイル |
Attachments.zip (19.26 KB)
MQL5で自己最適化エキスパートアドバイザーを構築する(第8回):複数戦略分析(2) MQL5で自己最適化エキスパートアドバイザーを構築する(第8回):複数戦略分析(2)
次のフォローアップディスカッションにぜひご参加ください。今回は、これまでの2つの取引戦略を統合し、アンサンブル取引戦略(複合戦略)を作成する方法を解説します。複数の戦略を組み合わせる際のさまざまな手法を紹介するとともに、パラメータ空間の制御方法についても説明します。これにより、パラメータの数が増えても、効果的な最適化が可能な状態を保つことができます。
データサイエンスとML(第44回):ベクトル自己回帰(VAR)を用いた外国為替OHLC時系列予測 データサイエンスとML(第44回):ベクトル自己回帰(VAR)を用いた外国為替OHLC時系列予測
本記事では、ベクトル自己回帰(VAR: Vector Autoregression)モデルを用いて、複数の通貨ペアのOHLC(始値、高値、安値、終値)時系列データを予測する方法を解説します。VARモデルの実装、学習、MetaTrader5上でのリアルタイム予測までをカバーし、通貨間の相互依存関係を分析して取引戦略の改善に役立てることができます。
MQL5での取引戦略の自動化(第20回):CCIとAOを使用した多銘柄戦略 MQL5での取引戦略の自動化(第20回):CCIとAOを使用した多銘柄戦略
この記事では、CCI (Commodity Channel Index)とAO (Awesome Oscillator)を用いてトレンド反転を検出する多銘柄取引戦略を作成します。戦略の設計、MQL5での実装、バックテストのプロセスについて解説します。記事の最後には、パフォーマンス改善のためのヒントも紹介します。
プライスアクション分析ツールキットの開発(第28回):Opening Range Breakout Tool プライスアクション分析ツールキットの開発(第28回):Opening Range Breakout Tool
各取引セッションの始まりでは、市場の方向性の偏りは、価格が初期価格幅(オープニングレンジ)を突破して初めて明確になります。本記事では、MQL5エキスパートアドバイザー(EA)を構築し、セッション開始直後の初期価格幅のブレイクアウトを自動的に検出して分析し、タイムリーでデータ駆動型のシグナルを提供して自信ある日中エントリーを可能にする方法を探ります。