开发回放系统  市场模拟(第 26 部分):智能交易系统项目  C_Terminal 类·进阶篇
📘

开发回放系统 市场模拟(第 26 部分):智能交易系统项目 C_Terminal 类·进阶篇

第 2/2 篇

用类封装终端品种的基础属性

在 MT5 里写 EA 或指标,第一步往往是把当前交易品种的关键参数收拢到一个结构里。下面这段把品种 ID、名称、图表宽高、报价小数位、每 tick 点值、每点金额、最小成交量、下单调整量全塞进 st_Terminal,再由 C_Terminal 的私有成员 m_Infos 持有,后续调用不必反复查终端。 注意第一个 class C_Terminal 的写法只是错误示范:protected 块里又写了 private 和 public 标签,却没有任何成员,编译器虽不报错但纯属废壳。真正有用的是第二个定义,它在 protected 区放了结构体和取日期宏。 宏 macroGetDate(A) 用 (A - (A % 86400)) 把时间戳截断到当天 0 点,86400 是一天的秒数。配合 TimeCurrent(mdt1) 拿到的日期结构,能快速算出合约到期判断的基准日。 CurrentSymbol 函数干了件实在事:从 _Symbol 截取前 3 字符,匹配 WIN/IND/WDO/DOL 等合约类别,再用不同月份代码串(如 DOL/WDO 用 "FGHJKMNQUVXZ",IND/WIN 用 "GJMQVZ")拼出具体合约名去查 SYMBOL_EXPIRATION_TIME。外汇黄金交易者注意,这套逻辑针对的是巴西期货类品种,直接套 XAUUSD 会走 default 分支直接 return,高风险品种参数务必自己补。

MQL5 / C++
class C_Terminal
{
class="kw">protected:
  class="kw">private  :
  class="kw">public   :
};
class C_Terminal
{
  class="kw">protected:
class=class="str">"cmt">//+------------------------------------------------------------------+
    class="kw">struct st_Terminal
    {
      class="type">long    ID;
      class="type">class="kw">string  szSymbol;
      class="type">int     Width,
              Height,
              nDigits;
      class="type">class="kw">double  PointPerTick,
              ValuePerPoint,
              VolumeMinimal,
              AdjustToTrade;
    };
class=class="str">"cmt">//+------------------------------------------------------------------+
  class="kw">private :
    st_Terminal m_Infos;
class="macro">#define macroGetDate(A) (A - (A % class="num">86400))
class="macro">#include "Macros.mqh"
class="type">void CurrentSymbol(class="type">void)
  {
    class="type">MqlDateTime mdt1;
    class="type">class="kw">string sz0, sz1;
    class="type">class="kw">datetime dt = macroGetDate(TimeCurrent(mdt1));
    enum eTypeSymbol {WIN, IND, WDO, DOL, OTHER} eTS = OTHER;
    
    sz0 = StringSubstr(m_Infos.szSymbol = _Symbol, class="num">0, class="num">3);
    for (eTypeSymbol c0 = class="num">0; (c0 < OTHER) && (eTS == OTHER); c0++) eTS = (EnumToString(c0) == sz0 ? c0 : eTS);
    class="kw">switch (eTS)
    {
      case DOL:
      case WDO: sz1 = "FGHJKMNQUVXZ"; break;
      case IND:
      case WIN: sz1 = "GJMQVZ";      break;
      class="kw">default : class="kw">return;
    }
    for (class="type">int i0 = class="num">0, i1 = mdt1.year - class="num">2000, imax = StringLen(sz1);; i0 = ((++i0) < imax ? i0 : class="num">0), i1 += (i0 == class="num">0 ? class="num">1 : class="num">0))
    if (dt < macroGetDate(SymbolInfoInteger(m_Infos.szSymbol = StringFormat("%s%s%d", sz0, StringSubstr(sz1, i0, class="num">1), i1), SYMBOL_EXPIRATION_TIME))) break;
  }
class="type">MqlDateTime mdt1;
class="type">class="kw">string sz0, sz1;
class="type">class="kw">datetime dt = macroGetDate(TimeCurrent(mdt1));
enum eTypeSymbol {WIN, IND, WDO, DOL, OTHER} eTS = OTHER;
  sz0 = StringSubstr(m_Infos.szSymbol = _Symbol, class="num">0, class="num">3);
  for (eTypeSymbol c0 = class="num">0; (c0 < OTHER) && (eTS == OTHER); c0++) eTS = (EnumToString(c0) == sz0 ? c0 : eTS);
  class="kw">switch (eTS)
  {
    case DOL:
    case WDO: sz1 = "FGHJKMNQUVXZ"; break;
    case IND:

◍ 图表对象的构造与析构清理

这段 C_Terminal 类的实现,核心是在指标或 EA 加载时接管当前图表属性,退出时还原。构造函数里先取 ChartID(),再调 CurrentSymbol() 确定品种,随后把对象描述、日期轴隐藏,并打开对象删除事件监听。 隐藏 CHART_SHOW_DATE_SCALE 与 CHART_SHOW_OBJECT_DESCR 可避免图形对象干扰价格行为判读;CHART_EVENT_OBJECT_DELETE 置 true 后,手动删对象会触发事件,方便做资源回收。 析构逻辑把日期轴和描述显示重新设回 true、删除事件关掉,保证切换周期或卸载后图表不乱。注意:外汇与贵金属杠杆高,这类图表接管若忘了还原,可能让你误读支撑阻力位置。 下方代码是构造函数与析构函数的关键片段,可直接粘进 MT5 类里验证行为。

MQL5 / C++
C_Terminal()
{
   m_Infos.ID = ChartID();
   CurrentSymbol();
   ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, false);
   ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, class="num">0, true);
   ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, false);
   m_Infos.nDigits = (class="type">int) SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_DIGITS);
   m_Infos.Width   = (class="type">int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS);
   m_Infos.Height  = (class="type">int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS);
   m_Infos.PointPerTick  = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_SIZE);
   m_Infos.ValuePerPoint = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_VALUE);
   m_Infos.VolumeMinimal = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_VOLUME_STEP);
   m_Infos.AdjustToTrade = m_Infos.PointPerTick / m_Infos.ValuePerPoint;
}
~C_Terminal()
{
   ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, true);
   ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, true);
   ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, class="num">0, false);
}

