在MQL5中创建交易管理员面板(第八部分):分析面板·进阶篇
📊

在MQL5中创建交易管理员面板(第八部分):分析面板·进阶篇

(2/3)· 手动算盈亏比和品种分布太慢?这篇用CDialog+PieChart直接自动化

案例拆解新手友好 第 2/3 篇

很多管理员面板的用户还在导出成交记录用Excel手算盈亏比,资产类别分布更是靠记忆估。等算完,行情早就不在那个位置了。把统计交给实时面板,决策才不会滞后。

面板按钮的事件路由怎么写

在 MT5 面板 EA 里,所有图形控件的交互都靠 OnChartEvent 统一收口。sparam 传回的是被点对象的名字,用一串 else if 比对字符串就能把不同按钮分流到各自处理函数,比写一堆独立回调清爽得多。 下面这段是典型的图表事件分支:点击类事件 CHARTEVENT_OBJECT_CLICK 下,SendButton、ClearButton、ChangeFontButton 等各自进自己的 OnXxxClick;若对象名里带 QuickMessageButton,则用 StringFind 定位后截取下标数字,转成索引去发快捷消息。 注意 OnInit 里先调 ShowAuthenticationPrompt(),返回 false 就直接中断初始化——这意味着没过鉴权前,后续面板创建逻辑都不会跑。外汇与贵金属插件涉及账户与通信,这类未授权加载可能引发高风险暴露,实盘前务必在策略测试器里先断这条路径。 把这段抄进你的 EA,改 sparam 字符串就能接自己的控件;开 MT5 挂上图表,点几下按钮看 Print 出来的分支对不对,是最快的验证法。

MQL5 / C++
else if (sparam == "AnalyticsPanelAccessButton") {
      analyticsPanel.Show(); adminHomePanel.Hide();
      if (!analyticsPanel.Create(ChartID(), "Analytics Panel", class="num">0, class="num">500, class="num">450, class="num">1280, class="num">650) || !CreateAnalyticsPanelControls()) {};
      CreateAnalyticsPanel();
   }
   else if (sparam == "ShowAllButton") {
      analyticsPanel.Show(); communicationsPanel.Show(); tradeManagementPanel.Show(); adminHomePanel.Hide();
   }
   else if (sparam == "MinimizeComsButton") { OnMinimizeComsButtonClick(); }
   else if (sparam == "CloseComsButton") { communicationsPanel.Destroy(); }
   else if (sparam == "LoginButton") { OnLoginButtonClick(); }
   else if (sparam == "CloseAuthButton") { OnCloseAuthButtonClick(); }
   else if (sparam == "TwoFALoginButton") { 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 == "MinimizeComsButton") OnMinimizeComsButtonClick();
      else if (sparam == "CloseComsButton") OnCloseComsButtonClick();
      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="type">int OnInit()
{
   if (!ShowAuthenticationPrompt())
   {

◍ 初始化时把后台面板藏起来

EA 在 OnInit 里要依次做权限校验、面板创建、控件创建三道关卡,任何一步返回 INIT_FAILED 都会让指标或 EA 直接罢工,MT5 终端日志里能看到对应的 Print 报错。 后台主面板用 adminHomePanel.Create(ChartID(), "Admin Home Panel", 0, 30, 80, 330, 550) 生成,其中 30 和 80 是相对图表的 x/y 偏移像素,330×550 是面板宽高;这套尺寸在 1920×1080 屏上约占右下角 17% 面积,初始化完默认调用 Hide() 收起,不挡价格K线。 想验证的话,把 adminHomePanel.Hide() 注释掉重新编译,挂 EURUSD 的 M5 图,面板会直接浮在图表上;外汇与贵金属杠杆高,面板误触下单可能造成快速亏损,建议保留默认隐藏。

MQL5 / C++
      Print("Authorization failed. Exiting...");
      class="kw">return INIT_FAILED;
   }
   if (!adminHomePanel.Create(ChartID(), "Admin Home Panel", class="num">0, class="num">30, class="num">80,class="num">330, class="num">550))
   {
      Print("Failed to create Admin Home Panel");
      class="kw">return INIT_FAILED;
   }
   if (!CreateAdminHomeControls())
   {
      Print("Home panel control creation failed");
      class="kw">return INIT_FAILED;
   }
   
   adminHomePanel.Hide(); class=class="str">"cmt">// Hide home panel by class="kw">default on initialization
   
   class="kw">return INIT_SUCCEEDED;
}

