带有图形用户界面的通用震荡指标·综合运用
🎛️

带有图形用户界面的通用震荡指标·综合运用

(3/3)· 当参数爆炸遇上 iCustom 的 64 上限,图形界面是唯一的体面解法

实战向进阶 第 3/3 篇
直接在导航窗拖指标、逐次改参数测震荡,半天只过三个品种。给通用指标套一层 GUI,切换和调参在图表上点几下就刷新,这才是批量筛信号的正确姿势。

「用基类与子类切分不同振荡指标的控件集」

做多指标切换面板时,数值输入走 incGUI 的 CSpinInputBox(带上下按钮的栏位),标准枚举走 CComBox 下拉框。外部参数上限按类型卡死:数值型最多 3 个,标准枚举最多 4 个。 表单类里先声明 3 个 CSpinInputBox 与 4 个 CComBox,并在 OnInitEvent() 用数组填充下拉项。例如价格枚举 e_price 含 7 个元素(PRICE_CLOSE 到 PRICE_WEIGHTED),方法枚举 e_method 含 4 个(SMA/EMA/SMMA/LWMA),循环调用 AddItem(EnumToString(...)) 即可批量灌入。 不同指标要显示的控件组合不同,所以抽一个基类 CUniOscControls 管指针与公共事件。SetPointers() 把 7 个控件地址存进类成员,Hide() 一键全隐,Event() 依次转发 7 个控件的事件,任一返回非 0 就向上报 1。 子类只重写虚函数:ATR 最简单,InitControls() 只给 m_value1 标 "ma_period",FormHeight() 返回 70;Stochastic 最重,3 个数值框加方法、价格枚举共 5 个控件,Show() 里 y 坐标按 20 像素步进(0/20/40/60/80),FormHeight() 返回 150。改旁边文字只能走 Init() 而非独立 setter,这是库的限制。 表单类持有一个 CUniOscControls* m_controls,析构里 delete,再由 SetType() 按当前指标 new 出对应子类。开 MT5 把这段基类抄进 EA,接 incGUI 后就能动态换指标面板。

MQL5 / C++
class CUniOscControls{
   class="kw">protected:
      CSpinInputBox *m_value1;
      CSpinInputBox *m_value2;
      CSpinInputBox *m_value3;
      CComBox *m_price;
      CComBox *m_method;
      CComBox *m_volume;
      CComBox *m_sto_price;
   class="kw">public:
   class="type">void SetPointers(CSpinInputBox &value1, CSpinInputBox &value2, CSpinInputBox &value3, CComBox &price, CComBox &method, CComBox &volume, CComBox &sto_price){
      m_value1=GetPointer(value1);
      m_value2=GetPointer(value2);
      m_value3=GetPointer(value3);
      m_price=GetPointer(price);
      m_method=GetPointer(method);
      m_volume=GetPointer(volume);
      m_sto_price=GetPointer(sto_price);
   }
   class="type">void Hide(){
      m_value1.Hide(); m_value2.Hide(); m_value3.Hide();
      m_price.Hide(); m_method.Hide(); m_volume.Hide(); m_sto_price.Hide();
   }
   class="type">int Event(class="type">int id, class="type">long lparam, class="type">class="kw">double dparam, class="type">class="kw">string sparam){
      class="type">int e1=m_value1.Event(id,lparam,dparam,sparam);
      class="type">int e2=m_value2.Event(id,lparam,dparam,sparam);
      class="type">int e3=m_value3.Event(id,lparam,dparam,sparam);
      class="type">int e4=m_price.Event(id,lparam,dparam,sparam);
      class="type">int e5=m_method.Event(id,lparam,dparam,sparam);
      class="type">int e6=m_volume.Event(id,lparam,dparam,sparam);
      class="type">int e7=m_sto_price.Event(id,lparam,dparam,sparam);
      if(e1!=class="num">0||e2!=class="num">0||e3!=class="num">0||e4!=class="num">0||e5!=class="num">0||e6!=class="num">0||e7!=class="num">0) class="kw">return(class="num">1);
      class="kw">return(class="num">0);
   }
   class="kw">virtual class="type">void InitControls(){}
   class="kw">virtual class="type">void Show(class="type">int x, class="type">int y){}
   class="kw">virtual class="type">int FormHeight(){ class="kw">return(class="num">0); }
};

