在 MQL5 中创建交易管理员面板(第五部分):双因素认证(2FA)·综合运用
(3/3)· 从 GUI 到 Telegram API,用 MQL5 把双因素认证做成可跑的通关闭环
◍ EA 面板的鉴权与全局变量骨架
这段初始化代码定义了一个带登录验证的 MT5 管理面板 EA。它在 OnInit 里先弹鉴权窗,失败就返回 INIT_FAILED 直接退出,过了才建 500×500 的 adminPanel 对话框并默认隐藏。 input string QuickMessage3~8 预设了 6 条快捷话术,比如 "In deep profits"、"Hold position"、"Swing Entry",交易时一键发到 Telegram 省打字。InputChatId 和 InputBotToken 留了 YOUR_ 占位,得换成自己的;常量 Hardcoded2FAChatId 和 Hardcoded2FABotToken 则是双因子用的硬写 ID。 全局变量里 Password 写死成 "2024",MAX_MESSAGE_LENGTH 是 Telegram 单消息上限 4096 字符,availableFonts 给了 Arial、Courier New、Verdana、Times New Roman 四款。下面逐行拆 OnInit 和 ShowAuthenticationPrompt 头部: 别把硬编码密码当安全 示例里 Password="2024" 只是演示,实盘 EA 绝不可明文写密码,外汇贵金属杠杆高,源码泄露可能被他人借面板操纵你的账户。 开 MT5 新建 EA,把这段粘进编辑器,改掉 YOUR_CHAT_ID 和 BotToken,跑起来会先卡在鉴权窗——验证面板生命周期从这步开始。
input class="type">class="kw">string QuickMessage3 = "In deep profits"; input class="type">class="kw">string QuickMessage4 = "Hold position"; input class="type">class="kw">string QuickMessage5 = "Swing Entry"; input class="type">class="kw">string QuickMessage6 = "Scalp Entry"; input class="type">class="kw">string QuickMessage7 = "Book profit"; input class="type">class="kw">string QuickMessage8 = "Invalid Signal"; input class="type">class="kw">string InputChatId = "YOUR_CHAT_ID"; input class="type">class="kw">string InputBotToken = "YOUR_BOT_TOKEN"; class=class="str">"cmt">// Constants for 2FA const class="type">class="kw">string Hardcoded2FAChatId = "ENTER YOUR REAL CHAT ID"; const class="type">class="kw">string Hardcoded2FABotToken = "ENTER YOUR Telegram Bot Token"; class=class="str">"cmt">// Global variables CDialog adminPanel; CDialog authentication, twoFactorAuth; CButton sendButton, clearButton, changeFontButton, toggleThemeButton; CButton loginButton, closeAuthButton, twoFALoginButton, close2FAButton; CButton quickMessageButtons[class="num">8], minimizeButton, maximizeButton, closeButton; CEdit inputBox, passwordInputBox, twoFACodeInput; CLabel charCounter, passwordPromptLabel, feedbackLabel, twoFAPromptLabel, twoFAFeedbackLabel; class="type">bool minimized = false; class="type">bool darkTheme = false; class="type">int MAX_MESSAGE_LENGTH = class="num">4096; class="type">class="kw">string availableFonts[] = { "Arial", "Courier New", "Verdana", "Times New Roman" }; class="type">int currentFontIndex = class="num">0; class="type">class="kw">string Password = "class="num">2024"; class=class="str">"cmt">// Hardcoded password class="type">class="kw">string twoFACode = ""; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { if (!ShowAuthenticationPrompt()) { Print("Authorization failed. Exiting..."); class="kw">return INIT_FAILED; } if (!adminPanel.Create(ChartID(), "Admin Panel", class="num">0, class="num">30, class="num">30, class="num">500, class="num">500)) { Print("Failed to create admin panel dialog"); class="kw">return INIT_FAILED; } if (!CreateControls()) { Print("Control creation failed"); class="kw">return INIT_FAILED; } adminPanel.Hide(); Print("Initialization complete"); class="kw">return INIT_SUCCEEDED; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Show authentication input dialog | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool ShowAuthenticationPrompt() { if (!authentication.Create(ChartID(), "Authentication", class="num">0, class="num">100, class="num">100, class="num">500, class="num">300)) {
「在图表上拼出登录与两步验证面板」
这段逻辑负责把授权对话框的控件逐个挂到当前图表上。任何一个 Create 返回 false,都会向专家日志打印具体失败项并中断流程,返回 false 表示面板没起来。 密码框 passwordInputBox 用 Create(ChartID(), "PasswordInputBox", 0, 20, 70, 260, 95) 定位:左 20、上 70、宽 260、高 95 像素;提示标签放在上 20 处,反馈标签 feedbackLabel 初始为空、染成 clrRed,专门用来在输错时显示红字。 登录与关闭按钮分别落在左 20 / 左 120、上 120 的位置,宽高 100x140 与 200x140。所有控件 Add 进 authentication 容器后调 Show() 加 ChartRedraw() 强制重绘,函数才返回 true。 两步验证对话框 twoFactorAuth 是另一套:Create(ChartID(), "Two-Factor Authentication", 0, 100, 100, 500, 300) 把窗口摆在图表坐标 (100,100),尺寸 500x300。创建失败仅打印 "Failed to create 2FA dialog" 后直接 return,不阻塞主逻辑其他部分。 开 MT5 把这段塞进 EA 的 OnInit 或按钮回调里,改 Create 的第三、四参数就能挪面板位置;若日志频繁报 Failed to create,多半是图表对象权限或同名控件已存在。
Print("Failed to create authentication dialog"); class="kw">return false; } if (!passwordInputBox.Create(ChartID(), "PasswordInputBox", class="num">0, class="num">20, class="num">70, class="num">260, class="num">95)) { Print("Failed to create password input box"); class="kw">return false; } authentication.Add(passwordInputBox); if (!passwordPromptLabel.Create(ChartID(), "PasswordPromptLabel", class="num">0, class="num">20, class="num">20, class="num">260, class="num">40)) { Print("Failed to create password prompt label"); class="kw">return false; } passwordPromptLabel.Text("Enter password: Access Admin Panel"); authentication.Add(passwordPromptLabel); if (!feedbackLabel.Create(ChartID(), "FeedbackLabel", class="num">0, class="num">20, class="num">140, class="num">380, class="num">160)) { Print("Failed to create feedback label"); class="kw">return false; } feedbackLabel.Text(""); feedbackLabel.Color(clrRed); class=class="str">"cmt">// Red class="type">class="kw">color for incorrect attempts authentication.Add(feedbackLabel); if (!loginButton.Create(ChartID(), "LoginButton", class="num">0, class="num">20, class="num">120, class="num">100, class="num">140)) { Print("Failed to create login button"); class="kw">return false; } loginButton.Text("Login"); authentication.Add(loginButton); if (!closeAuthButton.Create(ChartID(), "CloseAuthButton", class="num">0, class="num">120, class="num">120, class="num">200, class="num">140)) { Print("Failed to create close button for authentication"); class="kw">return false; } closeAuthButton.Text("Close"); authentication.Add(closeAuthButton); authentication.Show(); ChartRedraw(); class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Show two-factor authentication input dialog | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void ShowTwoFactorAuthPrompt() { if (!twoFactorAuth.Create(ChartID(), "Two-Factor Authentication", class="num">0, class="num">100, class="num">100, class="num">500, class="num">300)) { Print("Failed to create 2FA dialog"); class="kw">return; }
在图表上拼出双因素验证面板
给 MT5 面板加二次验证,不是另开对话框,而是直接用 CWnd 派生类在图表坐标上画控件。下面这段把输入框、提示标签、反馈标签、验证按钮、关闭按钮依次 Create 出来,再统一塞进 twoFactorAuth 容器里显示。 坐标参数都是 (chart_id, name, subwin, x, y, x_size, y_size) 的顺序。例如输入框 twoFACodeInput 放在主图(0)、左上角偏移 (20,70)、宽 260 高 95;反馈标签 twoFAFeedbackLabel 放在 (20,140)、宽 380 高 160,初始文字为空、颜色设 clrRed,专门用来标红错误的 2FA 尝试。 全部 Create 成功后才调 twoFactorAuth.Show() 和 ChartRedraw() 刷新。任一个 Create 返回 false 就 Print 错误并 return,面板不会出现半成品。 事件路由在 OnChartEvent 里做:当 id == CHARTEVENT_OBJECT_CLICK 且 sparam 是 "TwoFALoginButton" 时,走对应的验证分支(原文在此截断)。你可以把这段直接贴进自己的 CWnd 面板工程,把坐标微调到不挡 K 线即可验证交互。
if (!twoFACodeInput.Create(ChartID(), "TwoFACodeInput", class="num">0, class="num">20, class="num">70, class="num">260, class="num">95)) { Print("Failed to create 2FA code input box"); class="kw">return; } twoFactorAuth.Add(twoFACodeInput); if (!twoFAPromptLabel.Create(ChartID(), "TwoFAPromptLabel", class="num">0, class="num">20, class="num">20, class="num">380, class="num">40)) { Print("Failed to create 2FA prompt label"); class="kw">return; } twoFAPromptLabel.Text("Enter the 2FA code sent to your Telegram:"); twoFactorAuth.Add(twoFAPromptLabel); if (!twoFAFeedbackLabel.Create(ChartID(), "TwoFAFeedbackLabel", class="num">0, class="num">20, class="num">140, class="num">380, class="num">160)) { Print("Failed to create 2FA feedback label"); class="kw">return; } twoFAFeedbackLabel.Text(""); twoFAFeedbackLabel.Color(clrRed); class=class="str">"cmt">// Red class="type">class="kw">color for incorrect 2FA attempts twoFactorAuth.Add(twoFAFeedbackLabel); if (!twoFALoginButton.Create(ChartID(), "TwoFALoginButton", class="num">0, class="num">20, class="num">120, class="num">100, class="num">140)) { Print("Failed to create 2FA login button"); class="kw">return; } twoFALoginButton.Text("Verify"); twoFactorAuth.Add(twoFALoginButton); if (!close2FAButton.Create(ChartID(), "Close2FAButton", class="num">0, class="num">120, class="num">120, class="num">200, class="num">140)) { Print("Failed to create close button for 2FA"); class="kw">return; } close2FAButton.Text("Close"); twoFactorAuth.Add(close2FAButton); twoFactorAuth.Show(); ChartRedraw(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handle chart events | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { if (id == CHARTEVENT_OBJECT_CLICK) { if (sparam == "LoginButton") { OnLoginButtonClick(); } else if (sparam == "CloseAuthButton") { OnCloseAuthButtonClick(); } else if (sparam == "TwoFALoginButton") {
◍ 面板事件分发与两步验证触发逻辑
在 MT5 自定义面板里,所有图形对象的交互都汇流到 OnChartEvent。上面这段把 sparam 字符串和具体按钮名做比对:带「TwoFALoginButton」「Close2FAButton」的走独立分支,其余统一进 switch 按 CHARTEVENT_OBJECT_CLICK / ENDEDIT 分类处理。 注意 QuickMessageButton 的解析手法:用 StringFind 判断前缀后,从 sparam 第 18 位截子串转 long 再减 1 得到索引。若你复制这套,按钮命名必须严格占满前 18 字符,否则索引会错乱。 密码校验函数在 OnLoginButtonClick 内:取密码框文本比对预设 Password,一致才生成 6 位随机码并推送到 Telegram,随后销毁登录层、弹出 2FA 输入框。失败仅回写标签不退出,属于软拒绝。外汇 / 贵金属脚本若嵌这类远端鉴权,需意识到硬编码 bot token 有泄露风险。
OnTwoFALoginButtonClick(); } else if (sparam == "Close2FAButton") { OnClose2FAButtonClick(); } } class="kw">switch (id) { case CHARTEVENT_OBJECT_CLICK: if (sparam == "SendButton") OnSendButtonClick(); else if (sparam == "ClearButton") OnClearButtonClick(); else if (sparam == "ChangeFontButton") OnChangeFontButtonClick(); else if (sparam == "ToggleThemeButton") OnToggleThemeButtonClick(); else if (sparam == "MinimizeButton") OnMinimizeButtonClick(); else if (sparam == "MaximizeButton") OnMaximizeButtonClick(); else if (sparam == "CloseButton") OnCloseButtonClick(); else if (StringFind(sparam, "QuickMessageButton") != -class="num">1) { class="type">long index = StringToInteger(StringSubstr(sparam, class="num">18)); OnQuickMessageButtonClick(index - class="num">1); } class="kw">break; case CHARTEVENT_OBJECT_ENDEDIT: if (sparam == "InputBox") OnInputChange(); class="kw">break; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handle login button click | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnLoginButtonClick() { class="type">class="kw">string enteredPassword = passwordInputBox.Text(); if (enteredPassword == Password) { twoFACode = GenerateRandom6DigitCode(); SendMessageToTelegram("A login attempt was made on the Admin Panel. Please use this code to verify your identity. " + twoFACode, Hardcoded2FAChatId, Hardcoded2FABotToken); authentication.Destroy(); ShowTwoFactorAuthPrompt(); Print("Password authentication successful. A 2FA code has been sent to your Telegram."); } else { feedbackLabel.Text("Wrong password. Try again."); passwordInputBox.Text(""); } }
「EA 面板的二次验证与控件装配」
在 MT5 自建 EA 的管理面板里,双因素验证(2FA)通常不是装饰,而是拦截未授权调用的第一道闸。下面这段事件函数展示了点击验证按钮后的分支逻辑:比对输入框文本与预设 twoFACode,一致才销毁验证窗并放出 adminPanel,否则清空输入并提示错误。 void OnTwoFALoginButtonClick() { string enteredCode = twoFACodeInput.Text(); // 取输入框里的用户代码 if(enteredCode == twoFACode) // 与内部保存的正确码比对 { twoFactorAuth.Destroy(); // 比对通过,销毁2FA弹窗 adminPanel.Show(); // 显示后台管理面板 Print("2FA authentication successful."); // 终端打印成功日志 } else { twoFAFeedbackLabel.Text("Wrong code. Try again."); // 不对就写反馈标签 twoFACodeInput.Text(""); // 清空输入框等重试 } } 关闭逻辑更要干脆:无论验证窗还是 2FA 窗,点关闭直接 Destroy 后调 ExpertRemove() 让 EA 退出,避免挂着一个半死不活的面板耗着图表资源。 void OnCloseAuthButtonClick() { authentication.Destroy(); ExpertRemove(); // 直接卸掉EA Print("Authentication dialog closed."); } void OnClose2FAButtonClick() { twoFactorAuth.Destroy(); ExpertRemove(); Print("2FA dialog closed."); } CreateControls() 负责把输入框、字符计数器、清除与发送按钮挂到 adminPanel 上。注意坐标参数:inputBox 占 (5,25) 到 (460,95),charCounter 在 (380,5) 到 (460,25),clearButton 与 sendButton 的 y 都从 95 到 125,x 分别落在 235 和 350——这些像素值直接决定面板在图表上的排布,改错一个就可能看不见按钮。 bool CreateControls() { long chart_id = ChartID(); if(!inputBox.Create(chart_id, "InputBox", 0, 5, 25, 460, 95)) { Print("Failed to create input box"); return false; } adminPanel.Add(inputBox); if(!charCounter.Create(chart_id, "CharCounter", 0, 380, 5, 460, 25)) { Print("Failed to create character counter"); return false; } charCounter.Text("0/" + IntegerToString(MAX_MESSAGE_LENGTH)); adminPanel.Add(charCounter); if(!clearButton.Create(chart_id, "ClearButton", 0, 235, 95, 345, 125)) { Print("Failed to create clear button"); return false; } clearButton.Text("Clear"); adminPanel.Add(clearButton); if(!sendButton.Create(chart_id, "SendButton", 0, 350, 95, 460, 125)) { Print("Failed to create send button"); return false; } 开 MT5 新建 EA 把这段粘进 OnInit 之后的事件区,把 twoFACode 写死成你自己的串,跑起来点按钮就能看到面板拦截效果;外汇与贵金属 EA 涉及实盘信号,此类面板若误放权限可能引发高风险操作,验证环节不建议省。
class="type">void OnTwoFALoginButtonClick() { class="type">class="kw">string enteredCode = twoFACodeInput.Text(); if(enteredCode == twoFACode) { twoFactorAuth.Destroy(); adminPanel.Show(); Print("2FA authentication successful."); } else { twoFAFeedbackLabel.Text("Wrong code. Try again."); twoFACodeInput.Text(""); } } class="type">void OnCloseAuthButtonClick() { authentication.Destroy(); ExpertRemove(); Print("Authentication dialog closed."); } class="type">void OnClose2FAButtonClick() { twoFactorAuth.Destroy(); ExpertRemove(); Print("2FA dialog closed."); } class="type">bool CreateControls() { class="type">long chart_id = ChartID(); if(!inputBox.Create(chart_id, "InputBox", class="num">0, class="num">5, class="num">25, class="num">460, class="num">95)) { Print("Failed to create input box"); class="kw">return false; } adminPanel.Add(inputBox); if(!charCounter.Create(chart_id, "CharCounter", class="num">0, class="num">380, class="num">5, class="num">460, class="num">25)) { Print("Failed to create character counter"); class="kw">return false; } charCounter.Text("class="num">0/" + IntegerToString(MAX_MESSAGE_LENGTH)); adminPanel.Add(charCounter); if(!clearButton.Create(chart_id, "ClearButton", class="num">0, class="num">235, class="num">95, class="num">345, class="num">125)) { Print("Failed to create clear button"); class="kw">return false; } clearButton.Text("Clear"); adminPanel.Add(clearButton); if(!sendButton.Create(chart_id, "SendButton", class="num">0, class="num">350, class="num">95, class="num">460, class="num">125)) { Print("Failed to create send button");
面板控件的坐标与按钮布局
在 MT5 的 GUI 面板里,每个 CButton 实例都要用 Create() 显式给定图表 ID 与左上、右下坐标,否则控件不会渲染。上面这段把发送、改字体、切主题三个按钮排在 y=95 的同一行,x 分别从 5、95、5 起算,宽度 230/90 像素,说明同一面板内允许重叠区由 Add() 顺序决定层级。 最小化、最大化、关闭三个系统风按钮被放在 y=-22 的标题条位置,x 间距固定 30 像素(375→405→435),这种负 Y 坐标用法能把控件钉在面板外框上,是自定义边框常用的技巧。 快捷消息区用循环批量建 8 个按钮:起始点 (5,160),单键宽 222、高 65、间隙 5,按 i%2 分两列、i/2 换行。算下来第 8 个按钮右下角约落在 x=454、y=290,开 MT5 把这段贴进 EA 就能直接看到 2×4 的网格。 坐标写错最常见的现象是按钮看不见——若 Create() 返回 false,Print 会报对应失败信息并中断初始化,这时优先核对 chart_id 与像素边界是否超出面板范围。
class="kw">return false; } sendButton.Text("Send"); adminPanel.Add(sendButton); if (!changeFontButton.Create(chart_id, "ChangeFontButton", class="num">0, class="num">95, class="num">95, class="num">230, class="num">115)) { Print("Failed to create change font button"); class="kw">return false; } changeFontButton.Text("Font<>"); adminPanel.Add(changeFontButton); if (!toggleThemeButton.Create(chart_id, "ToggleThemeButton", class="num">0, class="num">5, class="num">95, class="num">90, class="num">115)) { Print("Failed to create toggle theme button"); class="kw">return false; } toggleThemeButton.Text("Theme<>"); adminPanel.Add(toggleThemeButton); if (!minimizeButton.Create(chart_id, "MinimizeButton", class="num">0, class="num">375, -class="num">22, class="num">405, class="num">0)) { Print("Failed to create minimize button"); class="kw">return false; } minimizeButton.Text("_"); adminPanel.Add(minimizeButton); if (!maximizeButton.Create(chart_id, "MaximizeButton", class="num">0, class="num">405, -class="num">22, class="num">435, class="num">0)) { Print("Failed to create maximize button"); class="kw">return false; } maximizeButton.Text("[ ]"); adminPanel.Add(maximizeButton); if (!closeButton.Create(chart_id, "CloseButton", class="num">0, class="num">435, -class="num">22, class="num">465, class="num">0)) { Print("Failed to create close button"); class="kw">return false; } closeButton.Text("X"); adminPanel.Add(closeButton); class="kw">return CreateQuickMessageButtons(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create quick message buttons | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CreateQuickMessageButtons() { class="type">class="kw">string quickMessages[] = { QuickMessage1, QuickMessage2, QuickMessage3, QuickMessage4, QuickMessage5, QuickMessage6, QuickMessage7, QuickMessage8 }; class="type">int startX = class="num">5, startY = class="num">160, width = class="num">222, height = class="num">65, spacing = class="num">5; for (class="type">int i = class="num">0; i < ArraySize(quickMessages); i++) { class="type">bool created = quickMessageButtons[i].Create(ChartID(), "QuickMessageButton" + IntegerToString(i + class="num">1), class="num">0, startX + (i % class="num">2) * (width + spacing), startY + (i / class="num">2) * (height + spacing),
◍ 按钮回调与面板销毁的落地写法
这段逻辑承接了前面批量创建快捷消息按钮的循环:当某个按钮对象没建出来,created 为 false 时直接打印失败并 return false,避免把空指针加进 adminPanel。注意坐标算法里 (i % 2) 控制左右两列、(i / 2) 控制行偏移,配合 width + spacing 与 height + spacing 能把最多 8 个按钮排成 2×4 网格。
OnDeinit 里只做两件事:adminPanel.Destroy() 释放 GUI 资源,然后 Print 一条注销完成日志。实盘跑 EA 时若切换周期或改参数触发反初始化,面板不主动 Destroy 可能留下残留控件,这一点在 MT5 里能直接观察到。
自定义发送靠 OnSendButtonClick:先取 inputBox.Text(),用 StringLen(message) > 0 拦空串,再调 SendMessageToTelegram 发到 InputChatId。快捷消息则走 OnQuickMessageButtonClick(long index),内部用固定数组装 8 条 QuickMessage1~8,按传入 index 取文案直发,不碰输入框。
清屏按钮 OnClearButtonClick 把 inputBox 置空串并手动调 OnInputChange 刷新计数——如果你自己写面板,漏掉这个刷新调用,字符计数器就不会归零。
startX + (i % class="num">2) * (width + spacing) + width, startY + (i / class="num">2) * (height + spacing) + height); if (!created) { Print("Failed to create quick message button ", i + class="num">1); class="kw">return false; } quickMessageButtons[i].Text(quickMessages[i]); adminPanel.Add(quickMessageButtons[i]); } class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { adminPanel.Destroy(); Print("Deinitialization complete"); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handle custom message send button click | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnSendButtonClick() { class="type">class="kw">string message = inputBox.Text(); if (StringLen(message) > class="num">0) { if (SendMessageToTelegram(message, InputChatId, InputBotToken)) Print("Custom message sent: ", message); else Print("Failed to send custom message."); } else { Print("No message entered."); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handle clear button click | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnClearButtonClick() { inputBox.Text(""); OnInputChange(); Print("Input box cleared."); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handle quick message button click | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnQuickMessageButtonClick(class="type">long index) { class="type">class="kw">string quickMessages[] = { QuickMessage1, QuickMessage2, QuickMessage3, QuickMessage4, QuickMessage5, QuickMessage6, QuickMessage7, QuickMessage8 }; class="type">class="kw">string message = quickMessages[(class="type">int)index]; if (SendMessageToTelegram(message, InputChatId, InputBotToken)) Print("Quick message sent: ", message); else Print("Failed to send quick message."); }
「输入框与面板的实时交互逻辑」
在 MT5 自定义面板里,输入框内容变化时要同步刷新字符计数并显示上限。下面这段代码在 OnInputChange 中先取输入框文本长度,再拼成「当前/最大」格式写进计数器,最后调 ChartRedraw 强制重绘。 切换深色与浅色主题靠一个布尔变量 darkTheme 反转实现。OnToggleThemeButtonClick 里翻转后调用 UpdateThemeColors,把文字、按钮背景、边框、整体背景四组颜色按主题赋值,再遍历七个主按钮和快捷消息按钮数组统一换肤,结束时重绘图表。 字体轮换用 currentFontIndex 对 availableFonts 数组长度取模,点一次换下一款,SetFontForAll 会把新字体名下发给所有输入框和按钮。外汇与贵金属图表挂这类面板时需注意:EA 或脚本占用 UI 资源较多,在低点差时段做交互验证可能更顺滑,但仍属高风险操作环境。
class="type">void OnInputChange() { class="type">int currentLength = StringLen(inputBox.Text()); charCounter.Text(IntegerToString(currentLength) + "/" + IntegerToString(MAX_MESSAGE_LENGTH)); ChartRedraw(); } class="type">void OnToggleThemeButtonClick() { darkTheme = !darkTheme; UpdateThemeColors(); Print("Theme toggled: ", darkTheme ? "Dark" : "Light"); } class="type">void UpdateThemeColors() { class="type">class="kw">color textColor = darkTheme ? clrWhite : clrBlack; class="type">class="kw">color buttonBgColor = darkTheme ? clrDarkSlateGray : clrGainsboro; class="type">class="kw">color borderColor = darkTheme ? clrSlateGray : clrGray; class="type">class="kw">color bgColor = darkTheme ? clrDarkBlue : clrWhite; UpdateButtonTheme(clearButton, textColor, buttonBgColor, borderColor); UpdateButtonTheme(sendButton, textColor, buttonBgColor, borderColor); UpdateButtonTheme(toggleThemeButton, textColor, buttonBgColor, borderColor); UpdateButtonTheme(changeFontButton, textColor, buttonBgColor, borderColor); UpdateButtonTheme(minimizeButton, textColor, buttonBgColor, borderColor); UpdateButtonTheme(maximizeButton, textColor, buttonBgColor, borderColor); UpdateButtonTheme(closeButton, textColor, buttonBgColor, borderColor); for (class="type">int i = class="num">0; i < ArraySize(quickMessageButtons); i++) { UpdateButtonTheme(quickMessageButtons[i], textColor, buttonBgColor, borderColor); } ChartRedraw(); } class="type">void UpdateButtonTheme(CButton &button, class="type">class="kw">color textColor, class="type">class="kw">color bgColor, class="type">class="kw">color borderColor) { button.SetTextColor(textColor); button.SetBackgroundColor(bgColor); button.SetBorderColor(borderColor); } class="type">void OnChangeFontButtonClick() { currentFontIndex = (currentFontIndex + class="num">1) % ArraySize(availableFonts); SetFontForAll(availableFonts[currentFontIndex]); Print("Font changed to: ", availableFonts[currentFontIndex]); ChartRedraw(); } class="type">void SetFontForAll(class="type">class="kw">string fontName) {
面板控件与 Telegram 推送的底层接法
这段逻辑把管理面板上所有按钮的字体一次性统一:先逐个给输入框、清除、发送、主题切换、字体切换以及最小化最大化关闭按钮设字体,再用循环扫 quickMessageButtons 数组批量处理,避免快捷消息按钮漏掉样式。 双因素认证的 6 位随机码用 MathRand() % 1000000 生成,范围 0–999999;StringFormat("%06d", code) 负责补前导零,保证返回字符串恒为 6 位,比如 42 会变成 "000042"。 最小化只隐藏 adminPanel 与 minimizeButton,切换出 maximizeButton 和 closeButton;最大化反向操作并把 minimized 置 false。closeButton 直接调 ExpertRemove() 卸载 EA,面板交互就此终止。 发 Telegram 走 api.telegram.org/bot 接口,JSON 用手拼 "{\"chat_id\":\"...\",\"text\":\"...\"}",postData 由 StringToCharArray 减 1 去掉结尾 \0 得到,超时写死 5000 毫秒。外汇与贵金属行情波动剧烈,这类自动推送仅作辅助通知,不构成任何交易方向暗示。
inputBox.Font(fontName); clearButton.Font(fontName); sendButton.Font(fontName); toggleThemeButton.Font(fontName); changeFontButton.Font(fontName); minimizeButton.Font(fontName); maximizeButton.Font(fontName); closeButton.Font(fontName); for (class="type">int i = class="num">0; i < ArraySize(quickMessageButtons); i++) { quickMessageButtons[i].Font(fontName); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Generate a random class="num">6-digit code for 2FA | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string GenerateRandom6DigitCode() { class="type">int code = MathRand() % class="num">1000000; class=class="str">"cmt">// Produces a class="num">6-digit number class="kw">return StringFormat("%06d", code); class=class="str">"cmt">// Ensures leading zeros } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handle minimize button click | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnMinimizeButtonClick() { minimized = true; adminPanel.Hide(); minimizeButton.Hide(); maximizeButton.Show(); closeButton.Show(); Print("Panel minimized."); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handle maximize button click | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnMaximizeButtonClick() { if (minimized) { adminPanel.Show(); minimizeButton.Show(); maximizeButton.Hide(); closeButton.Hide(); minimized = false; Print("Panel maximized."); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handle close button click for admin panel | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnCloseButtonClick() { ExpertRemove(); Print("Admin panel closed."); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Send the message to Telegram | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool SendMessageToTelegram(class="type">class="kw">string message, class="type">class="kw">string chatId, class="type">class="kw">string botToken) { class="type">class="kw">string url = "https:class=class="str">"cmt">//api.telegram.org/bot" + botToken + "/sendMessage"; class="type">class="kw">string jsonMessage = "{\"chat_id\":\"" + chatId + "\", \"text\":\"" + message + "\"}"; class="type">char postData[]; ArrayResize(postData, StringToCharArray(jsonMessage, postData) - class="num">1); class="type">int timeout = class="num">5000;
◍ 用 WebRequest 把信号推到外部接口
在 MT5 里把交易信号发到外部服务,核心就是 WebRequest 的 POST 调用。下面这段把 url、超时和 postData 传进去,拿返回码判断成败。 代码里 responseCode == 200 才视为发送成功并打印消息,否则把 HTTP 状态码、GetLastError() 的系统错误码以及返回的字符数组转成字符串一并打出,方便你直接在专家日志里排错。 实盘跑之前,先在策略测试器里用模拟 url 验证一次:若返回非 200,日志会暴露是超时(常见 timeout 设 1000~5000 毫秒)、证书问题还是接口格式错。外汇与贵金属波动剧烈,这类外发仅作辅助提醒,不替代风控。
class="type">char result[]; class="type">class="kw">string responseHeaders; class="type">int responseCode = WebRequest("POST", url, "Content-Type: application/json\r\n", timeout, postData, result, responseHeaders); if (responseCode == class="num">200) { Print("Message sent successfully: ", message); class="kw">return true; } else { Print("Failed to send message. HTTP code: ", responseCode, " Error code: ", GetLastError()); Print("Response: ", CharArrayToString(result)); class="kw">return false; } }
「后台登录与2FA的实测表现」
在管理员面板里,密码安全与双因素验证(2FA)已经跑通。实测时先故意输错密码,系统直接拒绝进入,没有给任何绕过入口。 再试错验证码:填了错误的 6 位动态码,后端同样拦截,说明 2FA 不是摆设。 完整流程走下来,正确密码 + Telegram 推送的实时验证码才能登录。我们把 Telegram 应用和登录链路做了同步,验证码从下发到界面捕获全程可见,方便排查延迟。 这组测试只验证了「错误凭证必被拒、正确链路可进」的行为,不涉及任何收益或胜率,外汇与贵金属相关系统接入时仍属高风险操作,建议你在 MT5 环境外先单独压测登录接口。
记住这一条就够了
给 MT5 管理员面板接上 2FA,核心是用 Telegram 实时推验证码,并在输入错误时回退重输密码或码。这一层能把未授权登录的概率压下去,但前提是 Telegram 客户端别和交易终端装在同一台裸奔的机器上。 设备被入侵或手机落别人手里,验证层就形同虚设。不使用时退登 App、物理隔离设备,比任何代码都实在。 做外汇和贵金属自动化本就高风险,安全实践松一寸,敏感数据就漏一丈。把这套实时验证码逻辑在本地跑通,比看十篇概述都有用。