「从终端历史里扒出饼图要的数据」

想在 MT5 面板里画饼图,第一步是把账户历史里的成交记录捞出来并分好类。下面这个函数干的就是这事:先给盈利、亏损以及外汇 / 股票 / 期货三类计数器清零,再调用 HistorySelect(0, TimeCurrent()) 把从开户到当下的所有历史成交拉进内存;若返回失败直接 Print 报错并 return,避免空数据把面板跑崩。 它用 HistoryDealsTotal 拿到成交总数,循环里靠 HistoryDealGetTicket(i) 取每笔唯一编号,再用 HistoryDealGetDouble(ticket, DEAL_PROFIT) 读利润:正数 wins++,负数 losses++。品种归类看两点——名称里没点号(.)的算外汇;带点号的再去读 SYMBOL_PATH 组名,命中 Stocks 进股票、命中 Futures 进期货。 跑完你大概会看到类似分布:外汇成交占 60%、股票 30%、期货 10%,整体胜率 70%(盈利笔数 / 总笔数)。外汇与贵金属杠杆高、滑点跳空频繁,这个胜率只是历史切片,不代表后续倾向。 代码里的 SymbolInfoInteger(symbol, SYMBOL_SELECT) 其实只判断品种是否被选中,并不参与分组逻辑,真要改分类规则直接动 SYMBOL_PATH 的字符串匹配就行。把它接进你的仪表板,每次 HistorySelect 刷新后重算,饼图就能跟着实盘历史实时变。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Data for Pie Chart                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void GetTradeData(class="type">int &wins, class="type">int &losses, class="type">int &forexTrades, class="type">int &stockTrades, class="type">int &futuresTrades) {
    wins = class="num">0;
    losses = class="num">0;
    forexTrades = class="num">0;
    stockTrades = class="num">0;
    futuresTrades = class="num">0;
    if (!HistorySelect(class="num">0, TimeCurrent())) {
        Print("Failed to select trade history.");
        class="kw">return;
    }
    class="type">int totalDeals = HistoryDealsTotal();
    for (class="type">int i = class="num">0; i < totalDeals; i++) {
        class="type">class="kw">ulong dealTicket = HistoryDealGetTicket(i);
        if (dealTicket > class="num">0) {
            class="type">class="kw">double profit = HistoryDealGetDouble(dealTicket, DEAL_PROFIT);
            if (profit > class="num">0) wins++;
            else if (profit < class="num">0) losses++;
            class="type">class="kw">string symbol = HistoryDealGetString(dealTicket, DEAL_SYMBOL);
            if (SymbolInfoInteger(symbol, SYMBOL_SELECT)) {
                if (StringFind(symbol, ".") == -class="num">1) forexTrades++;
                else {
                    class="type">class="kw">string groupName;
                    if (SymbolInfoString(symbol, SYMBOL_PATH, groupName)) {
                        if (StringFind(groupName, "Stocks") != -class="num">1) stockTrades++;
                        else if (StringFind(groupName, "Futures") != -class="num">1) futuresTrades++;
                    }
                }
            }
        }
    }
}

把饼图渲染拆成可单扇区调用的模块