class CUniOscControls_ATR: class="kw">public CUniOscControls{
   class="type">void InitControls(){ m_value1.Init("value1",SPIN_BOX_WIDTH,class="num">1,"ma_period"); }
   class="type">void Show(class="type">int x, class="type">int y){ m_value1.Show(x,y); }
   class="type">int FormHeight(){ class="kw">return(class="num">70); }
};

class CUniOscControls_Stochastic: class="kw">public CUniOscControls{
   class="type">void InitControls(){
      m_value1.Init("value1",SPIN_BOX_WIDTH,class="num">1,"Kperiod");
      m_value2.Init("value2",SPIN_BOX_WIDTH,class="num">1,"Dperiod");
      m_value3.Init("value3",SPIN_BOX_WIDTH,class="num">1,"slowing");
   }
   class="type">void Show(class="type">int x, class="type">int y){
      m_value1.Show(x,y);
      m_value2.Show(x,y+class="num">20);
      m_value3.Show(x,y+class="num">40);
      m_method.Show(x,y+class="num">60);
      m_sto_price.Show(x,y+class="num">80);
   }
   class="type">int FormHeight(){ class="kw">return(class="num">150); }
};

◍ 震荡指标面板的动态装配与事件接管

在 MT5 自定义指标里做统一参数面板,核心是一个 SetType() 方法:进来先检查 m_controls 指针是否为动态对象,是就 delete 掉并置 NULL,再根据传入的 type 分支 new 出对应指标的控件类(ATR、RSI、MACD 等 16 种都在 switch 里覆盖)。 new 完立刻调 SetPointers() 把数值框、价格枚举等控件地址绑进去,再 InitControls() 初始化。随后对三个 SpinBox 解除只读并设最小值为 1,最后用 m_controls.FormHeight() 把面板高度存进 m_Height。 [CODE]void SetType( long type){          if ( CheckPointer (m_controls)== POINTER_DYNAMIC ){              delete (m_controls);             m_controls= NULL ;          }                   switch ((EOscUniType)type){              case OscUni_ATR:                m_controls= new CUniOscControls_ATR();              break ;              case OscUni_BearsPower:                m_controls= new CUniOscControls_BearsPower();              break ;              case OscUni_BullsPower:                m_controls= new CUniOscControls_BullsPower();              break ;              case OscUni_CCI:                m_controls= new CUniOscControls_CCI();              break ;              case OscUni_Chaikin:                m_controls= new CUniOscControls_Chaikin();              break ;              case OscUni_DeMarker:                m_controls= new CUniOscControls_DeMarker();              break ;              case OscUni_Force:                m_controls= new CUniOscControls_Force();              break ;              case OscUni_Momentum:                m_controls= new CUniOscControls_Momentum();              break ;              case OscUni_MACD:                m_controls= new CUniOscControls_MACD();              break ;              case OscUni_OsMA:                m_controls= new CUniOscControls_OsMA();              break ;              case OscUni_RSI:                m_controls= new CUniOscControls_RSI();              break ;              case OscUni_RVI:                m_controls= new CUniOscControls_RVI();              break ;              case OscUni_Stochastic:                m_controls= new CUniOscControls_Stochastic();              break ;              case OscUni_TriX:                m_controls= new CUniOscControls_TriX();              break ;              case OscUni_WPR:                m_controls= new CUniOscControls_WPR();              break ;          }                   m_controls.SetPointers(m_value1,m_value2,m_value3,m_price,m_method,m_volume,m_sto_price);          m_controls.InitControls();                   m_value1.SetReadOnly( false );          m_value2.SetReadOnly( false );          m_value3.SetReadOnly( false );                   m_value1.SetMinValue( 1 );                  m_value2.SetMinValue( 1 );          m_value3.SetMinValue( 1 );                   m_Height=m_controls.FormHeight();                        }[/CODE] 逐行拆解:第 1 行定义接收 long 型 type 的 SetType;第 2–5 行用 CheckPointer 判动态指针,是则释放旧控件防内存泄漏;第 7 行起 switch 把 16 个枚举映射到具体控件类;第 39 行 SetPointers 绑定 7 个 UI 元素地址;第 40 行 InitControls 做实际布局;第 42–47 行三个 SetReadOnly(false) 允许键盘输入,SetMinValue(1) 限定下限;末行 FormHeight() 回写面板高度。 显示隐藏交给 OnShowEvent / OnHideEvent,里面分别 Show/Hide 下拉框与 m_controls,坐标偏移写死 10 和 30 像素。要在图表事件里让面板响应,得在 OnChartEvent() 加一句 int ce=frm.m_controls.Event(id,lparam,dparam,sparam); 把系统事件透传进去。 OnInit() 里先 frm.SetSelectedIndex() 再 frm.SetType(_Type) 决定装载哪类指标。装完后调 SetValues() 把 _Period1 等 7 个参数写回 UI:SpinBox 直接 SetValue,枚举类靠遍历 e_price / e_method 等数组匹配索引再 SetSelectedIndex。外汇与贵金属市场波动剧烈、杠杆风险高,这类面板仅降低调参摩擦,不预示任何方向。

