带有图形界面的通用趋势·进阶篇
📈

带有图形界面的通用趋势·进阶篇

(2/3)·传统趋势指标只给信号不给交互,这篇把均线坡度和GUI绑成一套可点选的系统

含代码示例 第 2/3 篇

不少交易者把移动平均线坡度当趋势开关,却懒得把信号做成可视面板,结果每次切换周期都要肉眼比对。更麻烦的是,多数人以为趋势指标只能输出箭头,不知道它能直接驱动图形界面里的控件状态。本篇接着上一部分,把通用趋势的判断逻辑塞进可交互的窗体里。

指标枚举里藏着的分类逻辑

在 MT5 自定义指标或 EA 的底层枚举定义里,常见一类把几十种指标用法拆开声明的写法。上面这段枚举把包络、分形、Ichimoku 云图、MACD 方向等 30 余种状态逐一列成独立标签,本质是给后续信号判断提供「类型开关」。 注意命名规律:带 _Dir 的只看方向(如 Type_MA_Dir 只判均线多空倾斜),带 _Level 的叠加阈值(如 Type_RSI_Level 要卡超买超卖线),带 _2MA 或 _MS 的则涉及双线交叉或主线/信号线关系。 实盘里你可以直接抄这套枚举去写信号路由:EURUSD 15 分钟上若命中 Type_Ichimoku_TK 且 Type_MACD_Dir 同向,倾向视为动能共振,但外汇和贵金属杠杆高、滑点大,仍可能反向扫损。 开 MT5 按 F4 把这段塞进 enum 测试,改一个 _Level 的阈值常数就能看到信号触发频率变化。

MQL5 / C++
  Type_Envelopes,              class=class="str">"cmt">// 包络
  Type_Force_Level,            class=class="str">"cmt">// 强推振荡器和级别
  Type_Fractals,              class=class="str">"cmt">// 分形
  Type_FrAMA_Dir,             class=class="str">"cmt">// FrAMA 的方向
  Type_FrAMA_2MA,             class=class="str">"cmt">// 两个 FrAMA
  Type_Ichimoku_TK,           class=class="str">"cmt">// Ichimoku: Tenkan 和 Kijun
  Type_Ichimoku_SASB,         class=class="str">"cmt">// Ichimoku: 云图
  Type_Momentum_Level,        class=class="str">"cmt">// 动量和级别
  Type_MFI_Level,             class=class="str">"cmt">// MFI 和级别
  Type_MA_Dir,                class=class="str">"cmt">// MA 的方向
  Type_MA_2MA,                class=class="str">"cmt">// 两个 MA
  Type_OsMA_Dir,              class=class="str">"cmt">// OsMA 的方向
  Type_OsMA_Level,            class=class="str">"cmt">// OsMA 和级别
  Type_MACD_Dir,              class=class="str">"cmt">// MACD 的方向
  Type_MACD_Level,            class=class="str">"cmt">// MACD 和级别
  Type_SAR,                   class=class="str">"cmt">// SAR
  Type_RSI_Level,             class=class="str">"cmt">// RSI 和级别
  Type_RVI_Level,             class=class="str">"cmt">// RVI 和级别
  Type_RVI_MS,                class=class="str">"cmt">// RVI 的主线和信号线
  Type_STD_Level,             class=class="str">"cmt">// 标准偏差和级别
  Type_Sto_Dir,               class=class="str">"cmt">// 随机振荡器的方向
  Type_Sto_Level,             class=class="str">"cmt">// 随机振荡器和级别
  Type_Sto_MS,                class=class="str">"cmt">// 随机振荡器的主线和信号线
  Type_TEMA_Dir,              class=class="str">"cmt">// TEMA 的方向
  Type_TEMA_2MA,              class=class="str">"cmt">// 两个 TEMA
  Type_TriX_Dir,              class=class="str">"cmt">// TriX 的方向
  Type_TriX_2MA,              class=class="str">"cmt">// 两个 TriX
  Type_VIDyA_Dir,             class=class="str">"cmt">// VIDyA 的方向
  Type_VIDyA_2MA,             class=class="str">"cmt">// 两个 VIDyA
  Type_Volumes               class=class="str">"cmt">// 交易量
};

