在 MQL5 中创建交易管理员面板(第五部分):双因素认证(2FA)·综合运用
🔐

在 MQL5 中创建交易管理员面板(第五部分):双因素认证(2FA)·综合运用

(3/3)· 从 GUI 到 Telegram API,用 MQL5 把双因素认证做成可跑的通关闭环

实战向 第 3/3 篇
只挂密码验证的管理员面板在 MT5 里等于把门虚掩。本篇把 2FA 的 OTP 生成、GUI 输入与 Telegram 推送串成一条可运行链路,省去你拼凑散件的试错成本。

◍ 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,跑起来会先卡在鉴权窗——验证面板生命周期从这步开始。

MQL5 / C++
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,多半是图表对象权限或同名控件已存在。

MQL5 / C++
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 线即可验证交互。

MQL5 / C++
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 有泄露风险。

MQL5 / C++
      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 涉及实盘信号,此类面板若误放权限可能引发高风险操作,验证环节不建议省。

MQL5 / C++
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 与像素边界是否超出面板范围。

MQL5 / C++
   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 + spacingheight + 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 取文案直发,不碰输入框。 清屏按钮 OnClearButtonClickinputBox 置空串并手动调 OnInputChange 刷新计数——如果你自己写面板,漏掉这个刷新调用,字符计数器就不会归零。

MQL5 / C++
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 资源较多,在低点差时段做交互验证可能更顺滑,但仍属高风险操作环境。

MQL5 / C++
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 毫秒。外汇与贵金属行情波动剧烈,这类自动推送仅作辅助通知,不构成任何交易方向暗示。

MQL5 / C++
  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 毫秒)、证书问题还是接口格式错。外汇与贵金属波动剧烈,这类外发仅作辅助提醒,不替代风控。

MQL5 / C++
  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、物理隔离设备,比任何代码都实在。 做外汇和贵金属自动化本就高风险,安全实践松一寸,敏感数据就漏一丈。把这套实时验证码逻辑在本地跑通,比看十篇概述都有用。

把验证码推送交给小布盯盘
这些诊断与推送链路小布盯盘的 AIGC 已内置,打开对应品种页即可看到面板安全状态,把重复劳动交给小布,你专注决策。

常见问题

不建议直接当种子。MathRand() 周期与种子可控性差,正式 2FA 应叠加时间因子与自定义盐值,降低被穷举概率。
倾向设在 15 分钟内,兼顾人工输入与令牌失效节奏;过长会放大截屏泄露风险,过短则实盘误触率高。
多为证书、网络权限或 JSON 封装格式问题;建议在测试环境先跑通回显再接面板逻辑。
可以。小布盯盘对品种页的会话与推送做持续诊断,出现异地或高频校验失败会标记,帮你早一步发现入侵倾向。
仅在管理会话建立时触发,常规 tick 级交易不受影响;若把 2FA 错放进每笔下单流才会明显拖慢。