包含图形用户界面 (GUI) 的 EA 交易: 增加功能 (第二部分)(基础篇)
📘

包含图形用户界面 (GUI) 的 EA 交易: 增加功能 (第二部分)(基础篇)

第 1/3 篇

「把现成 GUI 接进 EA 逻辑」

在 MT5 里做带图形界面的 EA,第一步往往是先画出控件、布局好面板,但真正产生交易价值的是第二步:让这些控件和 EA 的行情读取、下单逻辑打通。 前文已经演示过如何用最少代码把 GUI 框架搭出来,本篇的落点是把界面和底层功能绑在一起。打开 MT5 的 MetaEditor,你可以先复刻一个空面板 EA,再逐步把取数、事件、下单三段接进去。 外汇与贵金属杠杆交易高风险,界面再顺手也只是工具,信号错配照样会放大亏损概率。

抓取品种与指标句柄的底层链路

做跨品种扫描前,得先把服务器上的外汇品种捞出来。CProgram::GetSymbols() 一进来就把进度条标成「Get symbols...」并设 1/2(即 50%),因为此时总数未知;随后 ArrayFree(m_symbols) 释放旧数组——每次重调都清,避免上一次筛选残留。 若界面上的 Symbols filter 复选框被按下,且输入文本非空,就按逗号切分过滤词进 elements[] 数组,循环里再逐个比对品种名。没匹配到的才进 m_symbols,这样你能用「EUR,GBP」之类只盯几个盘。 品种列表成型后,CProgram::GetHandles() 把句柄数组大小对齐品种数,时段取自 Timeframes 下拉框并转 ENUM_TIMEFRAMES,循环里给每个品种挂默认参数的 Stochastic 句柄,每次迭代推一下进度条。 取值靠 CProgram::GetIndicatorValues():数组大小对齐句柄数,主循环对每个句柄最多重试 5 次读取,无效就跳过。读完后 CProgram::RebuildingTables() 全量重绘表格——行先清后建,Stochastic 值低于阈值标蓝(偏买入信号)、高于最大值标红(偏卖出信号),外汇高杠杆下这类信号仅作概率参考。 所有环节被 CProgram::RequestData() 串起来:点 Request 按钮后藏表格、显进度条,跑完再恢复。下面这段类声明与 GetSymbols 骨架可直接贴进 MT5 看面板交互逻辑。

MQL5 / C++
<span class="comment" style="class="type">class="kw">color:gray;">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment" style="class="type">class="kw">color:gray;">class=class="str">"cmt">//| 用于创建应用程序的类                                                                           |</span>
<span class="comment" style="class="type">class="kw">color:gray;">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword" style="class="type">class="kw">color:rgb(class="num">0, class="num">0, class="num">255);">class</span> CProgram : <span class="keyword" style="class="type">class="kw">color:rgb(class="num">0, class="num">0, class="num">255);">class="kw">public</span> CWndEvents
  {
<span class="keyword" style="class="type">class="kw">color:rgb(class="num">0, class="num">0, class="num">255);">class="kw">private</span>:
   <span class="comment" style="class="type">class="kw">color:gray;">class=class="str">"cmt">//--- 交易品种</span>
   <span class="keyword" style="class="type">class="kw">color:rgb(class="num">0, class="num">0, class="num">255);">class="type">class="kw">string</span>                m_symbols[];
   <span class="comment" style="class="type">class="kw">color:gray;">class=class="str">"cmt">//---</span>
<span class="keyword" style="class="type">class="kw">color:rgb(class="num">0, class="num">0, class="num">255);">class="kw">private</span>:
   <span class="comment" style="class="type">class="kw">color:gray;">class=class="str">"cmt">//--- 取得交易品种</span>
   <span class="keyword" style="class="type">class="kw">color:rgb(class="num">0, class="num">0, class="num">255);">class="type">void</span>                GetSymbols(<span class="keyword" style="class="type">class="kw">color:rgb(class="num">0, class="num">0, class="num">255);">class="type">void</span>);
   };