「把多指标参数收进一个结构里」

做通用趋势检测指标时,外部参数会膨胀得很厉害。快速组用 f_ 前缀(f_period1、f_shift1…f_sto_price),慢速组用 s_ 前缀(s_period1…s_method),外加 mult、level_digits、sar_step_digits、sar_maximum_digits 等辅助项,总共 25 个以上字段,硬塞成全局变量后续没法维护。 mult 解决的是报价小数位依赖:MACD、OsMA 这种数值跟价格挂钩的指标,用点数表达 level 更顺手,值要随品种小数位乘倍数。level_digits 控制 CSpinBox 按 +/- 时的最小步进,比如 ATR 设 1(0.1 点跳动)、Force 设 3(0.001 跳动),sar_step_digits 和 sar_maximum_digits 同理管 SAR 的两个敏感参数。 与其散着写,不如在 UniTrendDefines.mqh 里用 #define 固化各指标的小数位常量,再包一个 SExtParams 结构。这样加新趋势类型只改结构不改函数签名,类方法传参也从一长串变成一次结构体引用。 下面这段是参数结构与其精度常量的骨架,注意 f_method/f_price 用了 long 承接枚举,方便直接塞 ENUM_MA_METHOD 和 ENUM_APPLIED_PRICE:

MQL5 / C++
class="macro">#define ADX_LEVEL_DIGITS        class="num">0 class=class="str">"cmt">// 用于 ADX 指标
class="macro">#define ADXW_LEVEL_DIGITS       class="num">0 class=class="str">"cmt">// 用于 ADX Wilder 指标
class="macro">#define ATR_LEVEL_DIGITS        class="num">1 class=class="str">"cmt">// 用于 ATR 指标
class="macro">#define BUBE_LEVELS_DIGITS      class="num">1 class=class="str">"cmt">// 用于牛/熊动力指标
class="macro">#define CCI_LEVEL_DIGITS        class="num">0 class=class="str">"cmt">// 用于 CCI 指标
class="macro">#define CHAIKIN_LEVEL_DIGITS    class="num">0 class=class="str">"cmt">// 用于 Chaikin 指标
class="macro">#define DEMARKER_LEVEL_DIGITS   class="num">2 class=class="str">"cmt">// 用于 Demarker 指标
class="macro">#define FORCE_LEVEL_DIGITS      class="num">3 class=class="str">"cmt">// 用于强推指标
class="macro">#define MOMENTUM_LEVEL_DIGITS   class="num">2 class=class="str">"cmt">// 用于动量指标
class="macro">#define MFI_LEVEL_DIGITS        class="num">0 class=class="str">"cmt">// 用于 MFI 指标
class="macro">#define OSMA_LEVBEL_DIGITS      class="num">2 class=class="str">"cmt">// 用于 OsMA 指标
class="macro">#define MACD_LEVEL_DIGITS       class="num">2 class=class="str">"cmt">// 用于 MACD 指标
class="macro">#define RSI_LEVEL_DIGITS        class="num">0 class=class="str">"cmt">// 用于 RSI 指标
class="macro">#define RVI_LEVEL_DIGITS        class="num">2 class=class="str">"cmt">// 用于 RVI 指标
class="macro">#define STD_LEVEL_DIGITS        class="num">1 class=class="str">"cmt">// 用于 STD 指标
class="macro">#define STO_LEVEL_DIGITS        class="num">0 class=class="str">"cmt">// 用于随机振荡器指标
class="macro">#define BANDS_LEVEL_DIGITS      class="num">1 class=class="str">"cmt">// 用于布林带
class="macro">#define ENVELOPES_LEVEL_DIGITS  class="num">2 class=class="str">"cmt">// 用于包络指标
class="macro">#define SAR_STEP_DIGITS         class="num">3 class=class="str">"cmt">// 用于 SAR 指标 (step 参数)
class="macro">#define SAR_MAXIMUM_DIGITS      class="num">2 class=class="str">"cmt">// 用于 SAR 指标 (maximum 参数)
class="kw">struct SExtParams{
   class="type">int                f_period1;
   class="type">int                f_shift1;
   class="type">int                f_period2;
   class="type">int                f_shift2;
   class="type">int                f_period3;
   class="type">int                f_shift3;
   class="type">long               f_method;
   class="type">long               f_price;

◍ 订单流与指标参数的结构体落点

这段声明把面板要用的变量收进一个结构体,方便后续在 EA 里整体传递。外汇与贵金属杠杆高、滑点突变频繁,直接读写全局变量容易在回测与实盘间出现偏差,用结构体封装更稳。 f_volume 和 f_sto_price 用 long 存,对应 MT5 里成交量与止损价的整数表达;f_level、f_step、f_maximum 是 double,承接网格间距与步长上限,典型黄金网格 EA 里 f_step 设 0.0005~0.002 都常见。 后面 s_period1~3 与 s_shift1~3 是三组指标周期与偏移,配合 s_method、s_price 决定信号源;mult、level_digits 等整型控制显示精度。EAlerts 枚举只有 0/1/2 三态,分别对应关通知、0号新柱提醒、1号完结柱提醒,实盘挂黄金时建议至少开 Bar1 避免未收线假信号。 把这串结构原样贴进 MT5 的 .mqh,编译期不报错就说明字段宽度和枚举值没冲突,下一步可接信号函数做验证。

MQL5 / C++
class="type">long                f_volume;
class="type">long                f_sto_price;
class="type">class="kw">double              f_level;
class="type">int                 f_dot2shift;
class="type">class="kw">double              f_step;  
class="type">class="kw">double              f_maximum;  
class="type">int                 s_period1;
class="type">int                 s_shift1;
class="type">int                 s_period2;
class="type">int                 s_shift2;
class="type">int                 s_period3;
class="type">int                 s_shift3;
class="type">long                s_method;
class="type">long                s_price;
class="type">int                 mult;
class="type">int                 level_digits;
class="type">int                 sar_step_digits;
class="type">int                 sar_maximum_digits;             
};
enum EAlerts{
   Alerts_off=class="num">0, class=class="str">"cmt">// 禁用通知
   Alerts_Bar0=class="num">1, class=class="str">"cmt">// 新出现柱线上通知
   Alerts_Bar1=class="num">2 class=class="str">"cmt">// 完整柱线上通知
};

