基于预定义的风险和风险/回报比建立互动式半自动拖放EA 交易·进阶篇
📘

基于预定义的风险和风险/回报比建立互动式半自动拖放EA 交易·进阶篇

第 2/3 篇

「面板控件的像素级布局与创建」

在 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 的初值即可整体上下挪动。外汇与贵金属波动剧烈,这类面板只解决下单交互,不替代风控。

MQL5 / C++
  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() 类方法,编译后拖面板上去就能看到实时数值。外汇与贵金属波动剧烈,面板数值仅作参考,实际下单前仍需人工核对高风险敞口。

MQL5 / C++
  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()) 用自身状态取反,点一下选、再点取消。 外汇与贵金属杠杆高,这类拖拽下单逻辑若未校验点差与滑点,实盘可能以偏离预期的价格成交,务必在策略测试器先跑通再上真仓。

MQL5 / C++
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 是否合理。

MQL5 / C++
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 的比值判信号。

MQL5 / C++
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 时重算,下单后矩形不再重绘——这套交互在外汇和贵金属上均属于高风险操作,参数错配可能瞬间放大回撤。

MQL5 / C++
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)
    {

常见问题

用相对坐标加固定间距循环创建控件,先算画布起点再逐个偏移,避免写死绝对像素。布局前打印各控件宽高调试一次最稳妥。
用对象属性修改函数改OBJPROP_TEXT即可原地更新文字,不必删除重建。配合注释事件每 tick 轻量刷新就行。
小布盯盘的 AIGC 已内置常见品种的风险诊断与信号提示,打开对应品种页即可看到,把画线和参数核算交给它,你专注拖拽决策。
把止损距离作为输入参数参与手数公式,用账户权益和单笔风险百分比反推,别写死点数。每次拖完箭头重算一次即可。
取当前品种 TickValueProfit 乘手数再乘预期 tick 数得名义回报,反向除风险金额得手数。注意跨品种点值单位,贵金属和外汇高杠杆下差异明显。