MQL5多品种价格偏离指标开发实战
📊

MQL5多品种价格偏离指标开发实战

从架构拆分到双色渲染与图表事件处理

MQL5 编程 难度 · 进阶 2026-01-30 10 分钟阅读
#MQL5#多品种指标#价格偏离#图表事件

一、什么是指标分析的价格偏离

价格偏离(Price Divergence)在本指标语境中,并非传统技术指标里的背离信号,而是指多个品种的价格序列在某一参考点被“汇聚到同一水平”,从而直观对比它们在此后一段时间的相对强弱与走势差异。例如把 EURUSD、GBPUSD、USDCHF 等以美元为基准或反向基准的货币对,在每周开盘时刻对齐开盘价,随后在图表上并排绘制各自真实价格走势,交易者可一眼看出哪个货币对涨得多、哪个跌得狠。

与之前文章开发的“多货币波动指标”不同,本次指标直接输出原始价格数据(折线、柱线或蜡烛条),而非波动率数值。它支持五种汇聚起点模式:垂直线拖拽、月、周、日、小时,并允许对单个品种做上下翻转,以解决基准货币不一致导致的视觉颠倒问题。

✦ 适用场景
当你需要同时监视最多5个品种在同一时间锚点后的绝对价格位移,而非归一化比率时,这类指标比手动开多个副图更高效。

二、项目架构与多文件拆分

原文指标代码约1500行,作者将其拆分为主文件与三个 Include 头文件,这是大型 MQL5 指标必备的工程习惯:Checks.mqh 负责数据可用性检查与下载;Objects.mqh 管理图形对象(如垂直线);Chart.mqh 管理图表属性与可视区域最大最小值修正。主文件仅保留核心逻辑、全局变量与事件函数。

在 MT5 的 MQL5/Indicators 目录下建立与指标同名的文件夹,Include 子目录存放上述 .mqh 文件。主文件通过 #include "Include/Checks.mqh" 等相对路径引入。这种结构提升可读性,也方便多人协作或后续复用函数。

#define RESET           0
#define SYMBOLS_COUNT   5
#include <Canvas\Canvas.mqh>
#include "Include/Checks.mqh"
#include "Include/Chart.mqh"
#include "Include/Objects.mqh"
⚠ 常见坑
Include 路径若写成 #include <Checks.mqh> 而文件不在标准库目录,编译会报 cannot open file。务必使用相对路径或放入 MQL5/Include 根目录。

三、缓冲区和双色渲染机制

指标属性中 #property indicator_buffers 25 表示共25个缓冲区,#property indicator_plots 5 表示5个绘图序列(对应当前品种+5个外部品种)。单色折线模式仅需每个品种1个 close 缓冲;但柱线/蜡烛条模式需要 open/high/low/close 四个数据缓冲,外加一个 icolor 颜色索引缓冲来实现双色(阳线/阴线不同色)。因此 5 品种 × 5 = 25。

颜色在 #property indicator_colorN 中定义,逗号前为单色模式或阳线色,逗号后为阴线色。例如 #property indicator_color1 clrDodgerBlue,C'0,50,100'。在 SetIndicatorProperties() 中根据 DrawType 调用 SetIndexBuffer 绑定缓冲,并用 PLOT_DRAW_TYPE 设为 DRAW_LINE / DRAW_COLOR_BARS / DRAW_COLOR_CANDLES。

#property indicator_chart_window
#property indicator_buffers 25
#property indicator_plots   5
#property indicator_color1  clrDodgerBlue,C'0,50,100'
#property indicator_color2  clrLimeGreen,C'20,80,20'
#property indicator_color3  clrGold,C'160,140,0'
#property indicator_color4  clrAqua,C'0,140,140'
#property indicator_color5  clrMagenta,C'130,0,130'

SetBufferColorIndex() 函数在填充缓冲时依据 close>open 将 icolor 写为0或1,终端据此从颜色列表取色。若 TwoColor=false,则全部写0,使用第一种颜色。

void SetBufferColorIndex(int i,int symbol_number,double close,double open)
  {
   if(TwoColor)
     {
      if(close>open) buffer_data[symbol_number].icolor[i]=0;
      else buffer_data[symbol_number].icolor[i]=1;
     }
   else buffer_data[symbol_number].icolor[i]=0;
  }

四、外部参数与汇聚起点模式

通过两个枚举 ENUM_DRAWTYPE(LINE/BARS/CANDLES)和 ENUM_START_POINT(VERTICAL_LINE/MONTH/WEEK/DAY/HOUR)构建下拉输入。外部参数输入5个品种名及对应的 Inverse 翻转开关。品种数量从2计起,因为索引0预留给当前图表品种。

