面向对象法建立多时间表及多货币面板·进阶篇
🧩

面向对象法建立多时间表及多货币面板·进阶篇

(2/3)· 还在为每个品种每周期手写面板逻辑?用 OOP 基类把价格、RSI 与自定义条件塞进同一张表

进阶 第 2/3 篇
不少交易者把多周期多品种监控写成一堆重复 iCustom 和硬编码数组,改一个显示项就要动整块 EA。这类写法在品种一多、周期一换时就崩得快,还难排查哪根线算错了。

表格对象的清理与重绘逻辑

MT5 里做多品种或多周期盯盘面板时,旧对象不清理会越堆越多。下面这段从后往前扫图表对象,只要名字里带基准串就删,避免重复画线。 for(int i=total-1; i>=0; i--) if(StringFind(ObjectName(0,i),nameBase)!=-1) ObjectDelete(0,ObjectName(0,i)); //--- 删除列表行并释放内存 delete(rowList); 上面循环用 total-1 到 0 的倒序,是因为删对象时索引会前移,正序删容易漏。nameBase 是你给面板对象起的前缀,比如 "PB_",这样不会误删手动画的线。 重绘函数 Update() 先算行数 rows 和列数 columns:tfMode 为真时列数取 timeframes 数组长度,否则取 symbols 数组长度。接着分三步画——第一列画行名、第一行画周期或品种名、内部单元按模式取对应 symbol/tf 调 row.GetValue 出值。 最后一句 ChartRedraw() 是强制重绘,没它界面可能卡在上一帧。外汇和贵金属波动快,这类面板只辅助看盘,信号出错概率不低,真金白银下单前自己核对源数据。

MQL5 / C++
for(class="type">int i=total-class="num">1; i>=class="num">0; i--)
  if(StringFind(ObjectName(class="num">0,i),nameBase)!=-class="num">1)
    ObjectDelete(class="num">0,ObjectName(class="num">0,i));
class=class="str">"cmt">//--- 删除列表行并释放内存
class="kw">delete(rowList);
class=class="str">"cmt">//+------------------------------------------------------------------
class=class="str">"cmt">//| 在表格末尾插入新行                                      |
class=class="str">"cmt">//+------------------------------------------------------------------
CTable::AddRow(CRow *row)
  {
  rowList.Add(row);
  row.Init(symbols,timeframes);
  }
class=class="str">"cmt">//+------------------------------------------------------------------
class=class="str">"cmt">//| 重画表格                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------
CTable::Update()
  {
  CRow *row;
  class="type">class="kw">string symbol;
  ENUM_TIMEFRAMES tf;
  class="type">int rows=rowList.Total(); class=class="str">"cmt">// 行数量
  class="type">int columns;              class=class="str">"cmt">// 列数量
  if(tfMode)
    columns=ArraySize(timeframes);
  else
    columns=ArraySize(symbols);
class=class="str">"cmt">//--- 画第一列 (行名称)
  for(class="type">int y=class="num">0; y<rows; y++)
    {
    row=(CRow*)rowList.GetNodeAtIndex(y);
    class=class="str">"cmt">//--- 注: 从行对象返回它自己的名称
    DrawLabel(columns,y+class="num">1,row.GetName(),font,fontColor);
    }
class=class="str">"cmt">//--- 画第一行 (时间帧名或货币对名)
  for(class="type">int x=class="num">0; x<columns; x++)
    {
    if(tfMode)
      DrawLabel(columns-x-class="num">1,class="num">0,PeriodToString(timeframes[x]),font,fontColor);
    else
      DrawLabel(columns-x-class="num">1,class="num">0,symbols[x],font,fontColor);
    }
class=class="str">"cmt">//--- 画内部表格单元
  for(class="type">int y=class="num">0; y<rows; y++)
    for(class="type">int x=class="num">0; x<columns; x++)
      {
      row=(CRow*)rowList.GetNodeAtIndex(y);
      if(tfMode)
        {
        class=class="str">"cmt">//--- 在多时间帧模式使用当前交易品种及不同的时间帧
        tf=timeframes[x];
        symbol=_Symbol;
        }
      else
        {
        class=class="str">"cmt">//--- 在多货币模式使用当前时间帧及不同的交易品种
        tf=Period();
        symbol=symbols[x];
        }
      class=class="str">"cmt">//--- 注: 从行对象返回它自己的字体, 
      class=class="str">"cmt">//--- 颜色和给定时间帧和交易品种的当前计算值
      DrawLabel(columns-x-class="num">1,y+class="num">1,row.GetValue(symbol,tf),row.GetFont(symbol,tf),row.GetColor(symbol,tf));
      }
class=class="str">"cmt">//--- 强制重绘图表
  ChartRedraw();
  }