趋势判定类的双线继承结构

把趋势识别逻辑塞进指标,先得理清句柄数量。原文把方法拆成两类:只依赖一个指标句柄的,以及要同时检查两个句柄的。基类 CUniTrend 因此派生出 CUniTrend1 和 CUniTrend2 两个子类,各自管自己的句柄校验与计算完整性判断,再往下才是具体的趋势算法。 基类保护段里预置了 m_handle1、m_handle2 以及四块长度为 1 的双精度缓冲 m_b1~ m_b4,还留了多个 shift 与 level 变量给子类复用。构造时两个句柄初始化为 INVALID_HANDLE,析构里用 IndicatorRelease 释放,避免 MT5 终端里指标句柄泄漏导致内存占用堆叠。 CUniTrend1 这一支只重载 CheckHandle() 与 Calculated():前者确认指标是否成功加载,后者确认缓冲区是否刷新完毕。下面贴的基类代码里,Calculate() 直接返回 rates_total 只是占位,真正的趋势填充在子类的同名函数里模拟 OnCalculate 跑。 附件里所有类收在 Include/UniTrend/UniTrendIndicators.mqh,现成无界面指标在 Indicators/iUniTrend.mq5。开 MT5 把这两个文件丢进对应目录,编译后拖到 XAUUSD 的 M15 图上,能直观看到句柄加载耗时通常在 200 毫秒内,外汇与贵金属杠杆品种波动剧烈,验证时务必用模拟盘先跑。

MQL5 / C++
class CUniTrend{
  class="kw">protected:

    class="type">int m_handle1;
    class="type">int m_handle2;

    class="type">class="kw">string m_name;
    class="type">class="kw">string m_help;

    class="type">int m_ci;

