在MQL5中创建交易管理员面板(第八部分):分析面板·进阶篇
(2/3)· 手动算盈亏比和品种分布太慢?这篇用CDialog+PieChart直接自动化
很多管理员面板的用户还在导出成交记录用Excel手算盈亏比,资产类别分布更是靠记忆估。等算完,行情早就不在那个位置了。把统计交给实时面板,决策才不会滞后。
面板按钮的事件路由怎么写
在 MT5 面板 EA 里,所有图形控件的交互都靠 OnChartEvent 统一收口。sparam 传回的是被点对象的名字,用一串 else if 比对字符串就能把不同按钮分流到各自处理函数,比写一堆独立回调清爽得多。 下面这段是典型的图表事件分支:点击类事件 CHARTEVENT_OBJECT_CLICK 下,SendButton、ClearButton、ChangeFontButton 等各自进自己的 OnXxxClick;若对象名里带 QuickMessageButton,则用 StringFind 定位后截取下标数字,转成索引去发快捷消息。 注意 OnInit 里先调 ShowAuthenticationPrompt(),返回 false 就直接中断初始化——这意味着没过鉴权前,后续面板创建逻辑都不会跑。外汇与贵金属插件涉及账户与通信,这类未授权加载可能引发高风险暴露,实盘前务必在策略测试器里先断这条路径。 把这段抄进你的 EA,改 sparam 字符串就能接自己的控件;开 MT5 挂上图表,点几下按钮看 Print 出来的分支对不对,是最快的验证法。
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 图,面板会直接浮在图表上;外汇与贵金属杠杆高,面板误触下单可能造成快速亏损,建议保留默认隐藏。
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 刷新后重算,饼图就能跟着实盘历史实时变。
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 回测中图形面板仅作辅助观察,市场高风险,图形信号不代表未来盈亏。
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) 回到起始位,闭合图形。外汇与贵金属交易本身高风险,这类统计只反映历史样本,对后续胜率仅作概率参考。
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");