「表格单元格里塞文本标签的写法」

想在 MT5 图表上用 CTable 类把文字摆进指定格子里,核心就是 DrawLabel 这个成员函数。它接收行列坐标 x、y 和文本、字体、颜色,先在 nameBase 后面拼上「x:y」生成唯一对象名,避免不同单元格对象撞名。 若用 ObjectFind 在当前图表(图表ID 0)找不到该名,就 ObjectCreate 一个 OBJ_LABEL 标签对象;标签锚点固定在 CORNER_RIGHT_UPPER(右上角),再用 XDISTANCE / YDISTANCE 按 x*cellWidth、y*cellHeight 偏移量算像素位置。 下面这段是可直接塞进 EA 或指标源文件的方法体。外汇与贵金属图表叠加自定义文字属高频脚本操作,对象过多可能拖慢终端响应,实盘前建议在策略测试器里跑一遍看资源占用。

MQL5 / C++
CTable::DrawLabel(class="type">int x,class="type">int y,class="type">class="kw">string text,class="type">class="kw">string font,class="type">color col)
  {
class=class="str">"cmt">//--- 创建唯一单元名
   class="type">class="kw">string name=nameBase+IntegerToString(x)+":"+IntegerToString(y);
class=class="str">"cmt">//--- 创建标签
   if(ObjectFind(class="num">0,name)<class="num">0)
      ObjectCreate(class="num">0,name,OBJ_LABEL,class="num">0,class="num">0,class="num">0);
class=class="str">"cmt">//--- 设置标签属性
   ObjectSetInteger(class="num">0,name,OBJPROP_CORNER,CORNER_RIGHT_UPPER);
   ObjectSetInteger(class="num">0,name,OBJPROP_ANCHOR,ANCHOR_RIGHT_UPPER);
   ObjectSetInteger(class="num">0,name,OBJPROP_XDISTANCE,xDistance+x*cellWidth);
   ObjectSetInteger(class="num">0,name,OBJPROP_YDISTANCE,yDistance+y*cellHeight);
   ObjectSetString(class="num">0,name,OBJPROP_FONT,font);
   ObjectSetInteger(class="num">0,name,OBJPROP_COLOR,col);
   ObjectSetInteger(class="num">0,name,OBJPROP_FONTSIZE,fontSize);
class=class="str">"cmt">//--- 设置标签文本
   ObjectSetString(class="num">0,name,OBJPROP_TEXT,text);
  }

◍ 从 CRow 派生你自己的面板行

CRow 是盯盘面板里所有自定义行的基类,它继承自 CObject——因为只有 CObject 才能塞进 CList 容器里管理。基类预置了 5 个虚方法(Init / GetValue / GetColor / GetName / GetFont),大多只回默认值,专门留给你在子类里按需覆写,不必全接。 最简单的起步是 CPriceRow:只重写 GetName 和 GetValue 两个方法,就能在多货币面板第一栏显示工具名、在对应单元格抓最新买价。GetValue 内部用 MqlTick 结构取 tick,返回 bid,逻辑短到可以直接抄。 想显示 RSI 值也不麻烦。CPriceRow 的兄弟类在构造时收一个 period 存成成员变量,Init 里建指标句柄塞进 handles[] 数组,GetValue 从 RSI 缓冲区拷最新值返回;多出来的 GetHandle 只是在数组里查对应品种的周期句柄。

