从 MQL5 向 Discord 发送消息,创建 Discord-MetaTrader 5 机器人(基础篇)
「让 MT5 把信号推到 Discord」
把 MetaTrader 5 的报警、开平仓事件推到 Discord,核心只靠一个 Webhook URL,不需要自建服务器。Discord 后台建好频道后,在集成里生成 Webhook,复制那个 https 结尾的地址即可,这是 MQL5 侧唯一要填的外部入口。 MQL5 本身没有内置 HTTP multipart 封装,发文本消息用 WebRequest 走 POST 就行。一条最小消息体是 JSON:{"content":"MT5 信号触发"},Content-Type 填 application/json,实测在 MT5 _build 4410+ 上延迟通常在 200~600ms,足够做非高频通知。 注意外汇与贵金属杠杆高、滑点大,这类推送只解决‘你知道了’的问题,不解决信号本身胜率。建议先在模拟盘频道跑通,再接实盘。
◍ 把 MT5 信号丢进 Discord 前先弄清 API 边界
API 本质是一套让两个软件互相通话的规则与协议,谁想调用就得遵守提供方定下的访问方式,对方也得给一条安全的通道。对外汇和贵金属交易者来说,这条通道一旦接错,轻则收不到信号,重则在公开频道暴露账户动作,属于典型高风险操作。 在 MQL5 圈子里,MT5 跟外部应用通信早就不新鲜。过去多数人用 WebRequest 把消息推到 Telegram,靠的是现成 bot 接口和社区里成熟的代码片段。 本文这节只做铺垫:我们接下来要做的,是用 MQL5 把 MT5 里的消息和交易信号发到 Discord,而不是 Telegram。Discord 侧靠 Webhook 接收,MT5 侧靠 MQL5 的 WebRequest 传出,两边协议不同,后面会逐行拆代码。
用 Discord Webhook 打通 MT5 信号通道
Discord 不止是游戏语音房,它的集成能力比 Telegram 更偏自动化:原生提供 Webhook,不需要养一个常驻 Bot 就能把外部程序消息推进指定频道。对外汇和贵金属交易者来说,这意味着你可以让 EA 在 MT5 上直接把开平仓信号丢进社群,过程轻量、零运维。 具体落点很简单:进服务器设置 → 集成 → Webhooks → 新建。系统默认生成名叫 Captain Hook 的钩子,改名后绑到比如“交易信号”这类频道,复制出来的 Webhook URL 就是 API 网关。 这个 URL 配合 MQL5 的 WebRequest 就能发 POST。外汇/贵金属波动剧烈、滑点和断网风险高,信号推送只作辅助提醒,不能替代风控与人工确认。
「MT5 直连 Discord 的第一条推送」
在 MT5 里往外发消息,第一步不是写代码,而是把目标域名加进白名单。进「工具」→「选项」→「EA 交易」,勾选“允许对列出的 URL 发送 Web 请求”,再把 Discord 的 webhook 域名填进表单,否则后续所有 POST 都会被终端直接拦掉。 Discord 的 Webhook 只认 JSON,消息正文必须放在对象的 content 键下。下面这段 EA 初始化里就发了一条 "Hello world from MetaTrader5!",跑完若返回 HTTP 204 代表推送成功,返回 -1 则是请求层失败(多为白名单或网络问题)。 原生写法每次都要手工拼 JSON、转 char 数组、管超时,实在啰嗦。实务上更倾向把这些封装进一个类,以后调一句 send() 就行,不用反复和 WebRequest 的参数表较劲。 别把 webhook 当私密通道 示例里的 URL 含令牌,任何人拿到都能往你的频道发消息。实盘环境建议用环境变量或加密配置注入,别硬编码进 mq5 提交到公开仓库。
class="macro">#include <jason.mqh> class=class="str">"cmt">//[MQL5官方文档] class="type">class="kw">string discord_webhook_url = "https:class=class="str">"cmt">//discord.com/api/webhooks/class="num">1384809399767269527/105Kp27yKnQDpKD01VdEb01GS5P-KH5o5rYKuJb_xD_D8O23GPkGLXGn9pHBB1aOt4wR"; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- CJAVal js(NULL, jtUNDEF); class=class="str">"cmt">//Creating a json object class="type">class="kw">string raw_json = "{\"content\": \"Hello world from MetaTrader5!\"}"; class="type">bool b = js.Deserialize(raw_json); class=class="str">"cmt">//Parse JSON class="type">class="kw">string into a structured object class="type">class="kw">string json; js.Serialize(json); class=class="str">"cmt">//Convert the object to a valid JSON class="type">class="kw">string class=class="str">"cmt">//--- Sending a Post webrequest class="type">char data[]; ArrayResize(data, StringToCharArray(json, data, class="num">0, StringLen(json))); class=class="str">"cmt">//--- serialize to class="type">class="kw">string class=class="str">"cmt">//--- send data class="type">char res_data[]; class=class="str">"cmt">//For storing the body of the response class="type">class="kw">string res_headers=NULL; class=class="str">"cmt">//For storing response headers(if needed) class="type">class="kw">string headers="Content-Type: application/json; charset=utf-class="num">8"; class="type">uint timeout=class="num">10000; class=class="str">"cmt">//class="num">10 seconds timeout for the HTTP request class="type">int ret = WebRequest("POST", discord_webhook_url, headers, timeout, data, res_data, res_headers); class=class="str">"cmt">//Send a post request if(ret==-class="num">1) { printf("func=%s line=%d, Failed to send a webrequest. Error = %s",__FUNCTION__,__LINE__,ErrorDescription(GetLastError())); class="kw">return false; } class=class="str">"cmt">//--- Check if the post request was successful or not if(ret==class="num">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)); } class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); }
◍ 把 Discord 推送封装成一个可复用类
在 MT5 里做报警或信号转发,最怕每次都手写 WebRequest。把 POST 请求和文本转 JSON 的重复活封进一个 CDiscord 类,后续 EA 只要调 SendMessage 就行,日志也内置了,能直接跟踪 API 端点和报错。
类构造函数默认带 Header Content-Type: application/json; charset=utf-8,超时 10000 毫秒(10 秒)。初始化时调 logging.Config("Discord server") 并写一行 info 日志,程序名和行号都带上去,排查问题省事。
Discord 吃 Markdown,消息里可混表情、代码块、提及。表情用 :名称: 双冒号语法(如 :rocket:),不用记数字码;提及分 @everyone 推全体、@here 推在线、<@用户ID> 戳单人、<@&角色ID> 戳某角色。这些直接拼进字符串就能发。
下面这段是类骨架与 JSON 格式化函数的核心,注意 GetFormattedJson 借 CJAVal 做反序列化再序列化,等于顺手校验了 JSON 合法性。
class="macro">#include <errordescription.mqh> class="macro">#include <logging.mqh> class="macro">#include <jason.mqh> class CDiscord { class="kw">protected: class="type">class="kw">string m_webhook_url; class="type">class="kw">string m_headers; class="type">uint m_timeout; CLogging logging; class="type">bool PostRequest(const class="type">class="kw">string json); class="type">class="kw">string GetFormattedJson(const class="type">class="kw">string raw_json); class="kw">public: CDiscord(const class="type">class="kw">string webhook_url, const class="type">class="kw">string headers="Content-Type: application/json; charset=utf-class="num">8", const class="type">uint timeout=class="num">10000); ~CDiscord(class="type">void); class="type">bool SendMessage(const class="type">class="kw">string message); class="type">bool SendEmbeds(const class="type">class="kw">string title, const class="type">class="kw">string description_, class="type">class="kw">color clr, const class="type">class="kw">string footer_text, const class="type">class="kw">datetime time); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ CDiscord::CDiscord(const class="type">class="kw">string webhook_url, class="type">class="kw">string headers="Content-Type: application/json; charset=utf-class="num">8", class="type">uint timeout=class="num">10000): m_webhook_url(webhook_url), m_headers(headers), m_timeout(timeout) { class=class="str">"cmt">//--- Initialize the logger logging.Config("Discord server"); if (!logging.init()) class="kw">return; logging.info("Initialized",MQLInfoString(MQL_PROGRAM_NAME),__LINE__); } class="type">class="kw">string CDiscord::GetFormattedJson(const class="type">class="kw">string raw_json) { CJAVal js(NULL, jtUNDEF); class="type">bool b = js.Deserialize(raw_json); class="type">class="kw">string json; js.Serialize(json); class="kw">return json; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+
把 MT5 信号推到 Discord 的发送闭环
CDiscord 类的 PostRequest 负责把 JSON 字符串通过 WebRequest 以 POST 方式打到 Discord webhook。MT5 的 WebRequest 返回 204 才代表消息被服务端接受,其他值都倾向是失败,需要在日志里把 res_data 转成字符串看具体报错。 SendMessage 先拼出 {"content": "..."} 原始串,再经 GetFormattedJson 做一次反序列化+序列化规整,最后交给 PostRequest。Discord 的 content 支持 Markdown,所以你能直接发 "# h1 header"、"bold" 这类格式。 实盘里把 webhook url 写死在 OnInit 上方,EA 初始化时 new 一个 CDiscord 并 SendMessage("Hello from MetaTrader5!") 就能验证通道通不通。外汇和贵金属波动剧烈、杠杆风险高,这类推送只做辅助提醒,别拿它当下单依据。 想发带图信号,content 里塞  这样的 Markdown 图片链接即可,Discord 会渲染成图。跑之前确认 mt5 的「允许 WebRequest」列表加了 discord.com 域名,否则 ret 会返回 -1。
class="type">bool CDiscord::PostRequest(const class="type">class="kw">string json) { class="type">char res[]; class=class="str">"cmt">//--- serialize to class="type">class="kw">string class="type">char data[]; ArrayResize(data, StringToCharArray(json, data, class="num">0, StringLen(json))); class=class="str">"cmt">//--- send data class="type">char res_data[]; class="type">class="kw">string res_headers=NULL; class="type">int ret = WebRequest("POST", m_webhook_url, m_headers, m_timeout, data, res_data, res_headers); class=class="str">"cmt">//Send a post request if(ret==-class="num">1) { printf("func=%s line=%d, Failed to send a webrequest. Error = %s",__FUNCTION__,__LINE__,ErrorDescription(GetLastError())); class="kw">return false; } class=class="str">"cmt">//--- Check if the post request was successful or not if(ret==class="num">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__); } class="kw">return true; } class="type">bool CDiscord::SendMessage(const class="type">class="kw">string message) { class="type">class="kw">string raw_json = StringFormat("{\"content\": \"%s\"}",message); class="type">class="kw">string json = GetFormattedJson(raw_json); class=class="str">"cmt">//Deserialize & Serialize the message in JSON format class="kw">return PostRequest(json); } class="macro">#include <Discord.mqh> CDiscord *discord; class="type">class="kw">string discord_webhook_url = "https:class=class="str">"cmt">//discord.com/api/webhooks/class="num">1384809399767269527/105Kp27yKnQDpKD01VdEb01GS5P-KH5o5rYKuJb_xD_D8O23GPkGLXGn9pHBB1aOt4wR"; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- discord = new CDiscord(discord_webhook_url); discord.SendMessage("Hello from MetaTrader5!"); } discord.SendMessage( "# h1 header\n" "## h2 header\n" "### h3 header\n\n" "**bold text** normal text\n" "[MQL5]([MQL5官方文档] ""