enum ENUM_DRAWTYPE { LINE=0, BARS=1, CANDLES=2 };
enum ENUM_START_POINT { VERTICAL_LINE=0, MONTH=1, WEEK=2, DAY=3, HOUR=4 };
input ENUM_DRAWTYPE DrawType=CANDLES;
input ENUM_START_POINT StartPriceDivergence=VERTICAL_LINE;
input bool TwoColor=false;
input string Symbol02="GBPUSD";
input bool Inverse02=false;

翻转(Inverse)的意义:若当前图是 EURUSD(美元为报价币),而 USDCHF 中美元是基础币,直接同图对比价格走向会视觉相反。将 USDCHF 翻转后,其走势方向与其他以美元为基准的货币对一致,便于分析。InitInverse() 将输入布尔值读入 inverse[] 数组,在后续数据填充时对价格取负向偏移或倒置处理。

VERTICAL_LINE 模式在 OnInit 时创建一条可拖拽绿色垂直线,其时间位置即所有品种汇聚的参考点,取该处当前品种开盘价为基准。其他周期模式则在 InitStartPointTF() 中把 timeframe_start_point 设为 MN1/W1/D1/H1,并在数据中找到对应周期起始棒时间作为汇聚点。

void InitStartPointTF()
  {
   if(StartPriceDivergence==VERTICAL_LINE) return;
   switch(StartPriceDivergence)
     {
      case MONTH : timeframe_start_point=PERIOD_MN1; break;
      case WEEK  : timeframe_start_point=PERIOD_W1;  break;
      case DAY   : timeframe_start_point=PERIOD_D1;  break;
      case HOUR  : timeframe_start_point=PERIOD_H1;  break;
     }
  }
⚠ 参数校验
CheckInputParameters() 会阻止“当前图表周期大于等于汇聚周期”的非法组合,例如在主图 D1 上选 DAY 模式。否则数据无法向下细分,指标直接返回错误。

五、初始化函数与策略测试器限制

OnInit() 依次调用:CheckTesterMode 禁止在策略测试器中使用(因图表属性控制未完全支持);SetBarsColors 设置当前图表的蜡烛颜色;InitStartPointTF;CheckInputParameters;EventSetMillisecondTimer 开定时器;InitArrays/InitSymbolNames/InitInverse 初始化数组;SetIndicatorProperties 绑定缓冲;SetDivergenceLine 建线;ChartRedraw 刷新。

bool CheckTesterMode()
  {
   if(MQLInfoInteger(MQL_TESTER)||MQLInfoInteger(MQL_VISUAL_MODE)||MQLInfoInteger(MQL_OPTIMIZATION))
     {
      Comment("Currently, the indicator is not intended to be used in Strategy Tester!");
      return(false);
     }
   return(true);
  }

SetBarsColors() 利用 ChartSetInteger 修改 CHART_COLOR_CHART_UP 等属性,使主图蜡烛色与指标逻辑协调。若 TwoColor 关闭,阴线也用阳线色,避免花哨。

void SetBarsColors()
  {
   ChartSetInteger(0,CHART_COLOR_CHART_UP,color_bar_up);
   ChartSetInteger(0,CHART_COLOR_CANDLE_BULL,color_bar_up);
   if(TwoColor)
     {
      ChartSetInteger(0,CHART_COLOR_CHART_DOWN,color_bar_down);
      ChartSetInteger(0,CHART_COLOR_CANDLE_BEAR,color_bar_down);
     }
   else
     {
      ChartSetInteger(0,CHART_COLOR_CHART_DOWN,color_bar_up);
      ChartSetInteger(0,CHART_COLOR_CANDLE_BEAR,color_bar_up);
     }
  }

六、数据加载与缓冲填充逻辑

OnCalculate 中首先 CheckAvailableData() 严格校验各品种及高周期数据是否就绪,采用最多100次 CopyTime/CopyRates 重试,避免因网络或缓存延迟拿到空数组。随后 FillIndicatorBuffers() 在单循环里完成:取当前棒数据、判定汇聚时间、计算相对偏移、写缓冲、置颜色。对于非垂直线模式,还需通过高周期(如 W1)确定每周起始棒时间,作为 limit_time 对齐基准。

bool CheckAvailableData()
  {
   int attempts=100;
   for(int s=0;s<SYMBOLS_COUNT;s++)
     {
      if(symbol_names[s]!=empty_symbol)
        {
         datetime time[]; int total_period_bars=0;
         datetime terminal_first_date=(datetime)SeriesInfoInteger(symbol_names[s],Period(),SERIES_TERMINAL_FIRSTDATE);
         total_period_bars=Bars(symbol_names[s],Period(),terminal_first_date,TimeCurrent());
         for(int i=0;i<attempts;i++)
           if(CopyTime(symbol_names[s],Period(),0,total_period_bars,time) && ArraySize(time)>=total_period_bars) break;
         if(ArraySize(time)==0||ArraySize(time)<total_period_bars) return(false);
        }
     }
   return(true);
  }