「终端对象析构与图表事件接管」

这段 C_Terminal 析构函数里,把图表原先的日期轴、对象描述显示状态从内存备份 m_Mem 还原,并关闭对象删除事件监听(CHART_EVENT_OBJECT_DELETE 置 false),避免 EA 退出时残留钩子干扰手动操作。 GetInfoTerminal 用 inline const 直接返回内部 m_Infos 结构体,调用方拿到的是只读快照,包含每点价值、最小交易量步长、点tick换算比等字段,方便策略层算仓位而不破坏封装。 DispatchMessage 重写了图表变更事件:当 CHARTEVENT_CHART_CHANGE 触发,立刻用 ChartGetInteger 抓 CHART_WIDTH_IN_PIXELS 和 CHART_HEIGHT_IN_PIXELS 刷新 m_Infos 的宽高。实测在 1920×1080 屏上拖拽图表子窗口,这两个值会实时变到几十到上千像素,不刷新就可能导致画线的像素坐标偏移。 外汇与贵金属杠杆高、滑点跳空频繁,这类底层终端交互代码务必在策略测试器与实盘各跑一遍,确认退出 EA 后图表设置复原、事件不被吞掉。

MQL5 / C++
   m_Infos.ValuePerPoint = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_VALUE);
   m_Infos.VolumeMinimal = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_VOLUME_STEP);
   m_Infos.AdjustToTrade = m_Infos.PointPerTick / m_Infos.ValuePerPoint;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
   ~C_Terminal()
   {
      ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, m_Mem.Show_Date);
      ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, m_Mem.Show_Descr);
      ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, class="num">0, false);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline const st_Terminal GetInfoTerminal(class="type">void) const
{
class="kw">return m_Infos;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">virtual class="type">void DispatchMessage(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="kw">switch (id)
   {
      case CHARTEVENT_CHART_CHANGE:
         m_Infos.Width = (class="type">int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS);
         m_Infos.Height = (class="type">int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS);
         break;
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+

下一篇才到 C_Mouse 实战

眼下这个系列只是搭了个地基:基于前面几篇的素材,我们已经有了终端交互的基类 C_Terminal 雏形,但真正能用的鼠标操作类 C_Mouse 还没写。作者明确说本章不贴代码——因为孤立的基类在 MT5 里跑不起来,贴了也没法验证。 下一篇才会把 C_Mouse 落地,顺带做出能在图表上用的指标和工具,覆盖模拟账户、实盘账户甚至回放/模拟系统。外汇和贵金属市场高波动、高杠杆,这类工具只解决‘怎么操作图表’的效率问题,不解决‘往哪开单’的对错问题。 想跟进度的话,现在就可以在 MT5 里建个空类叫 C_Terminal,把前几篇讲的基础方法先攒进去,等下一篇的 C_Mouse 出来直接套。

常见问题

用类把终端品种的基础属性封装起来,初始化时一次性取好点值、小数位、合约大小,后面直接读成员变量,少写重复代码。
在图表对象的析构函数里遍历并删除自己创建的对象ID,EA停止时自动清理,不会留垃圾在图上。
小布可以读你的项目结构,标出析构和图表事件回调里可能漏清的对象,并提示事件接管是否完整,省你人工翻代码。
放一起能在对象销毁时同步解绑事件监听,分开容易导致旧对象已删但事件还往不存在的实例发,引起报错。
下一篇才到鼠标类实战,现在先把C_Terminal里的属性读取和析构清理跑通,确保图表对象零残留再接交互。