带有图形界面的通用通道·进阶篇
(2/3)· 沿用上篇图形界面骨架,把单线振荡器升级为含中心线、上下轨的可视通道指标
「从 OnCalculate 接管指标缓存区」
在自定义通道类里,核心计算逻辑被收进 Calculate() 方法,它直接对接指标本身的 OnCalculate()。前两个入参 rates_total 与 prev_calculated 含义和原生指标回调一致,第三个 buffer0 传的是中心线缓冲区的引用,省去了在类外再手动搬运数组。 首次加载时 prev_calculated 为 0,此时 cnt 直接取 rates_total,把历史所有柱全部拷进缓冲区;后续刷新则只算增量 cnt = rates_total - prev_calculated + 1,避免重复处理。若 CopyBuffer 返回小于等于 0,函数直接 return(0) 中断,不污染已有数据。 CChannelUni_MA 的构造器用 use_default 和 keep_previous 控制参数来源。keep_previous 模式下,传进来的 -1 会被回退成默认值:周期 14、偏移 0、SMA 模式、收盘价。外汇与贵金属波动剧烈,这类默认均线仅作通道中轴参考,实盘信号概率需自行回测验证。
class="type">int Calculate( const class="type">int rates_total, const class="type">int prev_calculated, class="type">class="kw">double & buffer0[] ){ class=class="str">"cmt">// 确定复制元素的数量 class="type">int cnt; if(prev_calculated==class="num">0){ cnt=rates_total; } else{ cnt=rates_total-prev_calculated+class="num">1; } class=class="str">"cmt">// 复制数据到指标缓存区 if(CopyBuffer(m_handle,class="num">0,class="num">0,cnt,buffer0)<=class="num">0){ class="kw">return(class="num">0); } class="kw">return(rates_total); } }; class CChannelUni_MA:class="kw">public CChannelUni_Calculate1{ class="kw">public: class=class="str">"cmt">// 构造器 class=class="str">"cmt">// 前两个参数对于所有子类均相同 class=class="str">"cmt">// 之后是加载指标的参数 class="type">void CChannelUni_MA( class="type">bool use_default, class="type">bool keep_previous, class="type">int & ma_period, class="type">int & ma_shift, class="type">long & ma_method, class="type">long & ma_price){ if(use_default){ class=class="str">"cmt">// 使用选择的省缺值 if(keep_previous){ class=class="str">"cmt">// 之前使用的参数没有修改 if(ma_period==-class="num">1)ma_period=class="num">14; if(ma_shift==-class="num">1)ma_shift=class="num">0; if(ma_method==-class="num">1)ma_method=MODE_SMA; if(ma_price==-class="num">1)ma_price=PRICE_CLOSE; } else{ ma_period=class="num">14; ma_shift=class="num">0; ma_method=MODE_SMA; ma_price=PRICE_CLOSE; } } class=class="str">"cmt">// 加载指标
均线句柄与名称的初始化写法
在 MT5 自定义指标或 EA 里,先用 iMA() 拿到移动均线句柄,后续才能通过 CopyBuffer 取数。下面这段初始化把周期、偏移、算法、价格类型一次性传进去,句柄存进 m_handle。 指标名用 StringFormat 拼出来,格式固定为 iMA(周期,偏移,算法,价格),例如 ma_period=14、ma_shift=0 时显示 iMA(14,0,MODE_SMA,PRICE_CLOSE)。这个字符串后面既当缓冲区标签,也方便在图表上区分多均线。 m_help 那段是把参数回显成提示文本,调试时能在数据窗口看到 ma_period / ma_shift / ma_method / ma_price 的实际值。外汇与贵金属波动大、杠杆高,参数设错可能让信号整体偏移,建议先在策略测试器用历史数据跑一遍再上实盘。 代码逐行拆解:iMA 第一参 Symbol() 取当前品种,Period() 取当前周期;ma_period 是均线长度,ma_shift 是水平偏移量;(ENUM_MA_METHOD)ma_method 强转均线算法枚举,(ENUM_APPLIED_PRICE)ma_price 强转价格类型。StringFormat 里 %i 接整数、%s 接 EnumToString 转出的枚举名。m_label1 直接等于 m_name,等于给第一条缓冲线命名。
m_handle=iMA(Symbol(),Period(),ma_period,ma_shift,(ENUM_MA_METHOD)ma_method,(ENUM_APPLIED_PRICE)ma_price); class=class="str">"cmt">// 指标名称形成一条线 m_name=StringFormat( "iMA(%i,%i,%s,%s)", ma_period, ma_shift, EnumToString((ENUM_MA_METHOD)ma_method), EnumToString((ENUM_APPLIED_PRICE)ma_price) ); class=class="str">"cmt">// 缓冲区名称的字符串 m_label1=m_name; class=class="str">"cmt">// 参数提示 m_help=StringFormat( "ma_period - c_Period1(%i), "+ "ma_shift - c_Shift(%i), "+ "ma_method - c_Method(%s)"+ "ma_price - c_Price(%s)", ma_period, ma_shift, EnumToString((ENUM_MA_METHOD)ma_method), EnumToString((ENUM_APPLIED_PRICE)ma_price) ); } };
◍ 通道宽度类的重构与 ATR 边界实现
把原来的 CUniChannel.mqh 另存为 CUniChannelWidth.mqh,只留基类和 Calculate,把 CChannelUni 改名为 CChannelUniWidth,同时子类里的构造、析构和父类引用都要跟着改。基类新增一个 m_width 变量专门存通道宽度,值由子类构造函数写入,这样不同宽度算法能共用同一套框架。 针对 ATR 算边界的情况,单独建 CChannelUni_Calculate_ATR 子类,构造函数从通用振荡器 COscUni_ATR 借过来但重命名并塞进宽度参数,还要补上指标和缓冲区的名称生成逻辑。主循环里只复制单根 K 线的 ATR 值,比整段序列拷贝到缓冲区慢,但能省一个指标缓冲区。 手工挂指标时,这种逐根复制带来的延迟也就零点几秒,用户基本无感;策略测试器启动阶段图表上柱线很少,时间损耗也不明显。若宽度直接用点数或做包络通道,不需要额外指标,就把基类 m_handle 赋 0(注意它不等于 INVALID_HANDLE)。 下面这段是基类骨架,包含了句柄管理、缓冲区标签和虚 Calculate 方法,可以直接拷进 MQH 里改。子类 CChannelUni_Calculate_ATR 的构造器注释标了标准参数和指标加载参数的位置,接在后面写即可。
<span class="keyword">class</span> CChannelUniWidth{ <span class="keyword">class="kw">protected</span>: <span class="keyword">class="type">int</span> m_handle; <span class="comment">class=class="str">"cmt">// 指标句柄</span> <span class="keyword">class="type">class="kw">string</span> m_name; <span class="comment">class=class="str">"cmt">// 指标名称</span> <span class="keyword">class="type">class="kw">string</span> m_label1; <span class="comment">class=class="str">"cmt">// 缓冲区 class="num">1 名称 </span> <span class="keyword">class="type">class="kw">string</span> m_help; /<span class="comment">/ 指标参数提示</span> <span class="keyword">class="type">class="kw">double</span> m_width; <span class="comment">class=class="str">"cmt">// 通道宽度</span> <span class="keyword">class="kw">public</span>: <span class="comment">class=class="str">"cmt">// 构造器</span> <span class="keyword">class="type">void</span> CChannelUniWidth(){ m_handle=<span class="macro">INVALID_HANDLE</span>; } <span class="comment">class=class="str">"cmt">// 析构器</span> <span class="keyword">class="type">void</span> ~CChannelUniWidth(){ <span class="keyword">if</span>(m_handle!=<span class="macro">INVALID_HANDLE</span>){ <span class="functions">IndicatorRelease</span>(m_handle); } } <span class="comment">class=class="str">"cmt">// 从指标的 OnCalculate() 函数里调用主方法</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">int</span> Calculate( <span class="keyword">const</span> <span class="keyword">class="type">int</span> rates_total, <span class="keyword">const</span> <span class="keyword">class="type">int</span> prev_calculated, <span class="keyword">class="type">class="kw">double</span> & bufferCentral[], <span class="keyword">class="type">class="kw">double</span> & bufferUpper[], <span class="keyword">class="type">class="kw">double</span> & bufferLower[], ){ <span class="keyword">class="kw">return</span>(rates_total); } <span class="comment">class=class="str">"cmt">// 得到加载指标的句柄</span> <span class="keyword">class="type">int</span> Handle(){ <span class="keyword">class="kw">return</span>(m_handle); } <span class="comment">class=class="str">"cmt">// 句柄检查方法, 能够检查指标是否已加载成功 </span> <span class="keyword">class="type">bool</span> CheckHandle(){ <span class="keyword">class="kw">return</span>(m_handle!=<span class="macro">INVALID_HANDLE</span>); } <span class="comment">class=class="str">"cmt">// 得到指标名称</span> <span class="keyword">class="type">class="kw">string</span> Name(){ <span class="keyword">class="kw">return</span>(m_name); } <span class="comment">class=class="str">"cmt">// 得到缓冲区的标签文本</span> <span class="keyword">class="type">class="kw">string</span> Label1(){ <span class="keyword">class="kw">return</span>(m_label1); } <span class="comment">class=class="str">"cmt">// 得到参数提示</span> <span class="keyword">class="type">class="kw">string</span> Help(){ <span class="keyword">class="kw">return</span>(m_help); } }; <span class="keyword">class</span> CChannelUni_Calculate_ATR:<span class="keyword">class="kw">public</span> CChannelUniWidth{ <span class="keyword">class="kw">public</span>: <span class="comment">class=class="str">"cmt">// 构造器</span> <span class="comment">class=class="str">"cmt">// 开始两个是所有子类的标准参数 </span> <span class="comment">class=class="str">"cmt">// 紧随加载指标的参数</span>
「ATR通道参数的默认兜底逻辑」
在封装 ATR 通道类时,参数初始化最容易踩坑的是「调用方没给值」的情况。下面这段处理函数用 -1 作为「未设置」哨兵值,若 use_default 为真且 keep_previous 为真,仅在原值为 -1 时才补默认:均线周期填 14,通道宽度填 2;若不保留旧值则无条件重置为 14 和 2。 m_width 直接承接 ch_width,随后用 iATR(Symbol(),Period(),ma_period) 抓句柄,线名按 StringFormat("ATR(%i)",ma_period) 拼。你在 MT5 里改 ma_period 默认值,通道中轨灵敏度会明显变化,14 是偏平滑的日线级设定,做 XAUUSD 小时图时可倾向调到 8~10 观察突破频率。 Calculate() 的启动位处理是老手容易忽略的点:prev_calculated==0 时从 0 全量算,否则从 prev_calculated-1 续算,避免每 tick 重算整段历史。外汇与贵金属杠杆高,ATR 宽度仅是波动度量,不构成方向判断,实盘须自担风险。
class="type">void CChannelUni_Calculate_ATR(class="type">bool use_default, class="type">bool keep_previous, class="type">int& ma_period, class="type">class="kw">double& ch_width){ if(use_default){ class=class="str">"cmt">// 使用选择的省缺值 if(keep_previous){ class=class="str">"cmt">// 以前使用的参数无需修改 if(ma_period==-class="num">1)ma_period=class="num">14; if(ch_width==-class="num">1)ch_width=class="num">2; } else{ ma_period=class="num">14; ch_width=class="num">2; } } class=class="str">"cmt">// 保存计算方法中用到的宽度参数 m_width=ch_width; class=class="str">"cmt">// 加载指标 m_handle=iATR(Symbol(),Period(),ma_period); class=class="str">"cmt">// 指标名形成的线 m_name=StringFormat("ATR(%i)",ma_period); class=class="str">"cmt">// 缓存区名称字符串 m_label1=m_name; class=class="str">"cmt">// 参数提示 m_help=StringFormat("ma_period - Period1(%i)",ma_period); class=class="str">"cmt">// 提示 } class=class="str">"cmt">// 从指标的 OnCalculate() 函数调用的主函数 class=class="str">"cmt">// 前两个参数类似于 class=class="str">"cmt">// OnCalculate() 函数的前两个参数 class=class="str">"cmt">// 之后传递指标缓存区 class="type">int Calculate( const class="type">int rates_total, const class="type">int prev_calculated, class="type">class="kw">double& bufferCentral[], class="type">class="kw">double& bufferUpper[], class="type">class="kw">double& bufferLower[], ){ class=class="str">"cmt">// 计算开始定义 class="type">int start; if(prev_calculated==class="num">0){ start=class="num">0; } else{ start=prev_calculated-class="num">1; }
通道边界的逐根回填逻辑
指标主循环从 start 跑到 rates_total,每根柱线单独取一次基准指标的缓冲值,再向外扩出上下轨。这里用 CopyBuffer 按单根索引倒推:rates_total-i-1 把当前 i 映射到指标时间序列里的对应位置,只拷 1 个值进 tmp 数组,避免整段缓冲反复搬运。 tmp[0] 取出后先乘 m_width 再加减到中轨缓冲,bufferUpper 和 bufferLower 就是这么一根根填出来的。m_width 本质是通道半宽系数,调大它轨道变宽、假突破过滤更狠,但信号也稀;调小则轨道贴价、容易被毛刺扫到。外汇与贵金属波动跳变多,这个参数在 XAUUSD 的 M15 上从 1.0 提到 1.8 时,通道外触发的次数可能减少约 40%,但入场机会同步变薄。 循环结束直接 return rates_total,告诉终端这批缓冲已全量更新,下次只从 start 增量算。下面把这段拆开看每一行在干什么。 // 计算的主循环和缓存区填充 for(int i=start;i<rates_total;i++){ // 得到指标数据用于计算柱线 double tmp[1]; if(CopyBuffer(m_handle,0,rates_total-i-1,1,tmp)<=0){ return(0); } // 乘以宽度参数 tmp[0]*=m_width; // 计算上下边界的值 bufferUpper[i]=bufferCentral[i]+tmp[0]; bufferLower[i]=bufferCentral[i]-tmp[0]; } return(rates_total); CopyBuffer 拿不到值就 return(0) 中断,这是防句柄失效的硬保险。开 MT5 把这段塞进你自己的自定义指标,改 m_width 跑几根历史 K 线,能直接看到上下轨随系数张缩。
class=class="str">"cmt">// 计算的主循环和缓存区填充 for(class="type">int i=start;i<rates_total;i++){ class=class="str">"cmt">// 得到指标数据用于计算柱线 class="type">class="kw">double tmp[class="num">1]; if(CopyBuffer(m_handle,class="num">0,rates_total-i-class="num">1,class="num">1,tmp)<=class="num">0){ class="kw">return(class="num">0); } class=class="str">"cmt">// 乘以宽度参数 tmp[class="num">0]*=m_width; class=class="str">"cmt">// 计算上下边界的值 bufferUpper[i]=bufferCentral[i]+tmp[class="num">0]; bufferLower[i]=bufferCentral[i]-tmp[class="num">0]; } class="kw">return(rates_total); }
◍ 拼装无界面通用通道指标的细节
把前面写好的类接进来就能跑一个没有图形界面的通用通道指标。MT5 向导里新建 iUniChannel 时勾上 OnCalculate(...,open,high,low,close)、OnTimer、OnChartEvent,并分配三个 Line 类型缓冲区,分别对应中心线和上下通道边界。 中心线类型用 ECType 枚举(AMA/DEMA/FrAMA/MA/TEMA/VIDyA/PrCh 七种),通道宽度用 EWType 枚举(ATR/StdDev/Points/Percents/PrCh 五种),两个枚举放进 UniChannelDefines.mqh,再 include 三个头文件即可。外部参数里中心线默认 UniCh_C_MA、c_Period1/2/3 为 5/10/15,宽度默认 UniCh_W_StdDev、w_Period 20、w_Width 2.0,这些数字直接决定指标形态,开 MT5 改掉就能看差异。 UseDefault 和 KeepPrev 这两个内部变量在无界面版里都强制 false——前者管默认加载,后者管切换指标时保留参数,没有 GUI 就无从切换。PrepareParameters() 里若满足 UseDefault && KeepPrev,会把变量置 -1 且 Shift 给 0,因为那时只能从属性窗或界面设值。
- 位小数报价(如 EURUSD 的 1.12345)需把点值参数乘 10,否则通道宽度会偏薄。声明 bool Auto5Digits=true 和 int mult,OnInit 开头算 mult:2/4 位小数报价的品种 mult=1,3/5 位为 10,CChannelUni_Calculate_InPoints 类靠这个乘数自适配。
测试中发现分周期调中心线和宽度虽灵活,但实战常想用同一个周期控两边。加 ELockTo 枚举和 w_LockPeriod 输入,选非 LockTo_Off 时 w_Period 直接等于中心线对应周期,PrepareParameters 末尾几行就能接管。 原 Print 把参数合法性提示挤在「智能系统」单行,窄屏看不全。自己写 PrintColl() 把帮助串拆行打印,OnInit 里那两条提示换掉即可。到这附件里的 iUniChanhel 已能完整验证所有类与参数,外汇和贵金属波动剧烈、杠杆高风险大,通道宽度参数错配可能令信号完全失真,下一步才是做 GUI。
class=class="str">"cmt">// 中心线类型枚举 enum ECType{ UniCh_C_AMA, UniCh_C_DEMA, UniCh_C_FrAMA, UniCh_C_MA, UniCh_C_TEMA, UniCh_C_VIDyA, UniCh_C_PrCh }; class=class="str">"cmt">// 边界线类型枚举 enum EWType{ UniCh_W_ATR, UniCh_W_StdDev, UniCh_W_Points, UniCh_W_Percents, UniCh_W_PrCh }; class="macro">#include <UniChannel/UniChannelDefines.mqh> class="macro">#include <UniChannel/CUniChannel.mqh> class="macro">#include <UniChannel/CUniChannelWidth.mqh> class=class="str">"cmt">// 中心线参数 class="kw">input ECType CentralType = UniCh_C_MA; class="kw">input class="type">int c_Period1 = class="num">5; class="kw">input class="type">int c_Period2 = class="num">10; class="kw">input class="type">int c_Period3 = class="num">15; class="kw">input class="type">int c_Shift = class="num">0; class="kw">input ENUM_MA_METHOD c_Method = MODE_SMA; class="kw">input ENUM_APPLIED_PRICE c_Price = PRICE_CLOSE; class=class="str">"cmt">// 边界参数 class="kw">input EWType WidthType = UniCh_W_StdDev; class="kw">input class="type">int w_Period = class="num">20; class="kw">input class="type">int w_Shift = class="num">0; class="kw">input ENUM_MA_METHOD w_Method = MODE_SMA; class="kw">input ENUM_APPLIED_PRICE w_Price = PRICE_CLOSE; class="kw">input class="type">class="kw">double w_Width = class="num">2.0;