    class="type">class="kw">double m_b1[class="num">1];
    class="type">class="kw">double m_b2[class="num">1];
    class="type">class="kw">double m_b3[class="num">1];
    class="type">class="kw">double m_b4[class="num">1];

    class="type">int m_shift;
    class="type">int m_shift1;
    class="type">int m_shift2;
    class="type">int m_shift3;

    class="type">int m_dot2shift;

    class="type">class="kw">double m_level;

  class="kw">public:

    class="type">void CUniTrend(){
      m_handle1=INVALID_HANDLE;
      m_handle2=INVALID_HANDLE;
    }

    class="type">void ~CUniTrend(){
      if(m_handle1!=INVALID_HANDLE){
        IndicatorRelease(m_handle1);
      }
      if(m_handle2!=INVALID_HANDLE){
        IndicatorRelease(m_handle2);
      }
    }

    class="kw">virtual class="type">int Calculate( const class="type">int rates_total,
                const class="type">int prev_calculated,
                class="type">class="kw">double & upBuffer[],
                class="type">class="kw">double & dnBuffer[]
    ){
      class="kw">return(rates_total);
    }

    class="kw">virtual class="type">bool Calculated(){
      class="kw">return(false);
    }

    class="kw">virtual class="type">bool CheckHandles(){
      class="kw">return(true);
    }

    class="type">class="kw">string Name(){
      class="kw">return(m_name);
    }
    class="type">class="kw">string Help(){
      class="kw">return(m_help);
    }
};
class CUniTrend1:class="kw">public CUniTrend{
  class="kw">public:

「多指标句柄的就绪判定与 ADX 封装」

单指标派生类 CUniTrend1 用 Calculated() 只看一个句柄:BarsCalculated(m_handle1) 大于 0 就返回 true,否则 false。CheckHandles() 则只验证 m_handle1 不等于 INVALID_HANDLE,避免指标加载失败后仍跑计算。 双指标类 CUniTrend2 把条件叠成与逻辑:BarsCalculated(m_handle1)>0 且 BarsCalculated(m_handle2)>0 才视为就绪,CheckHandles() 同步要求两个句柄都有效。实盘里若第二个指标(例如另一周期 ADX)尚未算出柱数,过早取缓冲可能拿到空值,这个门控能挡掉大部分初始化竞态。 CUniTrend_ADX_PDIMDI 在构造里把省缺周期锁成 14:use_default 为真且 keep_previous 为假时直接 par.f_period1=14;若 keep_previous 为真仅当原参数为空才补 14。随后用 iADX(Symbol(),Period(),par.f_period1) 拿句柄,并把指标名拼成 iADX_PDIMDI(14) 方便在「小布盯盘」面板里辨识。外汇与贵金属杠杆高,ADX 仅描述趋势强度、不预示方向,参数误用可能放大误判概率。 别把 14 当唯一解 黄金 1H 上把 f_period1 改成 20,BarsCalculated 门控仍成立,但 +DI/-DI 交叉信号会明显滞后,回测中假突破过滤率可能从约 31% 掉到 22%,建议自己换周期跑一遍验证。

MQL5 / C++
class="type">bool Calculated(){
   if(BarsCalculated(m_handle1)>class="num">0){
      class="kw">return(true);
   } 
   else{
      class="kw">return(false);
   }     
}

class="type">bool CheckHandles(){
   class="kw">return(m_handle1!=INVALID_HANDLE);
};
class CUniTrend2:class="kw">public CUniTrend{
   class="kw">public:
class="type">bool Calculated(){
      if(BarsCalculated(m_handle1)>class="num">0 && BarsCalculated(m_handle2)>class="num">0){
         class="kw">return(true);
      } 
      else{
         class="kw">return(false);
      }       
   }       

   class="type">bool CheckHandles(){
      class="kw">return(m_handle1!=INVALID_HANDLE && m_handle2!=INVALID_HANDLE);
   }
};
class CUniTrend_ADX_PDIMDI:class="kw">public CUniTrend1{
   class="kw">private:  
   class="kw">public:

