基于预定义的风险和风险/回报比建立互动式半自动拖放EA 交易·进阶篇
「面板控件的像素级布局与创建」
在 MT5 的自定义面板里,所有标签和按钮都靠 Create() 的绝对坐标定位。上面这段把 ask、bid、SL、TP 等只读标签从基准点 (m_baseX+120, m_baseY+160) 起逐行排开,行距固定 15~16 像素:bid 在 +160,sl 在 +176,tp 在 +191,最大允许手数在 +208,最大亏损 tick 在 +223,手数输入框在 +238(宽 35 高 15),风险回报标签一路排到 +270。 字体统一用 Verdana、字号 8,Description 全留空字符串,说明这些只是显示层、不挂 tooltip。下单类型切换、市价单、退出三个按钮则换宋体、字号同为 8,背景色 LightBlue,Y 坐标从 +314 到 +354 每 20 像素一个,市价单按钮写了「下达市价单」、退出写了「退出」作为可点击提示。 Refresh() 里先调 SymbolInfoTick(Symbol(), current_tick) 抓最新 tick,意味着面板数值会随行情刷新而非静态。你开 MT5 把这段塞进 CRRDialog 类编译,就能看到面板按这个像素网格落地;改 m_baseY 的初值即可整体上下挪动。外汇与贵金属波动剧烈,这类面板只解决下单交互,不替代风控。
m_askLabel.Font("Verdana"); m_askLabel.FontSize(class="num">8); m_askLabel.Description(""); m_bidLabel.Create(class="num">0, "bidLabel", class="num">0, m_baseX + class="num">120, m_baseY + class="num">160); m_bidLabel.Font("Verdana"); m_bidLabel.FontSize(class="num">8); m_bidLabel.Description(""); m_slLabel.Create(class="num">0, "slLabel", class="num">0, m_baseX + class="num">120, m_baseY + class="num">176); m_slLabel.Font("Verdana"); m_slLabel.FontSize(class="num">8); m_slLabel.Description(""); m_tpLabel.Create(class="num">0, "tpLabel", class="num">0, m_baseX + class="num">120, m_baseY + class="num">191); m_tpLabel.Font("Verdana"); m_tpLabel.FontSize(class="num">8); m_tpLabel.Description(""); m_maxAllowedLotsLabel.Create(class="num">0, "maxAllowedLotsLabel", class="num">0, m_baseX + class="num">120, m_baseY + class="num">208); m_maxAllowedLotsLabel.Font("Verdana"); m_maxAllowedLotsLabel.FontSize(class="num">8); m_maxAllowedLotsLabel.Description(""); m_maxTicksLossLabel.Create(class="num">0, "maxTicksLossLabel", class="num">0, m_baseX + class="num">120, m_baseY + class="num">223); m_maxTicksLossLabel.Font("Verdana"); m_maxTicksLossLabel.FontSize(class="num">8); m_maxTicksLossLabel.Description(""); m_orderLotsEdit.Create(class="num">0, "orderLotsEdit", class="num">0, m_baseX + class="num">120, m_baseY + class="num">238, class="num">35, class="num">15); m_orderLotsEdit.Font("Verdana"); m_orderLotsEdit.FontSize(class="num">8); m_orderLotsEdit.Description(""); m_orderEquityRiskLabel.Create(class="num">0, "orderEquityRiskLabel", class="num">0, m_baseX + class="num">120, m_baseY + class="num">255); m_orderEquityRiskLabel.Font("Verdana"); m_orderEquityRiskLabel.FontSize(class="num">8); m_orderEquityRiskLabel.Description(""); m_orderEquityRewardLabel.Create(class="num">0, "orderEquityRewardLabel", class="num">0, m_baseX + class="num">120, m_baseY + class="num">270); m_orderEquityRewardLabel.Font("Verdana"); m_orderEquityRewardLabel.FontSize(class="num">8); m_orderEquityRewardLabel.Description(""); m_switchOrderTypeButton.Create(class="num">0, "switchOrderTypeButton", class="num">0, m_baseX + class="num">20, m_baseY + class="num">314, class="num">160, class="num">20); m_switchOrderTypeButton.Font("宋体"); m_switchOrderTypeButton.FontSize(class="num">8); m_switchOrderTypeButton.BackColor(LightBlue); m_placeOrderButton.Create(class="num">0, "placeOrderButton", class="num">0, m_baseX + class="num">20, m_baseY + class="num">334, class="num">160, class="num">20); m_placeOrderButton.Font("宋体"); m_placeOrderButton.FontSize(class="num">8); m_placeOrderButton.BackColor(LightBlue); m_placeOrderButton.Description("下达市价单"); m_quitEAButton.Create(class="num">0, "quitEAButton", class="num">0, m_baseX + class="num">20, m_baseY + class="num">354, class="num">160, class="num">20); m_quitEAButton.Font("宋体"); m_quitEAButton.FontSize(class="num">8); m_quitEAButton.BackColor(LightBlue); m_quitEAButton.Description("退出"); class="kw">return isCreated; } class="type">void CRRDialog::Refresh() { class="type">MqlTick current_tick; SymbolInfoTick(Symbol(),current_tick);
◍ 面板标签如何用一行代码刷新
在 MT5 自定义面板里,所有动态数值都应集中在一次刷新函数里写回 Description,避免分散更新导致界面闪烁或逻辑漏写。下面这段把权益、风险、 tick 报价、止损止盈和最大手数一次性推到对应标签。
注意 AccountInfoDouble(ACCOUNT_EQUITY) 取的是浮动权益,乘以风险输入百分比得到最大可亏权益;而 Digits() 保证报价、SL、TP 的小数位与品种一致,否则黄金显示 2 位、欧美显示 5 位会错位。
买入/卖出状态用 m_switchOrderTypeButton.Description 直接切换中文文案,比维护两个隐藏控件更轻量。开 MT5 把这段塞进你的 CAppDialog::Refresh() 类方法,编译后拖面板上去就能看到实时数值。外汇与贵金属波动剧烈,面板数值仅作参考,实际下单前仍需人工核对高风险敞口。
m_equityLabel.Description(DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY),class="num">2)); m_maxEquityLossLabel.Description(DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY)*StringToDouble(m_riskValueEdit.Description())/class="num">100.0,class="num">2)); m_askLabel.Description(DoubleToString(current_tick.ask, Digits())); m_bidLabel.Description(DoubleToString(current_tick.bid, Digits())); m_slLabel.Description(DoubleToString(m_SL, Digits())); m_tpLabel.Description(DoubleToString(m_TP, Digits())); m_maxAllowedLotsLabel.Description(DoubleToString(m_maxAllowedLots,class="num">2)); m_maxTicksLossLabel.Description(DoubleToString(m_maxTicksLoss,class="num">0)); m_orderEquityRiskLabel.Description(DoubleToString(m_orderEquityRisk,class="num">2)); m_orderEquityRewardLabel.Description(DoubleToString(m_orderEquityReward,class="num">2)); if(m_orderType==ORDER_TYPE_BUY) m_switchOrderTypeButton.Description("订单类型: 买入"); else if(m_orderType==ORDER_TYPE_SELL) m_switchOrderTypeButton.Description("订单类型: 卖出"); }
用图表事件把EA变成能动手的面板
交互式 EA 不能只靠定时器扫盘,得接住交易员在图表上的动作。拖 S/L 箭头、点买卖切换、按市价单按钮、改风险与手数字段、退出 EA,这些都要映射到具体的图表事件上。 MQL5 里三类事件最常用:CHARTEVENT_OBJECT_CLICK 管指针选中与按钮,CHARTEVENT_OBJECT_DRAG 管拖 S/L 箭头后按 R/R 反推 T/P,CHARTEVENT_OBJECT_ENDEDIT 在编辑框失焦后更新参数。把 OnChartEvent 拆成多个 handler 后,主函数能从几页代码缩成可读的派发逻辑。 一个反直觉的技巧:图表对象默认要双击才选中,但在 OBJECT_CLICK 处理里直接调 Selected() 就能单击切换。下面这段是精简后的事件派发骨架,注意 SL_arrow.Selected(!SL_arrow.Selected()) 用自身状态取反,点一下选、再点取消。 外汇与贵金属杠杆高,这类拖拽下单逻辑若未校验点差与滑点,实盘可能以偏离预期的价格成交,务必在策略测试器先跑通再上真仓。
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) { class=class="str">"cmt">//--- 检查点击鼠标按钮事件 if(id==CHARTEVENT_OBJECT_CLICK) { class="type">class="kw">string clickedChartObject=sparam; if(clickedChartObject==slButtonID) SL_arrow.Selected(!SL_arrow.Selected()); if(clickedChartObject==switchOrderTypeButtonID) { EA_switchOrderType(); }; if(clickedChartObject==placeOrderButtonID) { EA_placeOrder(); } if(clickedChartObject==quitEAButtonID) ExpertRemove(); ChartRedraw(); } if(id==CHARTEVENT_OBJECT_DRAG) { class=class="str">"cmt">// 买进 if(visualRRDialog.GetOrderType()==ORDER_TYPE_BUY) { EA_dragBuyHandle(); }; class=class="str">"cmt">// 卖出 if(visualRRDialog.GetOrderType()==ORDER_TYPE_SELL) { EA_dragSellHandle(); }; ChartRedraw(); } if(id==CHARTEVENT_OBJECT_ENDEDIT) { if((sparam==riskRatioEditID || sparam==riskValueEditID || sparam==orderLotsEditID) && orderPlaced==false) { EA_editParamsUpdate(); } } } if(clickedChartObject==slButtonID) SL_arrow.Selected(!SL_arrow.Selected());
「把固定风险类改成可算最大止损的手数控制器」
做图表风险矩形之前,得先让资金管理类能回答一个问题:在给定净值回撤比例下,最小手数能扛多少个点止损。原生 CMoneyFixedRisk 只返回 broker 允许区间内的手数,我在 CheckOpenLong / CheckOpenShort 里加了道闸——不满足风险要求直接回 0.0,顺手扩出四个方法。 GetMaxSLPossible() 干的事很实:余额 10000、风险 2% 时最多亏 200 基础币;若最小手 0.1,它算出一个价格位,0.1 手挂单越过该位就破风控。CalcMaxTicksLoss() 把最大亏损除以最小手单 tick 亏损值再 MathFloor 成整数 tick 数,用来限制绘图矩形的拉伸。 CalcOrderEquityRisk() 按 (价格-止损)/TickSize × 手数 × TickValueLoss 实时算净值风险;CalcOrderEquityReward() 同理换 TickValueProfit 再乘 RR 比。这四个方法够你在图表上直接画出「能亏多少、回报多少」的框,不用切账户看报表。 外汇和贵金属杠杆高,TickValueLoss 随品种和点差浮动,实盘前务必在 MT5 用策略测试器跑一遍最小手数下的 maxTicksLoss 是否合理。
class CMoneyFixedRiskExt : class="kw">public CExpertMoney { class="kw">public: class=class="str">"cmt">//--- class="kw">virtual class="type">class="kw">double CheckOpenLong(class="type">class="kw">double price,class="type">class="kw">double sl); class="kw">virtual class="type">class="kw">double CheckOpenShort(class="type">class="kw">double price,class="type">class="kw">double sl); class="type">class="kw">double GetMaxSLPossible(class="type">class="kw">double price, ENUM_ORDER_TYPE orderType); class="type">class="kw">double CalcMaxTicksLoss(); class="type">class="kw">double CalcOrderEquityRisk(class="type">class="kw">double price, class="type">class="kw">double sl, class="type">class="kw">double lots); class="type">class="kw">double CalcOrderEquityReward(class="type">class="kw">double price, class="type">class="kw">double sl, class="type">class="kw">double lots, class="type">class="kw">double rrratio); }; class="type">class="kw">double CMoneyFixedRiskExt::GetMaxSLPossible(class="type">class="kw">double price, ENUM_ORDER_TYPE orderType) { class="type">class="kw">double maxEquityLoss, tickValLoss, maxTicksLoss; class="type">class="kw">double minvol=m_symbol.LotsMin(); class="type">class="kw">double orderTypeMultiplier; if(m_symbol==NULL) class="kw">return(class="num">0.0); class="kw">switch (orderType) { case ORDER_TYPE_SELL: orderTypeMultiplier = -class="num">1.0; break; case ORDER_TYPE_BUY: orderTypeMultiplier = class="num">1.0; break; class="kw">default: orderTypeMultiplier = class="num">0.0; } maxEquityLoss = m_account.Balance()*m_percent/class="num">100.0; class=class="str">"cmt">// 最大亏损 tickValLoss = minvol*m_symbol.TickValueLoss(); class=class="str">"cmt">// 订单亏损值 maxTicksLoss = MathFloor(maxEquityLoss/tickValLoss); class="kw">return (price - maxTicksLoss*m_symbol.TickSize()*orderTypeMultiplier); } class="type">class="kw">double CMoneyFixedRiskExt::CalcMaxTicksLoss() { class="type">class="kw">double maxEquityLoss, tickValLoss, maxTicksLoss; class="type">class="kw">double minvol=m_symbol.LotsMin(); if(m_symbol==NULL) class="kw">return(class="num">0.0); maxEquityLoss = m_account.Balance()*m_percent/class="num">100.0; class=class="str">"cmt">// 最大亏损 tickValLoss = minvol*m_symbol.TickValueLoss(); class=class="str">"cmt">// 订单亏损值 maxTicksLoss = MathFloor(maxEquityLoss/tickValLoss); class="kw">return (maxTicksLoss); } class="type">class="kw">double CMoneyFixedRiskExt::CalcOrderEquityRisk(class="type">class="kw">double price,class="type">class="kw">double sl, class="type">class="kw">double lots) { class="type">class="kw">double equityRisk; equityRisk = lots*m_symbol.TickValueLoss()*(MathAbs(price-sl)/m_symbol.TickSize()); if (dbg) Print("calcEquityRisk: lots = " + DoubleToString(lots) + " TickValueLoss = " + DoubleToString(m_symbol.TickValueLoss()) + " risk = " + DoubleToString(equityRisk)); class="kw">return equityRisk; } class="type">class="kw">double CMoneyFixedRiskExt::CalcOrderEquityReward(class="type">class="kw">double price,class="type">class="kw">double sl, class="type">class="kw">double lots, class="type">class="kw">double rrratio) {
◍ 用TickValueProfit算权益回报
在MT5里做风险回报测算,别只盯着点数,要把合约的 TickValueProfit 拉进来。下面这段计算把手数、止损距离和盈亏比揉成一个 equityReward 数值,直接反映这笔单子对账户权益的可能冲击。 double equityReward; equityReward = lots*m_symbol.TickValueProfit()*(MathAbs(price-sl)/m_symbol.TickSize())*rrratio; 第一行声明双精度变量存回报值;第二行用手数乘每跳盈利价值,再乘价格到止损的跳数(绝对值除 TickSize),最后乘 rrratio 盈亏比,得出理论权益回报。外汇和贵金属杠杆高,这个数值对净值波动只是概率性参考,实盘可能偏离。 if (dbg) Print("calcEquityReward: lots = " + DoubleToString(lots) + " TickValueProfit = " + DoubleToString(m_symbol.TickValueProfit()) + " reward = " + DoubleToString(equityReward)); return equityReward; 调试开关 dbg 打开时,把 lots、TickValueProfit、算出的 reward 打印到日志,方便你开 MT5 终端对照品种属性表核对。return 把结果交回调用处,接下去就能拿它和 calcEquityRisk 的比值判信号。
class="type">class="kw">double equityReward; equityReward = lots*m_symbol.TickValueProfit()*(MathAbs(price-sl)/m_symbol.TickSize())*rrratio; if (dbg) Print("calcEquityReward: lots = " + DoubleToString(lots) + " TickValueProfit = " + DoubleToString(m_symbol.TickValueProfit()) + " reward = " + DoubleToString(equityReward)); class="kw">return equityReward;
把拖拽箭头接进下单逻辑
在 MT5 图表上点 m_switchOrderTypeButton 会触发 EA_switchOrderType(),它在买卖类型间切换,同时把对话框里 SL、TP、手数、净值风险回报等变量全部清零,并删掉旧的风险/回报矩形。代码里用 visualRRDialog.ResetButtons() 和 rectRisk.Delete() 完成界面复位,SL_arrow 的锚点价格则按当前 Ask 或 Bid 重写,保证下次拖拽从合理位置起算。 拖放 SL_arrow 后 EA_dragBuyHandle() 接管:先从对象读时间和价格作为假设止损,再调用 MM.CheckOpenLong() 按净值风险反推可开手数。若拖出的止损过窄、连该品种最小手数都撑不住风险目标,函数会用 MM.GetMaxSLPossible() 取到的下限自动把 SL 推到最大允许位,避免无效下单。 卖出侧 EA_dragSellHandle() 逻辑对称,只是基准价换成 symbolInfo.Bid(),矩形绿色区画在价格下方表示利润空间。两种情况下 rectUpdate() 都会重绘浅粉(价到损)和淡绿(价到盈)矩形,直观看 R:R 随止损变动的拉伸。 EA_placeOrder() 在按 m_placeOrderButton 后发市价单,CExpertTrade 类让这步很轻量;EA_editParamsUpdate() 则在 riskRatioEdit 等框回车后重算手数与盈亏。EA_onTick() 只在未下单且已选 SL 时重算,下单后矩形不再重绘——这套交互在外汇和贵金属上均属于高风险操作,参数错配可能瞬间放大回撤。
class="type">void EA_switchOrderType() { symbolInfo.RefreshRates(); visualRRDialog.SwitchOrderType(); visualRRDialog.ResetButtons(); visualRRDialog.SetSL(class="num">0.0); visualRRDialog.SetTP(class="num">0.0); visualRRDialog.SetMaxAllowedLots(class="num">0.0); visualRRDialog.SetOrderLots(class="num">0.0); visualRRDialog.SetMaxTicksLoss(class="num">0); visualRRDialog.SetOrderEquityRisk(class="num">0.0); visualRRDialog.SetOrderEquityReward(class="num">0.0); if(visualRRDialog.GetOrderType()==ORDER_TYPE_BUY) SL_arrow.SetDouble(OBJPROP_PRICE,symbolInfo.Ask()); else if(visualRRDialog.GetOrderType()==ORDER_TYPE_SELL) SL_arrow.SetDouble(OBJPROP_PRICE,symbolInfo.Bid()); SL_arrow.SetInteger(OBJPROP_TIME,class="num">0,TimeCurrent()); rectReward.Delete(); rectRisk.Delete(); visualRRDialog.Refresh(); } class="type">void EA_dragBuyHandle() { SL_arrow.GetDouble(OBJPROP_PRICE,class="num">0,SL_price); SL_arrow.GetInteger(OBJPROP_TIME,class="num">0,startTime); symbolInfo.RefreshRates(); currentTime=TimeCurrent(); class=class="str">"cmt">// 买进 class="type">class="kw">double allowedLots=MM.CheckOpenLong(symbolInfo.Ask(),SL_price); Print("Allowed lots = "+DoubleToString(allowedLots,class="num">2)); class="type">class="kw">double lowestSLAllowed=MM.GetMaxSLPossible(symbolInfo.Ask(),ORDER_TYPE_BUY); if(SL_price<lowestSLAllowed) { SL_price=lowestSLAllowed; ObjectSetDouble(class="num">0,slButtonID,OBJPROP_PRICE,lowestSLAllowed); } visualRRDialog.SetSL(SL_price); visualRRDialog.SetTP(symbolInfo.Ask()+(symbolInfo.Ask()-SL_price)*visualRRDialog.GetRRRRatio()); if(visualRRDialog.GetTP()<SL_price) { visualRRDialog.SetSL(class="num">0.0); visualRRDialog.SetTP(class="num">0.0); SL_arrow.SetDouble(OBJPROP_PRICE,symbolInfo.Ask()); rectReward.Delete(); rectRisk.Delete(); class="kw">return; } class="type">class="kw">double lotSize=MM.CheckOpenLong(symbolInfo.Ask(),SL_price); visualRRDialog.SetMaxAllowedLots(lotSize); visualRRDialog.SetOrderLots(lotSize); visualRRDialog.SetMaxTicksLoss(MM.CalcMaxTicksLoss()); visualRRDialog.SetOrderEquityRisk(MM.CalcOrderEquityRisk(symbolInfo.Ask(), SL_price, lotSize)); visualRRDialog.SetOrderEquityReward(MM.CalcOrderEquityReward(symbolInfo.Ask(), SL_price, lotSize, visualRRDialog.GetRRRRatio())); visualRRDialog.Refresh(); rectUpdate(visualRRDialog.GetOrderType()); } class="type">void EA_dragSellHandle() { SL_arrow.GetDouble(OBJPROP_PRICE,class="num">0,SL_price); SL_arrow.GetInteger(OBJPROP_TIME,class="num">0,startTime); symbolInfo.RefreshRates(); currentTime=TimeCurrent(); class="type">class="kw">double allowedLots=MM.CheckOpenShort(symbolInfo.Bid(),SL_price); Print("Allowed lots = "+DoubleToString(allowedLots,class="num">2)); class="type">class="kw">double maxSLAllowed=MM.GetMaxSLPossible(symbolInfo.Bid(),ORDER_TYPE_SELL); if(SL_price>maxSLAllowed) {