MQL5 / C++
class="type">void SetType( class="type">long type){          if ( CheckPointer(m_controls)== POINTER_DYNAMIC ){              class="kw">delete (m_controls);             m_controls= NULL ;          }                   class="kw">switch ((EOscUniType)type){              case OscUni_ATR:                m_controls= new CUniOscControls_ATR();              class="kw">break ;              case OscUni_BearsPower:                m_controls= new CUniOscControls_BearsPower();              class="kw">break ;              case OscUni_BullsPower:                m_controls= new CUniOscControls_BullsPower();              class="kw">break ;              case OscUni_CCI:                m_controls= new CUniOscControls_CCI();              class="kw">break ;              case OscUni_Chaikin:                m_controls= new CUniOscControls_Chaikin();              class="kw">break ;              case OscUni_DeMarker:                m_controls= new CUniOscControls_DeMarker();              class="kw">break ;              case OscUni_Force:                m_controls= new CUniOscControls_Force();              class="kw">break ;              case OscUni_Momentum:                m_controls= new CUniOscControls_Momentum();              class="kw">break ;              case OscUni_MACD:                m_controls= new CUniOscControls_MACD();              class="kw">break ;              case OscUni_OsMA:                m_controls= new CUniOscControls_OsMA();              class="kw">break ;              case OscUni_RSI:                m_controls= new CUniOscControls_RSI();              class="kw">break ;              case OscUni_RVI:                m_controls= new CUniOscControls_RVI();              class="kw">break ;              case OscUni_Stochastic:                m_controls= new CUniOscControls_Stochastic();              class="kw">break ;              case OscUni_TriX:                m_controls= new CUniOscControls_TriX();              class="kw">break ;              case OscUni_WPR:                m_controls= new CUniOscControls_WPR();              class="kw">break ;          }                   m_controls.SetPointers(m_value1,m_value2,m_value3,m_price,m_method,m_volume,m_sto_price);          m_controls.InitControls();                   m_value1.SetReadOnly( class="kw">false );          m_value2.SetReadOnly( class="kw">false );          m_value3.SetReadOnly( class="kw">false );                   m_value1.SetMinValue( class="num">1 );                  m_value2.SetMinValue( class="num">1 );          m_value3.SetMinValue( class="num">1 );                   m_Height=m_controls.FormHeight();                        }

把 GUI 和震荡指标拧成一体

震荡指标类与 GUI 类都就位后,下一步是把两者在事件循环里接起来。OnChartEvent 里先让表单处理基础交互,再分别捕获主列表的指标切换事件(me)和参数面板的改动事件(ce),二者返回 1 即代表用户做了选择。 [CODE]void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam){ int e=frm.Event(id,lparam,dparam,sparam); if(e==1){ int win=ChartWindowFind(0,ShortName); ChartIndicatorDelete(0,win,ShortName); ChartRedraw(); } int me=frm.m_cmb_main.Event(id,lparam,dparam,sparam); int ce=frm.m_controls.Event(id,lparam,dparam,sparam); }[/CODE] 上面这段逐行看:frm.Event 处理窗体自身事件,e==1 时删掉旧指标窗口并重绘;m_cmb_main.Event 抓主下拉框事件存入 me;m_controls.Event 抓参数控件事件存入 ce。 切换指标时,me==1 会删旧对象、按 SelectedIndex 载入新类型、重设短名并 Refresh 表单,最后 EventSetMillisecondTimer(100) 拉起 100 毫秒定时器。参数改动 ce==1 的逻辑类似,但先校验 SpinBox 值必须 >0、下拉索引不等于 -1,再写回 _Period1~3、_MaMethod 等全局变量后重载指标。 指标计算不实时,所以定时器里用 BarsCalculated(osc.Handle())>0 判定就绪,再调 osc.Calculate(Bars(Symbol(),Period()),0,Label1Buffer,Label2Buffer) 做全量重算,成功后 ChartRedraw 并 EventKillTimer 关闭定时器。 想让指标脱离 GUI 也能跑,加 input bool UseGUI=true,仅当为真时在 OnInit 里 new CUniOscForm 并初始化下拉项与位置。另可挂 incGUI 的配色枚举,外部多出 ColorScheme 下拉,OnInit 开头 ClrScheme.SetScheme(ColorScheme) 即可切换 7 套皮肤(Default 到 AquaBlack)。外汇与贵金属图表加载自定义指标存在脚本错误导致图表卡死的高风险,建议先在模拟账户 MT5 按上面事件分支逐条打印验证。