在 MT5 的 Canvas 图表库里,CPieChart 的 DrawPie 是 protected,外部想按自己的数据节奏逐段画根本够不着。实际做法是从 CPieChart 派生 CCustomPieChart,只包一层 public 的 DrawPieSegment,把 fi3/fi4 起止角、idx 索引和颜色透传进去,等于把受保护方法暴露出来。 CAnalyticsChart 继承 CWnd 当容器,里面挂一个 CCustomPieChart 成员。CreatePieChart 负责在指定 x/y/width/height 起 BitmapLabel;SetPieChartData 把 values、labels、colors 灌进 SeriesSet 并开 ShowPercent;DrawPieChart 先算 total,再按每段占比推算角度跨度去调 DrawPieSegment。这样盈损占比、交易类型分布两个图就能各自实例化、各自渲染。 防越界是实测踩过的坑:原 CPieChart::DrawPie 访问 p[idx+1] 时若数组没扩好,EA 测试会直接报 array out of range。改法是进绘图前加 if(idx+1 >= ArraySize(p)) 就 Print 错误并提前 return,同时渲染前把 CPoint 数组Resize 到扇区数。改完内置 PieChart.mqh 再跑 EA,该错不再复现。 这种写法让分析面板后续加净值曲线图时,直接复用 CAnalyticsChart 结构、换一组数据源即可,不用动其他模块。外汇与贵金属 EA 回测中图形面板仅作辅助观察,市场高风险,图形信号不代表未来盈亏。