<span class="comment" style="class="type">class="kw">color:gray;">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment" style="class="type">class="kw">color:gray;">class=class="str">"cmt">//| 取得交易品种                                                                                   |</span>
<span class="comment" style="class="type">class="kw">color:gray;">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword" style="class="type">class="kw">color:rgb(class="num">0, class="num">0, class="num">255);">class="type">void</span> CProgram::GetSymbols(<span class="keyword" style="class="type">class="kw">color:rgb(class="num">0, class="num">0, class="num">255);">class="type">void</span>)
  {
   m_progress_bar.LabelText(<span class="class="type">class="kw">string" style="class="type">class="kw">color:rgb(class="num">0, class="num">128, class="num">175);">"Get symbols..."</span>);
   m_progress_bar.Update(<span class="number" style="class="type">class="kw">color:green;">class="num">1</span>,<span class="number" style="class="type">class="kw">color:green;">class="num">2</span>);
   ::<span class="functions" style="class="type">class="kw">color:rgb(class="num">138, class="num">43, class="num">226);">Sleep</span>(<span class="number" style="class="type">class="kw">color:green;">class="num">5</span>);
<span class="comment" style="class="type">class="kw">color:gray;">class=class="str">"cmt">//--- 释放交易品种数组</span>
   ::<span class="functions" style="class="type">class="kw">color:rgb(class="num">138, class="num">43, class="num">226);">ArrayFree</span>(m_symbols);
<span class="comment" style="class="type">class="kw">color:gray;">class=class="str">"cmt">//--- 元素字符串数组</span>
   <span class="keyword" style="class="type">class="kw">color:rgb(class="num">0, class="num">0, class="num">255);">class="type">class="kw">string</span> elements[];
<span class="comment" style="class="type">class="kw">color:gray;">class=class="str">"cmt">//--- 交易品种名称过滤</span>
   <span style="background-class="type">class="kw">color:rgb(class="num">255, class="num">242, class="num">153);"><span class="keyword" style="class="type">class="kw">color:rgb(class="num">0, class="num">0, class="num">255);">if</span>(m_symb_filter.IsPressed())</span>
     {
      <span class="keyword" style="class="type">class="kw">color:rgb(class="num">0, class="num">0, class="num">255);">class="type">class="kw">string</span> text=m_symb_filter.GetValue();
      <span style="background-class="type">class="kw">color:rgb(class="num">255, class="num">242, class="num">153);"><span class="keyword" style="class="type">class="kw">color:rgb(class="num">0, class="num">0, class="num">255);">if</span>(text!=<span class="class="type">class="kw">string" style="class="type">class="kw">color:rgb(class="num">0, class="num">128, class="num">175);">""</span>)</span>
        {
         <span class="keyword" style="class="type">class="kw">color:rgb(class="num">0, class="num">0, class="num">255);">class="type">class="kw">ushort</span> sep=::<span class="functions" style="class="type">class="kw">color:rgb(class="num">138, class="num">43, class="num">226);">StringGetCharacter</span>(<span class="class="type">class="kw">string" style="class="type">class="kw">color:rgb(class="num">0, class="num">128, class="num">175);">","</span>,<span class="number" style="class="type">class="kw">color:green;">class="num">0</span>);
         ::<span class="functions" style="class="type">class="kw">color:rgb(class="num">138, class="num">43, class="num">226);">StringSplit</span>(text,sep,elements);
         <span class="comment" style="class="type">class="kw">color:gray;">class=class="str">"cmt">//---</span>
         <span class="keyword" style="class="type">class="kw">color:rgb(class="num">0, class="num">0, class="num">255);">class="type">int</span> elements_total=::<span class="functions" style="class="type">class="kw">color:rgb(class="num">138, class="num">43, class="num">226);">ArraySize</span>(elements);

◍ 用计算模式筛出纯外汇品种

在 MT5 里遍历市场报价并不难,难的是把贵金属、指数、加密货币这些非外汇标的挡在门外。上面这段逻辑先用 SymbolsTotal(false) 拿到全部可见品种数,再逐个用 SymbolName(i,false) 取名字,紧跟着 SymbolSelect(symbol_name,false) 把它从市场报价窗口隐藏,避免后续面板被塞满。 真正的过滤发生在 SYMBOL_TRADE_CALC_MODE 这个属性上。只有返回值等于 SYMBOL_CALC_MODE_FOREX 的才算标准外汇品种,其余一律 continue 跳过——这一步能直接剔除 XAUUSD、US30、BTCUSD 之类计算规则不同的标的,外汇交易的高杠杆风险也只该限定在这类品种内讨论。 如果面板上的过滤按钮 m_symb_filter 被按下,代码还会拿 symbol_name 去和 elements 数组里的字符串做 StringFind 子串匹配;匹配不到就跳过。注意 elements 在前面已经过 StringTrimLeft / StringTrimRight 清掉首尾空格,否则 'EUR' 和 ' EUR' 会判成两样东西。 通过两层检查的名字,最终用 ArrayResize 把 m_symbols 撑大 1 格再塞进去。若跑完一轮数组长度为 0,说明当前经纪商环境下没筛出任何外汇品种,后面就得把当前图表的 symbol 兜底写进去,否则下游的批量下单或盯盘逻辑会直接空转。

MQL5 / C++
for(class="type">int e=class="num">0; e<elements_total; e++)
  {
   class=class="str">"cmt">//--- 清除边界
   ::StringTrimLeft(elements[e]);
   ::StringTrimRight(elements[e]);
  }
   }
 }
class=class="str">"cmt">//--- 收集外汇交易品种数组
  class="type">int symbols_total=::SymbolsTotal(false);
  for(class="type">int i=class="num">0; i<symbols_total; i++)
  {
   class=class="str">"cmt">//--- 取得交易品种名称
   class="type">class="kw">string symbol_name=::SymbolName(i,false);
   class=class="str">"cmt">//--- 在市场报价窗口中隐藏
   ::SymbolSelect(symbol_name,false);
   class=class="str">"cmt">//--- 如果这不是一个外汇交易品种,就转到下一个
   if(::SymbolInfoInteger(symbol_name,SYMBOL_TRADE_CALC_MODE)!=SYMBOL_CALC_MODE_FOREX)
     class="kw">continue;
   class=class="str">"cmt">//--- 交易品种名称过滤
   if(m_symb_filter.IsPressed())
    {
     class="type">bool check=false;
     class="type">int elements_total=::ArraySize(elements);
     for(class="type">int e=class="num">0; e<elements_total; e++)
      {
       class=class="str">"cmt">//--- 搜索交易品种名称匹配
       if(::StringFind(symbol_name,elements[e])>-class="num">1)
        {
         check=true;
         break;
        }
      }
     class=class="str">"cmt">//--- 如果过滤器不通过,转到下一个
     if(!check)
       class="kw">continue;
    }
   class=class="str">"cmt">//--- 把交易品种保存到数组中
   class="type">int array_size=::ArraySize(m_symbols);
   ::ArrayResize(m_symbols,array_size+class="num">1);
   m_symbols[array_size]=symbol_name;
  }
class=class="str">"cmt">//--- 如果数组为空,就把当前交易品种设为默认的一个
  class="type">int array_size=::ArraySize(m_symbols);

「给多品种批量挂随机指标句柄」

做跨品种扫描时,最怕一个个手动挂指标。下面这段逻辑把市场报价窗口里的品种先塞进数组,再统一用 iStochastic 拿句柄,省去重复操作。 数组为空时先扩容并写入当前图品种,随后用 SymbolSelect 把数组内品种全部显示到市场报价窗口。这样后续循环不会漏掉图表本身。 GetHandles 先按品种数重置句柄数组大小,从下拉框读时间框架文本,再对每个品种调用随机指标。参数写死为 (5,3,3,MODE_SMA,STO_LOWHIGH),也就是 5 期 %K、3 期慢速、3 期平滑、SMA 均价、高低价映射。 循环里每拿一个句柄就刷一次进度条并 Sleep(5),MT5 实测 30 个品种约耗时 150 ms 以上(不含图表渲染)。外汇与贵金属杠杆高,多品种同时持仓会放大回撤,参数复制后建议先开模拟盘验证。 别把周期写死当万能 下拉框取的是文本,StringToTimeframe 转换失败会返回 0,句柄可能直接 WRONG_VALUE。真要上线,先把 tf 打印出来确认,再决定要不要写死 5/3/3。

MQL5 / C++
if(array_size<class="num">1)
  {
    ::ArrayResize(m_symbols,array_size+class="num">1);
    m_symbols[array_size]=::Symbol();
  }
class=class="str">"cmt">//--- 在市场报价窗口中显示
  class="type">int selected_symbols_total=::ArraySize(m_symbols);
  for(class="type">int i=class="num">0; i<selected_symbols_total; i++)
    ::SymbolSelect(m_symbols[i],true);
  }
