在 EA 交易代码中实现指标的计算·进阶篇
📘

在 EA 交易代码中实现指标的计算·进阶篇

第 2/3 篇

◍ 指标基类的缓冲与历史加载骨架

在 MT5 自建指标类时,先得把缓冲区和历史长度这两个底层变量钉死。CIndicator 构造函数里直接把 m_data_len 和 m_history_len 设为 Bars(m_Symbol,m_Timeframe)-1,也就是当前图表可用 K 线数减 1,这一步决定了后续数组下标从 0 开始对齐。 Create() 方法接收 symbol、timeframe、price 三个参数,若 symbol 传 NULL 就退回到 _Symbol。注意里面用 SymbolInfoInteger 检查品种是否被添加到市场报价窗口,没添加就 SymbolSelect 拉一下,失败直接返回 false——很多人回测时品种没勾选,指标悄无声息地不加载,根子在这。 SetBufferSize 只接受正数 bars 来覆写 m_data_len,传 0 或负数是不生效的。这意味着你想动态限制计算长度,必须显式给大于 0 的值,否则类内部始终按构造时的全历史长度跑。 开 MT5 新建一个继承自 CIndicator 的空类,在 OnCreate 里打印 m_data_len,切换不同品种观察返回值差异,能直观验证这套初始化逻辑。

MQL5 / C++
class="kw">virtual class="type">bool        SetBufferSize(class="kw">const class="type">int bars);
class=class="str">"cmt">//--- 取得指标数据
  class="kw">virtual class="type">int          CopyBuffer(class="kw">const class="type">uint buffer_num,class="kw">const class="type">uint start, class="kw">const class="type">uint count, class="type">class="kw">double &double_array[]);
  class="kw">virtual class="type">class="kw">double       GetData(class="kw">const class="type">uint buffer_num,class="kw">const class="type">uint shift);
class="kw">protected:
  class="type">class="kw">double               m_source_data[];
  CArrayBuffer         ar_IndBuffers[];
  class="type">int                  m_buffers;
  class="type">int                  m_history_len;
  class="type">int                  m_data_len;
class=class="str">"cmt">//---
  class="type">class="kw">string               m_Symbol;
  ENUM_TIMEFRAMES      m_Timeframe;
  ENUM_APPLIED_PRICE   m_Price;
class=class="str">"cmt">//--- 设置指标的主要配置
  class="kw">virtual class="type">bool         SetHistoryLen(class="kw">const class="type">int bars=-class="num">1);
class=class="str">"cmt">//---
  class="kw">virtual class="type">bool         LoadHistory(class="type">void);
  class="kw">virtual class="type">bool         Calculate()                     { class="kw">return true; }
  };
CIndicator::CIndicator()  :  m_buffers(class="num">0),
                           m_Symbol(_Symbol),
                           m_Timeframe(PERIOD_CURRENT),
                           m_Price(PRICE_CLOSE),
                           m_last_load(class="num">0)
  {
   m_data_len=m_history_len  = Bars(m_Symbol,m_Timeframe)-class="num">1;
   ArrayFree(ar_IndBuffers);
   ArrayFree(m_source_data);
  }
class="type">bool CIndicator::Create(class="kw">const class="type">class="kw">string symbol=NULL,class="kw">const ENUM_TIMEFRAMES timeframe=class="num">0,class="kw">const ENUM_APPLIED_PRICE price=class="num">1)
  {
   m_Symbol=(symbol==NULL ? _Symbol : symbol);
   if(!SymbolInfoInteger(m_Symbol,SYMBOL_SELECT))
      if(!SymbolSelect(m_Symbol,true))
         class="kw">return class="kw">false;
class=class="str">"cmt">//---
   m_Timeframe=timeframe;
   m_Price=price;
class=class="str">"cmt">//---
   class="kw">return true;
  }
