从 MQL5 向 Discord 发送消息,创建 Discord-MetaTrader 5 机器人·进阶篇
「把交易信号推到 Discord 的代码片段」
在 MT5 里把策略告警外发到 Discord,最省事的做法是用 Webhook 对象直接 SendMessage。下面这段演示了如何塞代码块和表情符号,开 MT5 新建 EA 把 discord 指针接好就能跑。 发消息时若要在 Discord 里高亮代码,要在三个反引号后写语言名。例如 MQL5 块里顺手放了指针释放逻辑:CheckPointer(discord)!=POINTER_INVALID 时 delete discord,避免内存泄漏;Python 块则只是 while(True): break 的占位。 discord.SendMessage(":rotating_light: Trade Alert!"); 这行会推一条带警灯 emoji 的纯文本,适合做裸告警。 为了不在代码里到处写裸字符串,用 #define 把常用 emoji 映射成常量更干净。原文列了 15 个宏,从 :rocket: 到 :blush: 都有,覆盖趋势、风险、情绪三类提示。复制下面宏定义进头文件,之后发消息拼 DISCORD_EMOJI_CHART_UP 比手写字符串不易拼错。 外汇与贵金属波动剧烈,这类推送只解决通知延迟,不替你过滤假突破,实盘前先在模拟盘验证 Webhook 可达性。
discord.SendMessage("```MQL5\n" class=class="str">"cmt">//we put the type of language for markdown highlighters after three consecutive backticks "if (CheckPointer(discord)!=POINTER_INVALID)\n" " class="kw">delete discord;\n" "```"); discord.SendMessage("```Python\n" "while(True):\n" " class="kw">break\n" "```"); discord.SendMessage(":rotating_light: Trade Alert!"); class="macro">#define DISCORD_EMOJI_ROCKET ":rocket:" class="macro">#define DISCORD_EMOJI_CHART_UP ":chart_with_upwards_trend:" class="macro">#define DISCORD_EMOJI_CHART_DOWN ":chart_with_downwards_trend:" class="macro">#define DISCORD_EMOJI_BAR_CHART ":bar_chart:" class="macro">#define DISCORD_EMOJI_BOMB ":bomb:" class="macro">#define DISCORD_EMOJI_THUMBS_UP ":thumbsup:" class="macro">#define DISCORD_EMOJI_THUMBS_DOWN ":thumbsdown:" class="macro">#define DISCORD_EMOJI_WARNING ":warning:" class="macro">#define DISCORD_EMOJI_JOY ":joy:" class="macro">#define DISCORD_EMOJI_SOB ":sob:" class="macro">#define DISCORD_EMOJI_SMILE ":smile:" class="macro">#define DISCORD_EMOJI_FIRE ":fire:" class="macro">#define DISCORD_EMOJI_STAR ":star:" class="macro">#define DISCORD_EMOJI_BLUSH ":blush:"
把状态写成 Emoji 宏
在 MT5 里给 Discord 推信号,最烦的是每条消息手敲表情符号。用预处理器一次性把 Discord 的 emoji 短码定义成宏,后面发消息直接拼常量,可读性立刻上来。 上面这组宏覆盖了从 :thinking: 到 :keycap_ten: 共 18 个基础符号,外加红绿圆点,足够标记「思考中 / 报警 / 通过 / 序号」等 EA 运行状态。 实际调用时直接字符串拼接:discord.SendMessage(DISCORD_EMOJI_ROTATING_LIGHT " Trade Alert! ") 就会在频道里先弹旋转警灯再出文字。外汇与贵金属波动剧烈,这类推送只作状态同步,任何信号都不构成入场保证,爆仓风险自担。 顺手补一句,@everyone 和 @here 的 mention 语法在 Discord webhook 里照样生效,但群里人多时慎用,容易刷屏被踢。
class="macro">#define DISCORD_EMOJI_THINKING ":thinking:" class="macro">#define DISCORD_EMOJI_ROTATING_LIGHT ":rotating_light:" class="macro">#define DISCORD_EMOJI_X ":x:" class="macro">#define DISCORD_EMOJI_WHITE_CHECK_MARK ":white_check_mark:" class="macro">#define DISCORD_EMOJI_BALLOT_BOX_WITH_CHECK ":ballot_box_with_check:" class="macro">#define DISCORD_EMOJI_HASH ":hash:" class="macro">#define DISCORD_EMOJI_ASTERISK ":asterisk:" class="macro">#define DISCORD_EMOJI_ZERO ":zero:" class="macro">#define DISCORD_EMOJI_ONE ":one:" class="macro">#define DISCORD_EMOJI_TWO ":two:" class="macro">#define DISCORD_EMOJI_THREE ":three:" class="macro">#define DISCORD_EMOJI_FOUR ":four:" class="macro">#define DISCORD_EMOJI_FIVE ":five:" class="macro">#define DISCORD_EMOJI_SIX ":six:" class="macro">#define DISCORD_EMOJI_SEVEN ":seven:" class="macro">#define DISCORD_EMOJI_EIGHT ":eight:" class="macro">#define DISCORD_EMOJI_NINE ":nine:" class="macro">#define DISCORD_EMOJI_TEN ":keycap_ten:" class="macro">#define DISCORD_EMOJI_RED_CIRCLE ":red_circle:" class="macro">#define DISCORD_EMOJI_GREEN_CIRCLE ":green_circle:" discord.SendMessage(DISCORD_EMOJI_ROTATING_LIGHT " Trade Alert! " DISCORD_EMOJI_JOY); 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 最实用的一个点:它不锁死默认昵称和头像,每次发请求都能临时改。多个策略机器人共用同一个 webhook 往一个频道推信号时,靠不同身份区分来源,比事后翻日志省事得多。 我们把身份字段 m_name 和 m_avatar_url 声明为 public,原因很直接——用户可能在类初始化之后还想改昵称或换头像,公开才能随时访问。构造函数里把传入的 name 和 avatar_url 直接赋给成员,并顺手用 logging.Config(m_name) 把日志标签也绑上,后续排查某个用户名发了什么就清晰了。 SendMessage() 里拼 JSON 时,把 "username" 和 "avatar_url" 写死进模板,值取自 m_name / m_avatar_url。这样不传原始 JSON 的普通调用也会自动带身份。 注意 SendJSON() 应把这些字段设为可选:当用户想完全自己控制 payload 时,就别强行塞用户名。外汇与贵金属信号推送涉及高风险,身份伪装仅用于区分策略,不代表任何胜率保证,开 MT5 挂 EA 前先确认 webhook 地址没写错。
class CDiscord { class="kw">protected: class=class="str">"cmt">//... class=class="str">"cmt">//... class="kw">public: class="type">class="kw">string m_name; class="type">class="kw">string m_avatar_url; CDiscord(const class="type">class="kw">string webhook_url, const class="type">class="kw">string name, const class="type">class="kw">string avatar_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::CDiscord(const class="type">class="kw">string webhook_url, const class="type">class="kw">string name, const class="type">class="kw">string avatar_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): m_webhook_url(webhook_url), m_headers(headers), m_timeout(timeout), m_name(name), m_avatar_url(avatar_url) { class=class="str">"cmt">//--- Initialize the logger logging.Config(m_name); if (!logging.init()) class="kw">return; logging.info("Initialized",MQLInfoString(MQL_PROGRAM_NAME),__LINE__); } class="type">bool CDiscord::SendMessage(const class="type">class="kw">string message) { class="type">class="kw">string raw_json = StringFormat("{" "\"username\": \"%s\"," "\"avatar_url\": \"%s\"," "\"content\": \"%s\"" "}", m_name, m_avatar_url, message);
「给 Discord 推送换上自定义身份」
把交易信号丢到 Discord 并不只是发一段文本,EA 初始化时给 Webhook 绑定用户名和头像,能让多条策略在群里一眼区分开。下面这段 MQL5 在 OnInit 里 new 了一个 CDiscord 对象,构造参数依次塞入 webhook 地址、机器人名和头像链接,随后调 SendMessage 自报身份。 CDiscord 的 SendJSON 方法里有个 use_id 开关:为真时把类构造时存的 m_name 和 m_avatar_url 写进 JSON 的 username 与 avatar_url 字段,再序列化 POST 出去;关掉它就只发你传进去的裸 raw_json。实测用文内写死的 webhook(ID 1384809399767269527 那段)能直接推到对应频道,换身份不用重建 hook。 在外接消息通道这块,外汇与贵金属行情跳动快、误报成本高,自动化推送只是通知手段,信号本身仍需人工核对,杠杆品种高风险不可忽视。
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="type">bool CDiscord::SendJSON(const class="type">class="kw">string raw_json, class="type">bool use_id=true) { CJAVal js; js.Deserialize(raw_json); if (use_id) class=class="str">"cmt">//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; } class="type">class="kw">string json; js.Serialize(json); class="kw">return PostRequest(json); } class="macro">#include <Discord.mqh> CDiscord *discord; input class="type">class="kw">string discord_webhook_url = "https:class=class="str">"cmt">//discord.com/api/webhooks/class="num">1384809399767269527/105Kp27yKnQDpKD01VdEb01GS5P-KH5o5rYKuJb_xD_D8O23GPkGLXGn9pHBB1aOt4wR"; input class="type">class="kw">string avatar_url_ = "https:class=class="str">"cmt">//imgur.com/m7sVf51.jpeg"; input class="type">class="kw">string bots_name = "Signals Bot"; 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, bots_name, avatar_url_); discord.SendMessage("Same webhook but, different Identity :smile: cheers!"); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); }
处理嵌入式文件和图像文件
尽管在创建 Discord 类时已将图像附加到上一节中讨论的消息中,但仍有一种正确的方法可以将文件和嵌入项附加到消息中。 我们可以通过独立的 JSON 请求来实现这一点。 结果。 遗憾的是, 尽管 Discord 能够接收文件, 但您无法使用 Web Request 直接从 MQL5 发送图像。 目前,您所能做的最好的事情就是从在线托管文件的某个链接中共享图像。 在之前的例子中,我必须使用 imgur.com 。
discord.SendJSON( <span class="class="type">class="kw">string">"{"</span> <span class="class="type">class="kw">string">"\"content\": \"Here&class="macro">#x27;s the latest chart update:\","</span> <span class="class="type">class="kw">string">"\"embeds\": ["</span> <span class="class="type">class="kw">string">"{"</span> <span class="class="type">class="kw">string">"\"title\": \"Chart Update\","</span> <span class="class="type">class="kw">string">"\"image\": {"</span> <span class="class="type">class="kw">string">"\"url\": \"https:class=class="str">"cmt">//imgur.com/SBsomI7.png\""</span> <span class="class="type">class="kw">string">"}"</span> <span class="class="type">class="kw">string">"}"</span> <span class="class="type">class="kw">string">"]"</span> <span class="class="type">class="kw">string">"}"</span> );
◍ 用 EA 把 MT5 成交推到 Discord 频道
在 MT5 里做外部消息推送,核心钩子是 OnTradeTransaction。它能在每笔交易事务发生时被触发,比轮询持仓省资源,也更适合做实时通知。 开仓、修改、平仓三类动作靠宏区分:DEAL_ENTRY_IN 判为开仓,DEAL_ENTRY_OUT 且非 SL/TP 原因判为手动平仓,TRADE_TRANSACTION_POSITION 则对应止损止盈改动。实测用一键交易手动开两个反向单,Discord 能秒级收到带内联价格的卡片。 Discord 排版有个细节:价格类短值用单个反引号包裹会生成行内代码块,视觉上和数字分隔明显,方便复制;大段内容才用三个反引号。平仓盈利放 👍、亏损放 👎,且已平头寸走交易单号而非持仓编号——手动平掉的那笔,DEAL_REASON 会落为客户端(桌面终端)。 外汇与贵金属杠杆高、滑点跳空频繁,这类通知只解决「知道发生了什么」,不替代风控。把下面宏和函数骨架直接塞进 EA,改下 Webhook 发送段就能在 MT5 跑通验证。
class="macro">#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) class="macro">#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)) class="macro">#define IS_TRANSACTION_POSITION_MODIFIED(trans.type == TRADE_TRANSACTION_POSITION) class="type">void OnTradeTransaction(const MqlTradeTransaction& trans, const class="type">MqlTradeRequest& request, const class="type">MqlTradeResult& result) { class="type">ulong deal_ticket = trans.deal; class="type">ulong position_ticket = trans.position; class=class="str">"cmt">//--- m_deal.Ticket(deal_ticket); class=class="str">"cmt">//select a deal by it&class="macro">#x27;s ticket if (IS_TRANSACTION_POSITION_OPENED) { if (deal_ticket==class="num">0) class="kw">return; if (!m_position.SelectByTicket(position_ticket)) class=class="str">"cmt">//select a position by ticket class="kw">return; if (!m_symbol.Name(m_deal.Symbol())) class=class="str">"cmt">//select a symbol from a position { printf("line=%d Failed to select symbol %s. Error = %s",__LINE__,m_deal.Symbol(),ErrorDescription(GetLastError())); class="kw">return; } class="type">class="kw">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"