MQL5 / C++
class="type">void OnChartEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam){
   class="type">int e=frm.Event(id,lparam,dparam,sparam);
   if(e==class="num">1){
      class="type">int win=ChartWindowFind(class="num">0,ShortName);
      ChartIndicatorDelete(class="num">0,win,ShortName);
      ChartRedraw();
   }
   class="type">int me=frm.m_cmb_main.Event(id,lparam,dparam,sparam);
   class="type">int ce=frm.m_controls.Event(id,lparam,dparam,sparam);
}

「改参即见形的直观验证缺口」

把外部参数接到指标绘制逻辑后,拖动参数滑块时图形会实时重绘,这种即时反馈在标准属性窗口里拿不到。 属性窗口只能改色改线宽,动不了算法层的输入变量,因此你调周期、平滑系数时,肉眼看不到外观随参数漂移的过程。 开 MT5 自己写个带外部参数重绘的脚本,改一处输入值看一次图,比静态读文档更能建立参数敏感度的直觉。外汇与贵金属波动剧烈,参数误用会放大误判风险。

◍ 一点提醒

这套通用振荡器附带的文件分工很明确:UniOscDefines.mqh 管枚举类型,CUniOsc.mqh 是核心类,iUniOsc.mq5 与 iUniOscGUI.mq5 分别对应无界面和有界面版本,IncGUI_v4.mqh 则负责图形对象操作。注意库版本有混淆点——代码库里的 IncGUI_v4 比文章内同名文件多了更新过的 CTable 表格类,直接覆盖可能改掉你已有的界面逻辑。 下面这段句柄代码暴露了多符号调用的坑:前 three 行写死 Symbol() 和 Period(),在单图表 EA 里没问题,但丢进多符号 EA 时只会返回当前图表句柄。改成用 iCustom 传入的 pSymbol、pPeriod 参数,才能真正拿到指定品种和周期的 ATR、BearsPower、DeMarker 句柄。 外汇和贵金属波动剧烈、杠杆风险高,这类自定义指标只帮你识别振荡结构,信号失效是常态。把句柄参数改对,再去 MT5 多品种回测一遍,比盲目跟代码更重要。

MQL5 / C++
m_handle=iATR(Symbol(),Period(),ma_period);
m_handle=iBearsPower(Symbol(),Period(),ma_period);
m_handle=iDeMarker(Symbol(),Period(),ma_period);
pSymbol class=class="str">"cmt">// 来自 iCustom 的参数符号变量
pPeriod class=class="str">"cmt">// 来自 iCustom 的参数周期变量
m_handle=iATR(pSymbol,pPeriod,ma_period);
m_handle=iBearsPower(pSymbol,pPeriod,ma_period);
m_handle=iDeMarker(pSymbol,pPeriod,ma_period);
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到当前震荡类指标的聚合状态,把重复劳动交给小布,你专注决策。

常见问题

用动态控件组按子类构造函数提示生成,选中指标后重建面板,只暴露该指标实际需要的参数,避开 iCustom 参数数量上限。
可以,小布品种页支持自定义指标叠加,你把编译好的通用指标挂上,它会在盘口侧展示参数切换后的实时形态,不必手动来回拖。
若需图形界面内动态调用且参数少,用 IndicatorCreate 更稳;要兼容老 EA 逻辑则走 iCustom,但注意 64 参数含品种名与指标名后的实际余量。
倾向友好,新震荡指标只需写子类构造函数传参,基类计算与 GUI 逻辑复用,扩展成本主要在控件映射而非架构改动。