带有图形界面的通用趋势·进阶篇
(2/3)·传统趋势指标只给信号不给交互,这篇把均线坡度和GUI绑成一套可点选的系统
不少交易者把移动平均线坡度当趋势开关,却懒得把信号做成可视面板,结果每次切换周期都要肉眼比对。更麻烦的是,多数人以为趋势指标只能输出箭头,不知道它能直接驱动图形界面里的控件状态。本篇接着上一部分,把通用趋势的判断逻辑塞进可交互的窗体里。
指标枚举里藏着的分类逻辑
在 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 的阈值常数就能看到信号触发频率变化。
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:
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,编译期不报错就说明字段宽度和枚举值没冲突,下一步可接信号函数做验证。
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 毫秒内,外汇与贵金属杠杆品种波动剧烈,验证时务必用模拟盘先跑。
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%,建议自己换周期跑一遍验证。
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 交叉只是概率倾向,实盘前务必在策略测试器跑一遍不同品种验证。
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); }