MQL5 / C++
class CRow : class="kw">public CObject
  {
class="kw">public:
   class="kw">virtual class="type">void Init(class="type">class="kw">string &symb[],ENUM_TIMEFRAMES &tfs[]) { }
   class="kw">virtual class="type">class="kw">string GetValue(class="type">class="kw">string symbol,ENUM_TIMEFRAMES tf) { class="kw">return("-"); }
   class="kw">virtual class="type">color GetColor(class="type">class="kw">string symbol,ENUM_TIMEFRAMES tf) { class="kw">return(clrWhite); }
   class="kw">virtual class="type">class="kw">string GetName() { class="kw">return("-"); }
   class="kw">virtual class="type">class="kw">string GetFont(class="type">class="kw">string symbol,ENUM_TIMEFRAMES tf) { class="kw">return("Arial"); }
  };
class CPriceRow : class="kw">public CRow
  {
class="kw">public:
   class="kw">virtual class="type">class="kw">string     GetValue(class="type">class="kw">string symbol,ENUM_TIMEFRAMES tf);
   class="kw">virtual class="type">class="kw">string     GetName();
  };
class="type">class="kw">string CPriceRow::GetValue(class="type">class="kw">string symbol,ENUM_TIMEFRAMES tf)
  {
   class="type">MqlTick tick;
class=class="str">"cmt">//--- 取当前价格
上面这段是 CRow 与 CPriceRow 的骨架:CRow 各虚方法默认回 "-" 或 clrWhite;CPriceRow 只声明自己要改 GetValue / GetName,其余继承默认行为。 再进阶一点,用 SMA 写条件行:价格站上均线回 'Yes',线下回 'No'。它比 RSI 行多扛 period / shift / type 三个参数,GetName 按类型与周期拼名字,GetValue 逻辑同 RSI 行但做了二值化判断。 最花哨的是 CPriceChangeRow,支持三种模式——Wingdings 箭头、绝对价格变动、百分比变动。构造入参第一个 bool 控制是否箭头模式(true 时忽略第二参),它一口气重写四个方法:GetName 给名、GetFont 切 Wingdings、GetColor 涨 lime 跌 red 否则 white、GetValue 算最新柱收开差并返回数值或箭头码。外汇与贵金属波动剧烈,这类变动行在高杠杆下误读概率偏高,上 MT5 跑之前先拿历史 tick 验证颜色与箭头逻辑。

MQL5 / C++
class CRow : class="kw">public CObject
  {
class="kw">public:
   class="kw">virtual class="type">void Init(class="type">class="kw">string &symb[],ENUM_TIMEFRAMES &tfs[]) { }
   class="kw">virtual class="type">class="kw">string GetValue(class="type">class="kw">string symbol,ENUM_TIMEFRAMES tf) { class="kw">return("-"); }
   class="kw">virtual class="type">color GetColor(class="type">class="kw">string symbol,ENUM_TIMEFRAMES tf) { class="kw">return(clrWhite); }
   class="kw">virtual class="type">class="kw">string GetName() { class="kw">return("-"); }
   class="kw">virtual class="type">class="kw">string GetFont(class="type">class="kw">string symbol,ENUM_TIMEFRAMES tf) { class="kw">return("Arial"); }
  };
class CPriceRow : class="kw">public CRow
  {
class="kw">public:
   class="kw">virtual class="type">class="kw">string     GetValue(class="type">class="kw">string symbol,ENUM_TIMEFRAMES tf);
   class="kw">virtual class="type">class="kw">string     GetName();
  };
class="type">class="kw">string CPriceRow::GetValue(class="type">class="kw">string symbol,ENUM_TIMEFRAMES tf)
  {
   class="type">MqlTick tick;
class=class="str">"cmt">//--- 取当前价格

把 RSI 塞进多品种监控表

做跨品种盯盘时,最忌讳每看一个货币对就切一次图表。把 RSI 封装成一行(CRow 派生类),表头自动标“RSI”,后面每个品种各自取数,思路就顺了。 构造函数只接一个周期参数,比如 CRSIRow r(14) 就是 14 周期 RSI;真正干活的是 Init(),它把外部传进来的品种数组和时间帧数组各复制一份,再逐个调 iRSI() 拿指标句柄存进 handles[]。 注意句柄获取放在 Init 而非构造函数里:若品种或周期数组还没就位,提前建句柄会返回 INVALID_HANDLE。下面这段是核心初始化逻辑,复制进 MT5 的 include 类文件就能跑。 外汇与贵金属波动剧烈,RSI 仅作概率参考,实际信号需结合价格结构;高杠杆下误判可能快速放大亏损。

MQL5 / C++
class CRSIRow : class="kw">public CRow
  {
class="kw">private:
   class="type">int               rsiPeriod;      class=class="str">"cmt">// RSI 周期
   class="type">class="kw">string            symbols[];      class=class="str">"cmt">// 交易品种数组
   ENUM_TIMEFRAMES   timeframes[];   class=class="str">"cmt">// 时间帧数组
   class="type">int               handles[];      class=class="str">"cmt">// RSI 句柄数组
   class=class="str">"cmt">//--- 寻找给定交易品种及时间帧的指标句柄
   class="type">int               GetHandle(class="type">class="kw">string symbol,ENUM_TIMEFRAMES tf);
class="kw">public:
   class=class="str">"cmt">//--- 构造函数
                     CRSIRow(class="type">int period);
   class=class="str">"cmt">//--- 重写 CRow 的默认 GetValue(..) 方法
   class="kw">virtual class="type">class="kw">string    GetValue(class="type">class="kw">string symbol,ENUM_TIMEFRAMES tf);
   class=class="str">"cmt">//--- 重写 CRow 的默认 GetName() 方法
   class="kw">virtual class="type">class="kw">string    GetName();
   class=class="str">"cmt">//--- 重写 CRow 的默认 Init(..) 方法
   class="kw">virtual class="type">void      Init(class="type">class="kw">string &symb[],ENUM_TIMEFRAMES &tfs[]);
   };
CRSIRow::CRSIRow(class="type">int period)
  {
   rsiPeriod=period;
  }
class="type">void CRSIRow::Init(class="type">class="kw">string &symb[],ENUM_TIMEFRAMES &tfs[])
  {
   class="type">int size=ArraySize(symb);
   ArrayResize(symbols,size);
   ArrayResize(timeframes,size);
   ArrayResize(handles,size);
   ArrayCopy(symbols,symb);
   ArrayCopy(timeframes,tfs);
   for(class="type">int i=class="num">0; i<ArraySize(symbols); i++)
      handles[i]=iRSI(symbols[i],timeframes[i],rsiPeriod,PRICE_CLOSE);
  }

「给 RSI 行和均线行写自己的取值逻辑」

把指标塞进表格行对象里,核心就是重写父类的 GetValue 和 GetName。下面这段 CRow 派生类 CRSIRow 的做法很直接:GetValue 先找句柄,再用 CopyBuffer 抓最新一根的 RSI 值,DoubleToString 保留两位小数返回;句柄找不到就回 "err",拷贝失败回 "-",不会让面板崩。 GetName 拼出 "RSI(周期)" 当列名,GetHandle 则在 symbols / timeframes / handles 三个数组里做线性匹配,命中就返句柄,否则 INVALID_HANDLE。线性查找在品种和周期数量少时开销可忽略,但若你要挂 30 个品种 × 7 个周期,匹配耗时可能倾向线性增长,可考虑改成映射结构。 紧跟着的 CPriceMARow 类声明了 maPeriod、maShift、maType 以及同样的句柄数组,构造函数接收均线类型和周期平移,显然是要把价格均线也做成可复用的一行。外汇与贵金属波动剧烈,这类面板只做多品种监控,信号本身不代表方向,实盘高风险。

MQL5 / C++
class="type">class="kw">string CRSIRow::GetValue(class="type">class="kw">string symbol,ENUM_TIMEFRAMES tf)
  {
   class="type">class="kw">double value[class="num">1];
class=class="str">"cmt">//--- 获取 RSI 指标句柄
   class="type">int handle=GetHandle(symbol,tf);
   if(handle==INVALID_HANDLE) class="kw">return("err");
class=class="str">"cmt">//--- 取得当前 RSI 数值
   if(CopyBuffer(handle,class="num">0,class="num">0,class="num">1,value)<class="num">0) class="kw">return("-");
   class="kw">return(DoubleToString(value[class="num">0],class="num">2));
  }
class="type">class="kw">string CRSIRow::GetName()
  {
   class="kw">return("RSI("+IntegerToString(rsiPeriod)+")");
  }
class="type">int CRSIRow::GetHandle(class="type">class="kw">string symbol,ENUM_TIMEFRAMES tf)
  {
   for(class="type">int i=class="num">0; i<ArraySize(timeframes); i++)
      if(symbols[i]==symbol && timeframes[i]==tf)
         class="kw">return(handles[i]);
   class="kw">return(INVALID_HANDLE);
  }
class CPriceMARow : class="kw">public CRow
  {
class="kw">private:
   class="type">int               maPeriod; class=class="str">"cmt">// 移动平均周期
   class="type">int               maShift;  class=class="str">"cmt">// 移动平均平移
   ENUM_MA_METHOD    maType;   class=class="str">"cmt">// SMA, EMA, SMMA 或 LWMA
   class="type">class="kw">string            symbols[];                class=class="str">"cmt">// 交易品种数组
   ENUM_TIMEFRAMES   timeframes[];            class=class="str">"cmt">// 时间帧数组
   class="type">int               handles[];               class=class="str">"cmt">// MA 句柄数组
   class=class="str">"cmt">//--- 寻找给定交易品种及时间帧的指标句柄
   class="type">int               GetHandle(class="type">class="kw">string symbol,ENUM_TIMEFRAMES tf);
class="kw">public:
   class=class="str">"cmt">//--- 构造函数
                     CPriceMARow(ENUM_MA_METHOD type,class="type">int period,class="type">int shift);
   class=class="str">"cmt">//--- 重写 CRow 的默认 GetValue(..) 方法
   class="kw">virtual class="type">class="kw">string    GetValue(class="type">class="kw">string symbol,ENUM_TIMEFRAMES tf);
   class=class="str">"cmt">// 重写 CRow 的默认 GetName() 方法
   class="kw">virtual class="type">class="kw">string    GetName();
   class=class="str">"cmt">//--- 重写 CRow 的默认 Init(..) 方法

◍ 用一行 Yes/No 把多品种均线位置跑出来

CPriceMARow 把「价格在均线上方吗」这种判断封装成表格里的一行。构造函数只接三个参数:均线类型、周期、偏移,例如 CPriceMARow(MODE_EMA,50,0) 就是一根 50 周期偏移 0 的 EMA。 Init 负责把外部传进来的品种数组 symb[] 和周期数组 tfs[] 拷进类内部,并给每个「品种+周期」组合调用 iMA 拿句柄。ArrayResize 先按 ArraySize(symb) 定长,再 ArrayCopy 复制内容,最后 for 循环里 handles[i]=iMA(...) 一次性建好所有句柄。 GetValue 是真正出结果的地方:先用 GetHandle 拿句柄,失败返回 err;CopyBuffer 取最新一根均线值到 value[1],SymbolInfoTick 取最新 bid。tick.bid>value[0] 成立就返回 Yes,否则 No。也就是说,XAUUSD 的 M5 图表上若 bid 站在 EMA50 之上,这一格就显示 Yes,概率上倾向多头占优,但外汇贵金属杠杆高,信号失效也常见。 GetName 把类型转成短名拼出列头,如 Price>EMA(50)。把这套类丢进前文的多品种监控表,MT5 里就能直接扫一批货币对和周期,肉眼看哪格是 Yes。

MQL5 / C++
class="kw">virtual class="type">void      Init(class="type">class="kw">string &symb[],ENUM_TIMEFRAMES &tfs[]);
};
class=class="str">"cmt">//+------------------------------------------------------------------
class=class="str">"cmt">//| CPriceMARow类的构造函数                                          |
class=class="str">"cmt">//+------------------------------------------------------------------
CPriceMARow::CPriceMARow(ENUM_MA_METHOD type,class="type">int period,class="type">int shift)
  {
   maPeriod= period;
   maShift = shift;
   maType=type;
  }
class=class="str">"cmt">//+------------------------------------------------------------------
class=class="str">"cmt">//| 重写 CRow 的默认Init(..) 方法                                     |
class=class="str">"cmt">//+------------------------------------------------------------------
class="type">void CPriceMARow::Init(class="type">class="kw">string &symb[],ENUM_TIMEFRAMES &tfs[])
  {
   class="type">int size=ArraySize(symb);

   ArrayResize(symbols,size);
   ArrayResize(timeframes,size);
   ArrayResize(handles,size);

class=class="str">"cmt">//--- 复制数组内容至它自己的数组
   ArrayCopy(symbols,symb);
   ArrayCopy(timeframes,tfs);

class=class="str">"cmt">//--- 取得所有交易品种或时间帧的 MA 句柄
   for(class="type">int i=class="num">0; i<ArraySize(symbols); i++)
      handles[i]=iMA(symbols[i],timeframes[i],maPeriod,maShift,maType,PRICE_CLOSE);
  }
class=class="str">"cmt">//+------------------------------------------------------------------
class=class="str">"cmt">//| 重写 CRow 的默认GetValue(..)方法                                  |
class=class="str">"cmt">//+------------------------------------------------------------------
class="type">class="kw">string CPriceMARow::GetValue(class="type">class="kw">string symbol,ENUM_TIMEFRAMES tf)
  {
   class="type">class="kw">double value[class="num">1];
   class="type">MqlTick tick;
class=class="str">"cmt">//--- 获取 MA 指标句柄
   class="type">int handle=GetHandle(symbol,tf);
   if(handle==INVALID_HANDLE) class="kw">return("err");
class=class="str">"cmt">//--- 取最后 MA 数值
   if(CopyBuffer(handle,class="num">0,class="num">0,class="num">1,value)<class="num">0) class="kw">return("-");
class=class="str">"cmt">//--- 取最后价格
   if(!SymbolInfoTick(symbol,tick)) class="kw">return("-");
class=class="str">"cmt">//--- 检查当前条件: 价格 > MA
   if(tick.bid>value[class="num">0])
      class="kw">return("Yes");
   else
      class="kw">return("No");
  }
class=class="str">"cmt">//+------------------------------------------------------------------
class=class="str">"cmt">//| 重写 CRow 的默认GetName()方法                                     |
class=class="str">"cmt">//+------------------------------------------------------------------
class="type">class="kw">string CPriceMARow::GetName()
  {
   class="type">class="kw">string name;
   class="kw">switch(maType)
     {
      case MODE_SMA: name = "SMA"; break;
      case MODE_EMA: name = "EMA"; break;
      case MODE_SMMA: name = "SMMA"; break;
      case MODE_LWMA: name = "LWMA"; break;
     }
   class="kw">return("Price>"+name+"("+IntegerToString(maPeriod)+")");
  }
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到多周期 RSI 与价格变动的同屏视图,你只需专注条件是否该调。

常见问题

它在所监视品种图表上响应 OnCalculate,新订单号抵达时发 CHARTEVENT_CUSTOM 给 CTable 对象触发刷新,逻辑延续了多货币模式实现文的事件桥接思路。
本文方案是两种工作模式二选一:前者锁品种换周期,后者锁周期换品种;并行需另起实例或改核心类,概率上会增加事件冲突。
小布的内置多周期视图走的是轻量聚合通道,不暴露 CTable 源码,但显示维度与本文的 CPriceRow / CRSIRow 等价,省去你自己编译 SpyAgent。
继承 CRow 写个 CPriceMARow 扩展类,在内部算 SMA 并比对买价,面板核心不用改一行,外汇贵金属高波动下信号可能频繁翻转。
写死会让 CTable 随内容膨胀,扩展类分离后加 RSI 或价格变动只是新增子类,符合本文少改核心的通用面板目标。