FillIndicatorBuffers 对每根可见棒,用 CopyRates 取指定品种同时间棒,若时间不匹配或含 EMPTY_VALUE 则写空值。汇聚点价格作为基准,后续棒价格减去基准并叠加到主图坐标。若 Inverse 为真,则做反向位移。此函数还调用 SetBufferColorIndex 决定阴阳色。

✦ 性能提示
重试机制虽稳,但 attempts=100 在极端掉线时会造成卡顿。实盘可改为10次并配合定时器重算,或仅对失败品种局部刷新。

七、图表事件与可视区域自适应

指标响应两类图表事件:CHARTEVENT_OBJECT_DRAG(拖拽垂直线)时,若处于 VERTICAL_LINE 模式,手动调用 OnCalculate 重算缓冲,使所有品种重新以新参考点汇聚;CHARTEVENT_CHART_CHANGE(缩放或属性修改)时,调用 CorrectChartMaxMin() 重算所有品种在可视区域内的真实最高/最低,并通过 ChartSetInteger 设置图表上下界,防止某些品种价格超出视图被裁剪。

void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(id==CHARTEVENT_OBJECT_DRAG)
     {
      if(StartPriceDivergence==VERTICAL_LINE)
         OnCalculate(OC_rates_total,0,OC_time,OC_open,OC_high,OC_low,OC_close,OC_tick_volume,OC_volume,OC_spread);
     }
   if(id==CHARTEVENT_CHART_CHANGE) CorrectChartMaxMin();
  }

CorrectChartMaxMin 先用 ChartGetInteger 取 CHART_VISIBLE_BARS 与 CHART_FIRST_VISIBLE_BAR,反推最后可见棒索引,再用 CopyHigh/CopyLow 收集当前品种及所有外部品种在可视区间的极值,取全局最大最小并加偏移写回图表属性。这样多品种叠加时,视图总能完整容纳全部线条。

⚠ 读者反馈坑
原文评论区有用户指出在新版 MT5 下 Checks.mqh 编译挂起或安装报错。原因多是近年 MQL5 对 SeriesInfoInteger 返回值类型或 Canvas 类路径调整。迁移时需检查 datetime 强转与 #include <Canvas/Canvas.mqh> 斜杠方向。

八、与其他多品种方案的对比与增强思路

对比“多货币波动指标”:本文直接绘价格,更直观但占纵向空间大;波动指标用比率,适合副图叠加。对比手动开多个窗口:本指标同图对齐时间轴,避免肉眼错频。若想增强,可加入品种相关性计算、自动识别基准币、或支持超过5个品种(需动态申请缓冲,突破25限制需用指标句柄或离线计算)。

由于指标禁用了测试器,无法用于回测信号研究;若需历史偏离统计,应抽取 FillIndicatorBuffers 核心算法为脚本或 EA,脱离图表属性依赖。整体而言,该案例是学习 MQL5 多品种、多缓冲、图表事件与工程拆分的优质范本。

常见问题

按绘图模式计算:折线模式每品种1个close缓冲;柱线/蜡烛模式每品种需open/high/low/close共4个数据缓冲加1个icolor颜色索引缓冲。例如5品种双色蜡烛需5×5=25缓冲,用#property indicator_buffers 25声明,并在SetIndicatorProperties里循环SetIndexBuffer绑定。
在OnInit用CreateVerticalLine建可拖拽OBJ_VLINE并命名;在OnChartEvent中捕获CHARTEVENT_OBJECT_DRAG且模式为VERTICAL_LINE时,直接调用OnCalculate传入保存的OC_系列全局数组重算;FillIndicatorBuffers会读取ObjectGetInteger的OBJPROP_TIME作为新汇聚点。
因为指标大量使用ChartSetInteger修改主图属性、Canvas类及CHARTEVENT事件,而MT5策略测试器未完全支持图表属性控制。代码用MQLInfoInteger(MQL_TESTER)等判断,若为真则返回INIT_FAILED并Comment提示。
在外部参数设Inverse布尔输入,InitInverse读入inverse数组。填充缓冲时若inverse[s]为真,将该品种相对汇聚点的偏移取反(或价格轴倒置),使得如USDCHF在以EURUSD为图时视觉方向一致,便于同基准分析。
在CHARTEVENT_CHART_CHANGE事件里调用CorrectChartMaxMin,利用ChartGetInteger取可视棒数,CopyHigh/CopyLow收集所有品种可视区极值,用ChartSetInteger设置CHART_FIXED_MAX/CHART_FIXED_MIN,保证多品种完整显示。