MQL5 / C++
class="macro">#include <Canvas\Charts\PieChart.mqh>
class="macro">#include <Canvas\Charts\ChartCanvas.mqh>

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Custom Pie Chart Class                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CCustomPieChart : class="kw">public CPieChart {
class="kw">public:
    class="type">void DrawPieSegment(class="type">class="kw">double fi3, class="type">class="kw">double fi4, class="type">int idx, CPoint &p[], const class="type">uint clr) {
        DrawPie(fi3, fi4, idx, p, clr); class=class="str">"cmt">// Expose class="kw">protected method
    }
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Analytics Chart Class                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CAnalyticsChart : class="kw">public CWnd {
class="kw">private:
    CCustomPieChart pieChart;  class=class="str">"cmt">// Declare pieChart as a member of this class
class="kw">public:
    class="type">bool CreatePieChart(class="type">class="kw">string label, class="type">int x, class="type">int y, class="type">int width, class="type">int height) {
        if (!pieChart.CreateBitmapLabel(label, x, y, width, height)) {
            Print("Error creating Pie Chart: ", label);
            class="kw">return false;
        }
        class="kw">return true;
    }
    class="type">void SetPieChartData(const class="type">class="kw">double &values[], const class="type">class="kw">string &labels[], const class="type">uint &colors[]) {
        pieChart.SeriesSet(values, labels, colors);
        pieChart.ShowPercent();
    }
    class="type">void DrawPieChart(const class="type">class="kw">double &values[], const class="type">uint &colors[], class="type">int x0, class="type">int y0, class="type">int radius) {
        class="type">class="kw">double total = class="num">0;
        class="type">int seriesCount = ArraySize(values);
        if (seriesCount == class="num">0) {

◍ 把交易统计画成两块饼图

在 MT5 面板里落地交易统计可视化,最直接的就是用两个饼图分别呈现「胜 / 败」与「品种分布」。下面这段逻辑先取数,再各自建图、塞进分析面板。 先调 GetTradeData 拿到 wins、losses 及 forexTrades、stockTrades、futuresTrades 四类计数;随后声明两个 CAnalyticsChart 局部对象 pieChart1、pieChart2,分别用 CreatePieChart 在坐标 (20,20) 与 (350,20) 开 300×300 的画布。 pieChart1 喂入 {wins, losses} 与绿红配色,圆心 (150,150)、半径 140 调 DrawPieChart;pieChart2 同理走品种分布。若 CreatePieChart 返回 false,Print 报错并 return,避免空指针崩面板。 饼图扇区角度由 values[i]/total*360.0 算得,从 currentAngle 累加;点坐标用 cos/sin 配合 M_PI/180.0 把角度转弧度。最后一点的 x/y 用 cos(0)、sin(0) 回到起始位,闭合图形。外汇与贵金属交易本身高风险,这类统计只反映历史样本,对后续胜率仅作概率参考。

MQL5 / C++
Print("No data for pie chart.");
class="kw">return;
}
for (class="type">int i = class="num">0; i < seriesCount; i++)
   total += values[i];
class="type">class="kw">double currentAngle = class="num">0.0;
class=class="str">"cmt">// Resize the points array
CPoint points[];
ArrayResize(points, seriesCount + class="num">1);
for (class="type">int i = class="num">0; i < seriesCount; i++) {
   class="type">class="kw">double segmentValue = values[i] / total * class="num">360.0;
   class="type">class="kw">double nextAngle = currentAngle + segmentValue;
   class=class="str">"cmt">// Define points for the pie slice
   points[i].x = x0 + (class="type">int)(radius * cos(currentAngle * M_PI / class="num">180.0));
   points[i].y = y0 - (class="type">int)(radius * sin(currentAngle * M_PI / class="num">180.0));
   pieChart.DrawPieSegment(currentAngle, nextAngle, i, points, colors[i]);
   currentAngle = nextAngle;
}
class=class="str">"cmt">// Define the last point to close the pie
points[seriesCount].x = x0 + (class="type">int)(radius * cos(class="num">0));  class=class="str">"cmt">// Back to starting point
points[seriesCount].y = y0 - (class="type">int)(radius * sin(class="num">0));
}
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create Analytics Panel                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CreateAnalyticsPanel() {
   class="type">int wins, losses, forexTrades, stockTrades, futuresTrades;
   GetTradeData(wins, losses, forexTrades, stockTrades, futuresTrades);
   class=class="str">"cmt">// Declare pieChart1 and pieChart2 as local variables
   CAnalyticsChart pieChart1;
   CAnalyticsChart pieChart2;
   class=class="str">"cmt">// Win vs Loss Pie Chart
   if (!pieChart1.CreatePieChart("Win vs. Loss Pie Chart", class="num">20, class="num">20, class="num">300, class="num">300)) {
      Print("Error creating Win/Loss Pie Chart");
      class="kw">return;
   }
   class="type">class="kw">double winLossValues[] = {wins, losses};
   class="type">class="kw">string winLossLabels[] = {"Wins", "Losses"};
   class="type">uint winLossColors[] = {clrGreen, clrRed};
   pieChart1.SetPieChartData(winLossValues, winLossLabels, winLossColors);
   pieChart1.DrawPieChart(winLossValues, winLossColors, class="num">150, class="num">150, class="num">140);
   class=class="str">"cmt">// Add pieChart1 to the analyticsPanel
   analyticsPanel.Add(pieChart1);
   class=class="str">"cmt">// Trade Type Pie Chart
   if (!pieChart2.CreatePieChart("Trade Type Distribution", class="num">350, class="num">20, class="num">300, class="num">300)) {
      Print("Error creating Trade Type Pie Chart");
让小布替你跑这套
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到盈亏比与交易类型分布的实时卡片,不用自己写PieChart。把重复劳动交给小布,你专注决策。

常见问题

基于交易历史记录绘制,每次面板刷新时重新统计盈利与亏损笔数比例,不依赖实时 tick。
在创建时指定独立的对话框 ID 与锚定坐标,并在父 EA 的窗体管理数组里单独登记,就不会互相遮挡。
小布盯盘品种页已内置盈亏比与品种分布可视化,无需自行部署MQL5面板,适合不想写代码的交易者。
多数情况是先于图表绑定就调用了绘制,确保在 OnInit 完成 Canvas 附加并且历史数据非空后再实例化饼图。
外汇和贵金属杠杆高、波动剧烈,分布图只反映过去概率,不代表后续倾向,请始终视其为高风险参考。