class="type">bool CIndicator::SetBufferSize(class="kw">const class="type">int bars)
  {
   if(bars>class="num">0)
      m_data_len  =  bars;
   else

缓冲区与历史数据的初始化分支

自定义指标类在每次计算前要先确认 Bars 数量,若返回 0 或负值说明品种/周期未就绪,此时必须逐一 Shutdown 缓冲区并返回 false,避免后续数组越界。 当 m_history_len 小于当前数据长度时,会调用 SetHistoryLen 把需求长度补齐;随后对每个缓冲区执行 Resize(m_data_len),任一失败即中断。这套逻辑保证了 MT5 重绘或切换品种时,指标内存不会残留旧引用。 LoadHistory 用 SeriesInfoInteger 取最后一根 K 线时间,若 m_last_load 已不早于该时间且源数组够长,直接返回 true 跳过拷贝——这是减少重复 IO 的关键判断。 价格类型分支里,PRICE_MEDIAN 与 PRICE_TYPICAL 走 CopyRates 后手动算 (H+L)/2 与 (H+L+C)/3,而 OPEN/HIGH/LOW 分支原代码漏写 break,可能穿透执行,复制代码时需手动补断点。外汇与贵金属行情跳空频繁,这类历史加载缺陷在实盘可能倾向导致数组错位,开 MT5 跑一遍断点最稳妥。

MQL5 / C++
m_data_len = Bars(m_Symbol,m_Timeframe);
class=class="str">"cmt">//---
  if(m_data_len<=class="num">0)
   {
     for(class="type">int i=class="num">0;i<m_buffers;i++)
       ar_IndBuffers[i].Shutdown();
     class="kw">return class="kw">false;
   }
class=class="str">"cmt">//---
  if(m_history_len<m_data_len)
    if(!SetHistoryLen(m_data_len))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
  for(class="type">int i=class="num">0;i<m_buffers;i++)
   {
     ar_IndBuffers[i].Shutdown();
     if(!ar_IndBuffers[i].Resize(m_data_len))
       class="kw">return class="kw">false;
   }
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CIndicator::LoadHistory(class="type">void)
  {
  class="type">class="kw">datetime cur_date=(class="type">class="kw">datetime)SeriesInfoInteger(m_Symbol,m_Timeframe,SERIES_LASTBAR_DATE);
  if(m_last_load>=cur_date && ArraySize(m_source_data)>=m_history_len)
    class="kw">return true;
class=class="str">"cmt">//---
  class="type">MqlRates rates[];
  class="type">int total=class="num">0,i;
  class="kw">switch(m_Price)
    {
    case PRICE_CLOSE:
      total=CopyClose(m_Symbol,m_Timeframe,class="num">1,m_history_len,m_source_data);
      class="kw">break;
    case PRICE_OPEN:
      total=CopyOpen(m_Symbol,m_Timeframe,class="num">1,m_history_len,m_source_data);
    case PRICE_HIGH:
      total=CopyHigh(m_Symbol,m_Timeframe,class="num">1,m_history_len,m_source_data);
    case PRICE_LOW:
      total=CopyLow(m_Symbol,m_Timeframe,class="num">1,m_history_len,m_source_data);
    case PRICE_MEDIAN:
      total=CopyRates(m_Symbol,m_Timeframe,class="num">1,m_history_len,rates);
      if(total!=ArraySize(m_source_data))
        total=ArrayResize(m_source_data,total);
      for(i=class="num">0;i<total;i++)
        m_source_data[i]=(rates[i].high+rates[i].low)/class="num">2;
      class="kw">break;
    case PRICE_TYPICAL:
      total=CopyRates(m_Symbol,m_Timeframe,class="num">1,m_history_len,rates);
      if(total!=ArraySize(m_source_data))
        total=ArrayResize(m_source_data,total);
      for(i=class="num">0;i<total;i++)
        m_source_data[i]=(rates[i].high+rates[i].low+rates[i].close)/class="num">3;
      class="kw">break;
    case PRICE_WEIGHTED:
      total=CopyRates(m_Symbol,m_Timeframe,class="num">1,m_history_len,rates);
      if(total!=ArraySize(m_source_data))

「把典型价塞进数组再封装成可复用均线类」

这段逻辑先按 K 线数扩容 m_source_data,再把每根 bar 的 (high+low+2*close)/4 写进去。这个加权典型价给收盘价两倍权重,比普通 (H+L+C)/3 更贴收盘价轨迹,在贵金属 1H 上回看 200 根,对突刺噪声的平滑倾向更明显。 ArrayResize 返回的实际长度赋给 total,循环填完直接 break 退出;若 total<=0 直接返回 false,说明拿不到 rates 数据,MT5 里常见原因是品种休市或历史不全。 后面 CIndicator::CopyBuffer 先做 Calculate() 守卫,buffer 序号越界就释放数组并返回 -1,否则转发给 ar_IndBuffers 对应缓冲区。CMA 类继承它,构造时默认 m_Period=25、m_Shift=0、m_Method=MODE_SMA,并固死 m_buffers=1。 开 MT5 新建 EA 把这段抄进去,改 CMA 构造参数里的 25 为 50,加载到 XAUUSD 看缓冲输出差异,外汇与贵金属杠杆高,参数验证请用模拟盘。

MQL5 / C++
 total=ArrayResize(m_source_data,total);
   for(i=class="num">0;i<total;i++)
      m_source_data[i]=(rates[i].high+rates[i].low+class="num">2*rates[i].close)/class="num">4;
      class="kw">break;
   }
class=class="str">"cmt">//---
   if(total<=class="num">0)
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   m_last_load=cur_date;
   class="kw">return (total>class="num">0);
   }
class="type">int CIndicator::CopyBuffer(class="kw">const class="type">uint buffer_num,class="kw">const class="type">uint start,class="kw">const class="type">uint count,class="type">class="kw">double &double_array[])
  {
   if(!Calculate())
      class="kw">return -class="num">1;
class=class="str">"cmt">//---
   if((class="type">int)buffer_num>=m_buffers)
     {
      ArrayFree(double_array);
      class="kw">return -class="num">1;
     }
class=class="str">"cmt">//---
   class="kw">return ar_IndBuffers[buffer_num].CopyBuffer(start,count,double_array);
  }
class CMA : class="kw">public CIndicator
  {
class="kw">private:
   class="type">int                m_Period;
   class="type">int                m_Shift;
   ENUM_MA_METHOD    m_Method;
   class="type">class="kw">datetime           m_last_calculate;

class="kw">public:
                     CMA();
                    ~CMA();
   class="type">bool              Create(class="kw">const class="type">class="kw">string symbol, class="kw">const ENUM_TIMEFRAMES timeframe, class="kw">const class="type">int ma_period, class="kw">const class="type">int ma_shift, class="kw">const ENUM_MA_METHOD ma_method, class="kw">const ENUM_APPLIED_PRICE price=PRICE_CLOSE);
   class="kw">virtual class="type">bool      SetBufferSize(class="kw">const class="type">int bars);

class="kw">protected:
   class="kw">virtual class="type">bool      Calculate();
   class="kw">virtual class="type">class="kw">double    CalculateSMA(class="kw">const class="type">int shift);
   class="kw">virtual class="type">class="kw">double    CalculateEMA(class="kw">const class="type">int shift);
   class="kw">virtual class="type">class="kw">double    CalculateLWMA(class="kw">const class="type">int shift);
   class="kw">virtual class="type">class="kw">double    CalculateSMMA(class="kw">const class="type">int shift);
   };
CMA::CMA()  :  m_Period(class="num">25),
               m_Shift(class="num">0),
               m_Method(MODE_SMA)
  {
   m_buffers=class="num">1;
   ArrayResize(ar_IndBuffers,class="num">1);
  }
class="type">bool CMA::Create(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int ma_period,class="kw">const class="type">int ma_shift,class="kw">const ENUM_MA_METHOD ma_method,class="kw">const ENUM_APPLIED_PRICE price=class="num">1)
  {

◍ 移动均线类的初始化与增量计算逻辑

自定义均线指标在 MT5 里先走 Create 流程:若底层 CIndicator 句柄建不出来直接返 false;ma_period 若 <=0 也拒建,避免除零和空缓冲。 接着把周期、偏移、平滑方式存进成员,再按 ma_period 开缓冲,历史长度取 2*ma_period 加上正偏移量,给近端重算留余量。 Calculate 里先用 SERIES_LASTBAR_DATE 拿最后 K 线时间,若日期未变且源数据大小已等于 m_history_len,就跳过省算力。 若跨了根新 K 线,用 Bars 算偏移 shift;shift 超过数据长就清空缓冲重来,否则把缓冲 Shift 往前推,只补算新段。 循环里按 m_Method 分流 SMA/EMA/SMMA/LWMA:超出现有总数就 Add,已存在就 Update,保证只动增量、不整段重算。外汇与贵金属波动大,这类自管缓冲在跳空时可能漏根,上 MT5 跑前先打印 m_history_len 核对。

MQL5 / C++
  if(!CIndicator::Create(symbol,timeframe,price))
        class="kw">return class="kw">false;
class=class="str">"cmt">//---
  if(ma_period<=class="num">0)
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
  m_Period=ma_period;
  m_Shift=ma_shift;
  m_Method=ma_method;
class=class="str">"cmt">//---
  if(!SetBufferSize(ma_period))
      class="kw">return class="kw">false;
  if(!SetHistoryLen(class="num">2*ma_period+(m_Shift>class="num">0 ? m_Shift : class="num">0)))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CMA::Calculate(class="type">void)
  {
  class="type">class="kw">datetime cur_date=(class="type">class="kw">datetime)SeriesInfoInteger(m_Symbol,m_Timeframe,SERIES_LASTBAR_DATE);
  if(m_last_calculate==cur_date && ArraySize(m_source_data)==m_history_len)
      class="kw">return true;
class=class="str">"cmt">//---
  if(!LoadHistory())
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
  class="type">int shift=Bars(m_Symbol,m_Timeframe,m_last_calculate,cur_date)-class="num">1;
  if(shift>m_data_len)
     {
       ar_IndBuffers[class="num">0].Initilize();
       shift=m_data_len;
     }
  else
      ar_IndBuffers[class="num">0].Shift(shift);
class=class="str">"cmt">//---
  for(class="type">int i=(m_data_len-shift);i<m_data_len;i++)
     {
       class="type">int data_total=ar_IndBuffers[class="num">0].Total();
       class="kw">switch(m_Method)
         {
          case MODE_SMA:
            if(i>=data_total)
                ar_IndBuffers[class="num">0].Add(CalculateSMA(i+m_Shift));
            else
                ar_IndBuffers[class="num">0].Update(i,CalculateSMA(i+m_Shift));
            class="kw">break;
          case MODE_EMA:
            if(i>=data_total)
                ar_IndBuffers[class="num">0].Add(CalculateEMA(i+m_Shift));
            else
                ar_IndBuffers[class="num">0].Update(i,CalculateEMA(i+m_Shift));
            class="kw">break;
          case MODE_SMMA:
            if(i>=data_total)
                ar_IndBuffers[class="num">0].Add(CalculateSMMA(i+m_Shift));
            else
                ar_IndBuffers[class="num">0].Update(i,CalculateSMMA(i+m_Shift));
            class="kw">break;
          case MODE_LWMA:
            if(i>=data_total)
                ar_IndBuffers[class="num">0].Add(CalculateLWMA(i+m_Shift));
            else
                ar_IndBuffers[class="num">0].Update(i,CalculateLWMA(i+m_Shift));
            class="kw">break;

收尾时把日期与缓冲长度锁住

指标计算走到末段,先把当前日期写进 m_last_calculate,再把零号缓冲区的元素总数赋给 m_data_len,这两步决定了下一根 K 线进来时是否跳过重复运算。 从代码看,ar_IndBuffers[0].Total() 返回的是已填充的指标缓冲条数,若你在日志里打印这个值,就能确认每天只重算一次而不是每 tick 重算。 最后 return true 代表本次 Calculate 正常结束,MT5 不会因返回值误报初始化失败;想验证可故意把 m_last_calculate 赋值注释掉,观察 CPU 占用是否随 tick 数线性抬升。

MQL5 / C++
   }
   }
class=class="str">"cmt">//---
   m_last_calculate=cur_date;
   m_data_len=ar_IndBuffers[class="num">0].Total();
class=class="str">"cmt">//---
   class="kw">return true;
   }

常见问题

先在初始化分支里判断历史柱数是否小于所需周期,不足就直接返回INIT_FAILED;确认缓冲数组用SetIndexBuffer绑定且索引从0连续分配。
把典型价先写入一个临时数组,再传给封装好的均线类做平滑;类内部只暴露输入数组和周期参数,外部无需关心缓冲细节。
可以,把EA或指标代码贴给小布,它会按缓冲绑定、历史加载分支、增量计算三层指出可能越界或漏加载的位置。
不需要全量重算;在OnCalculate里只对新增柱调用增量公式,用上一根的输出加新典型价做递推,省CPU也避免滑点。
锁住可防止后续调参或切换周期时缓冲越界和日期错位;不锁可能在回测与实盘切换时出现数组长度不匹配导致EA报错退出。