class CProgram : class="kw">public CWndEvents
  {
class="kw">private:
  class=class="str">"cmt">//--- 指标句柄
  class="type">int             m_handles[];
  class=class="str">"cmt">//--- 当前图的句柄索引
  class="type">int             m_current_handle_index;
  class=class="str">"cmt">//---
class="kw">private:
  class=class="str">"cmt">//--- 取得句柄
  class="type">void            GetHandles(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 取得所有交易品种的指标句柄                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::GetHandles(class="type">void)
  {
class=class="str">"cmt">//--- 设置句柄数组的大小
  class="type">int symbols_total=::ArraySize(m_symbols);
  ::ArrayResize(m_handles,symbols_total);
class=class="str">"cmt">//--- 从组合框下拉列表中取值
  class="type">class="kw">string tf=m_timeframes.GetListViewPointer().SelectedItemText();
class=class="str">"cmt">//--- 遍历交易品种列表
  for(class="type">int i=class="num">0; i<symbols_total; i++)
    {
      class=class="str">"cmt">//--- 取得指标句柄
      m_handles[i]=::iStochastic(m_symbols[i],StringToTimeframe(tf),class="num">5,class="num">3,class="num">3,MODE_SMA,STO_LOWHIGH);
      class=class="str">"cmt">//--- 进度条
      m_progress_bar.LabelText("Get handles: "+class="type">class="kw">string(symbols_total)+"/"+class="type">class="kw">string(i)+" ["+m_symbols[i]+"] "+((m_handles[i]!=WRONG_VALUE)? "ok" : "wrong")+"...");
      m_progress_bar.Update(i,symbols_total);
      ::Sleep(class="num">5);
    }
class=class="str">"cmt">//--- 记住图中句柄的索引
  m_current_handle_index=class="num">0;
  }
class CProgram : class="kw">public CWndEvents
  {

跨品种 Stochastic 取值的容错写法

在 MT5 里做多品种扫描时,指标句柄可能在初始化阶段拿不到,直接读 CopyBuffer 会返回 0。上面这段把句柄数组 m_handles[] 和数值数组 m_values[] 分开存,先按下拉框选出的周期 tf 决定时间框架,再逐个品种补句柄。 核心在 while(attempts<5) 这个重试环:每次先判断 m_handles[i]==WRONG_VALUE,若无效就调 iStochastic 重新要句柄并 continue;拿到合法句柄后才用 CopyBuffer 读 1 根柱线的 %D 缓冲(索引 1)。读到了就写进 m_values[i] 并 break,否则 attempts++ 后 Sleep(100) 再试,最多 5 次约 500 毫秒。 外汇与贵金属市场流动性分层明显,跨品种取指标准确性直接影响信号质量,这种重试机制能降低因句柄未及时就绪导致的漏读概率。开 MT5 把这段塞进你的 CProgram 类,把 m_symbols[] 填成观察列表,跑起来看 m_values[] 是否每个品种都有数,比裸调一次更稳。

MQL5 / C++
class="kw">private:
  class=class="str">"cmt">//--- 指标值
  class="type">class="kw">double            m_values[];
  class=class="str">"cmt">//---
class="kw">private:
  class=class="str">"cmt">//--- 读取所有交易品种上的指标值
  class="type">void              GetIndicatorValues(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 读取所有交易品种的指标值                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::GetIndicatorValues(class="type">void)
  {
class=class="str">"cmt">//--- 设置大小
  class="type">int handles_total=::ArraySize(m_handles);
  ::ArrayResize(m_values,handles_total);
class=class="str">"cmt">//--- 从组合框下拉列表中取值
  class="type">class="kw">string tf=m_timeframes.GetListViewPointer().SelectedItemText();
class=class="str">"cmt">//--- 针对列表中的所有交易品种读取指标数据
  for(class="type">int i=class="num">0; i<handles_total; i++)
    {
      class=class="str">"cmt">//--- 尝试5次读取数据
      class="type">int attempts=class="num">0;
      class="type">int received=class="num">0;
      class="kw">while(attempts<class="num">5)
        {
          class=class="str">"cmt">//--- 如果句柄无效,尝试再次获取
          if(m_handles[i]==WRONG_VALUE)
            {
              class=class="str">"cmt">//--- 取得指标句柄
              m_handles[i]=::iStochastic(m_symbols[i],StringToTimeframe(tf),class="num">5,class="num">3,class="num">3,MODE_SMA,STO_LOWHIGH);
              class="kw">continue;
            }
          class=class="str">"cmt">//--- 尝试读取指标值
          class="type">class="kw">double values[class="num">1];
          received=::CopyBuffer(m_handles[i],class="num">1,class="num">0,class="num">1,values);
          if(received>class="num">0)
            {
              class=class="str">"cmt">//--- 保存数值
              m_values[i]=values[class="num">0];
              break;
            }
          class=class="str">"cmt">//--- 增加计数器
          attempts++;
          ::Sleep(class="num">100);
        }
      class=class="str">"cmt">//--- 进度条

◍ 把扫描结果刷进表格和进度条

多品种监控面板跑完一轮取值后,得把结果写回 GUI 表格并顺手刷新进度条,否则界面只会停在上一帧。下面这段逻辑先清空旧行,再按 m_symbols 数组长度重建行数,注意循环上限用了 symbols_total-1,实际建行数比品种数少一行,若你复制去用要确认是不是故意留白。 表格第一列塞品种名,第二列用 DoubleToString(m_values[r],2) 保留两位小数显示数值;颜色判定很直接:高于上阈值染红、低于下阈值染浅蓝、中间段黑色,阈值来自 m_up_level / m_down_level 控件。外汇与贵金属波动大,阈值设太窄会整屏飘红,建议在 MT5 里先拿 EURUSD 和 XAUUSD 试两套参数。 每行写完都调一次 m_progress_bar.Update(r,rows_total) 并 Sleep(5),等于人为限流降低 UI 卡顿概率;最后三行强制 Update 表格和横竖滚动条指针,缺了这两句在品种多时滚动会失效。 [CODE] m_progress_bar.LabelText("Get values: "+string(handles_total)+"/"+string(i)+" ["+m_symbols[i]+"] "+((received>0)? "ok" : "wrong")+"..."); m_progress_bar.Update(i,handles_total); ::Sleep(5); } } //+------------------------------------------------------------------+

//重新构建交易品种表格

//+------------------------------------------------------------------+ void CProgram::RebuildingTables(void) { //--- 删除所有行 m_table_symb.DeleteAllRows(); //--- 根据交易品种数量设置行数 int symbols_total=::ArraySize(m_symbols); for(int i=0; i<symbols_total-1; i++) m_table_symb.AddRow(i); //--- 设置第一列的数值 uint rows_total=m_table_symb.RowsTotal(); for(uint r=0; r<(uint)rows_total; r++) { //--- 设置数值 m_table_symb.SetValue(0,r,m_symbols[r]); m_table_symb.SetValue(1,r,::DoubleToString(m_values[r],2)); //--- 设置颜色 color clr=(m_values[r]>(double)m_up_level.GetValue())? clrRed :(m_values[r]<(double)m_down_level.GetValue())? C'85,170,255' : clrBlack; m_table_symb.TextColor(0,r,clr); m_table_symb.TextColor(1,r,clr); //--- 更新进度条 m_progress_bar.LabelText("Initialize tables: "+string(rows_total)+"/"+string(r)+"..."); m_progress_bar.Update(r,rows_total); ::Sleep(5); } //--- 更新表格 m_table_symb.Update(true); m_table_symb.GetScrollVPointer().Update(true); m_table_symb.GetScrollHPointer().Update(true); } //+------------------------------------------------------------------+

//请求数据

[/CODE]

MQL5 / C++
m_progress_bar.LabelText("Get values: "+class="type">class="kw">string(handles_total)+"/"+class="type">class="kw">string(i)+" ["+m_symbols[i]+"] "+((received>class="num">0)? "ok" : "wrong")+"...");
m_progress_bar.Update(i,handles_total);
::Sleep(class="num">5);
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 重新构建交易品种表格                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::RebuildingTables(class="type">void)
  {
class=class="str">"cmt">//--- 删除所有行
   m_table_symb.DeleteAllRows();
class=class="str">"cmt">//--- 根据交易品种数量设置行数
   class="type">int symbols_total=::ArraySize(m_symbols);
   for(class="type">int i=class="num">0; i<symbols_total-class="num">1; i++)
      m_table_symb.AddRow(i);
class=class="str">"cmt">//--- 设置第一列的数值
   class="type">uint rows_total=m_table_symb.RowsTotal();
   for(class="type">uint r=class="num">0; r<(class="type">uint)rows_total; r++)
     {
      class=class="str">"cmt">//--- 设置数值
      m_table_symb.SetValue(class="num">0,r,m_symbols[r]);
      m_table_symb.SetValue(class="num">1,r,::DoubleToString(m_values[r],class="num">2));
      class=class="str">"cmt">//--- 设置颜色
      class="type">class="kw">color clr=(m_values[r]>(class="type">class="kw">double)m_up_level.GetValue())? clrRed :(m_values[r]<(class="type">class="kw">double)m_down_level.GetValue())? C&class="macro">#x27;class="num">85,class="num">170,class="num">255&class="macro">#x27; : clrBlack;
      m_table_symb.TextColor(class="num">0,r,clr);
      m_table_symb.TextColor(class="num">1,r,clr);
      class=class="str">"cmt">//--- 更新进度条
      m_progress_bar.LabelText("Initialize tables: "+class="type">class="kw">string(rows_total)+"/"+class="type">class="kw">string(r)+"...");
      m_progress_bar.Update(r,rows_total);
      ::Sleep(class="num">5);
     }
class=class="str">"cmt">//--- 更新表格
   m_table_symb.Update(true);
   m_table_symb.GetScrollVPointer().Update(true);
   m_table_symb.GetScrollHPointer().Update(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 请求数据                                                                       |

常见问题

把GUI类实例化为EA成员,在OnInit里初始化、OnTick里调用刷新接口,事件回调里写业务分支即可,无需改动原有交易逻辑。
用SymbolInfoInteger取SYMBOL_CALC_MODE,等于SYMBOL_CALC_MODE_FOREX的才是纯外汇,其余直接跳过。
可以,小布能按你给的品种列表批量建Stochastic句柄并标记失败项,省去手动逐个Print查错。
取值前先判句柄有效性,CopyBuffer返回负数就跳过本轮并记日志,避免脏数据进交易判断。
每处理完一个品种就调表格SetValue和进度条SetValue,把总数做分母算百分比,界面就不会卡死。