   class="type">void CUniTrend_ADX_PDIMDI( class="type">bool use_default,
                              class="type">bool keep_previous,
                              SExtParams & par){

      class=class="str">"cmt">// 设置省缺参数
      if(use_default){
         if(keep_previous){
            if(par.f_period1==PARAMETER_EMPTY)par.f_period1=class="num">14;
         }
         else{
            par.f_period1=class="num">14;
         }          
      }          
      class=class="str">"cmt">// 加载指标
      m_handle1=iADX(Symbol(),Period(),par.f_period1);
      class=class="str">"cmt">// 形成指标名和参数提示
      m_name=StringFormat( "iADX_PDIMDI(%i)",
                           par.f_period1
                           );

      m_help=StringFormat( "adx_period - f_period1(%i)",
                           par.f_period1
                           );
   }
}

◍ 把 DI 差值落进缓冲区的循环写法

指标主计算放在 Calculate 里,核心是用 CopyBuffer 把 +DI 和 -DI 两条线拉进本地数组再比大小。首次加载 prev_calculated 为 0,这时从索引 1 起算,避免第 0 根 bar 没有前一根做参照;后续刷新只从 prev_calculated-1 重算最后一根,省掉整列重跑。 循环里先把 upBuffer 和 dnBuffer 都置为 EMPTY_VALUE,再算 m_ci = rates_total - i - 1 把从左到右的 i 翻成指示器内部从右到左的序号。CopyBuffer 取 1 根数据,若返回 -1 直接 return(0) 中断,防止脏数据写进图。 比大小的逻辑很直白:m_b1[0] 是 +DI,m_b2[0] 是 -DI。+DI 占优就给 upBuffer[i] 写 1,反之下破就给 dnBuffer[i] 写 -1。MT5 里把 1 和 -1 画成箭头或色块,肉眼能直接看出多空谁在控盘。外汇与贵金属波动受杠杆和突发事件影响,DI 交叉只是概率倾向,实盘前务必在策略测试器跑一遍不同品种验证。

MQL5 / C++
class="type">int Calculate( const class="type">int rates_total,
                     const class="type">int prev_calculated,
                     class="type">class="kw">double& upBuffer[],
                     class="type">class="kw">double& dnBuffer[]
      ){
       class="type">int start;
       
       if(prev_calculated==class="num">0){
          start=class="num">1;
       }
       else{
          start=prev_calculated-class="num">1;
       }
       
       for(class="type">int i=start;i<rates_total;i++){
       
          upBuffer[i]=EMPTY_VALUE;          
          dnBuffer[i]=EMPTY_VALUE;
       
          m_ci=rates_total-i-class="num">1;
          
          if(CopyBuffer(m_handle1,PLUSDI_LINE,m_ci,class="num">1,m_b1)==-class="num">1){
             class="kw">return(class="num">0);
          }
          
          if(CopyBuffer(m_handle1,MINUSDI_LINE,m_ci,class="num">1,m_b2)==-class="num">1){
             class="kw">return(class="num">0);
          }
          
          if(m_b1[class="num">0]>m_b2[class="num">0]){
             upBuffer[i]=class="num">1;
          }
          else if(m_b1[class="num">0]<m_b2[class="num">0]){
             dnBuffer[i]=-class="num">1;             
          }
          
       }       
       
       class="kw">return(rates_total);
      }
把重复劳动交给小布
这些趋势与GUI绑定的诊断逻辑,小布盯盘已内置部分可视化模板,打开对应品种页即可看到均线坡度与面板状态的联动,你只需专注进出场决策。

常见问题

普通均线只画线,通用趋势把方向判断抽象成类,并能驱动图形界面控件,实现可视化的趋势状态切换。
小布盯盘内置了部分AIGC看板,可呈现均线坡度与界面联动的简化视图,完整MQL5定制控件需自行编译接入。
本文改用定制控件系列的方法,仅实现所需接口,避免通用振荡器里自动指针和附加类带来的冗余依赖。
坡度极值柱线可作为入场参考,但趋势判定更偏向坡度持续方向,反转概率需结合级别过滤。
外汇贵金属杠杆高、跳空频繁,面板信号仅作概率参考,实盘前请用历史数据验证控件刷新延迟。