在视图内/外绘制通道(基础篇)
📘

在视图内/外绘制通道(基础篇)

第 1/3 篇

「在可见与不可见区间画通道」

MT5 自带的通道类指标通常只在可见 K 线范围内重绘,一旦图表滚动,历史区间的通道边界就会消失,回看形态很不方便。 要实现在视图内外都保留通道,核心是在 OnCalculate 里用全局坐标而非仅限可视窗的索引来算回归线。下面这段演示了用最小二乘拟合一条贯穿全部历史数据的直线通道,不随平移而截断。 外汇与贵金属杠杆高,这类通道只作结构参考,突破不代表趋势确认,需结合量价与多周期过滤。

MQL5 / C++
class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">class="kw">datetime &time[], class="kw">const class="type">class="kw">double &open[], class="kw">const class="type">class="kw">double &high[], class="kw">const class="type">class="kw">double &low[], class="kw">const class="type">class="kw">double &close[], class="kw">const class="type">long &tick_volume[], class="kw">const class="type">long &volume[], class="kw">const class="type">int &spread[]) {
   class="type">int start = prev_calculated > class="num">0 ? prev_calculated - class="num">1 : class="num">0;
   class="type">class="kw">double sum_x = class="num">0, sum_y = class="num">0, sum_xy = class="num">0, sum_x2 = class="num">0;
   class="type">int n = rates_total;
   for(class="type">int i = class="num">0; i < n; i++) {
      sum_x += i;
      sum_y += close[i];
      sum_xy += i * close[i];
      sum_x2 += i * i;
   }
   class="type">class="kw">double slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x);
   class="type">class="kw">double intercept = (sum_y - slope * sum_x) / n;
   for(class="type">int i = start; i < rates_total; i++) {
      class="type">class="kw">double val = slope * i + intercept;
      PlotIndexSetDouble(class="num">0, i, val + class="num">0.001);
      PlotIndexSetDouble(class="num">1, i, val - class="num">0.001);
   }
   class="kw">return rates_total;
}

通道工具为什么没那么好画

通道常被看作仅次于移动平均线的主流分析工具,但真正在 MT5 客户端里把它画出来,远不是列个直线方程那么轻松。 表面看只是小学级别的直线方程,落地时却会撞上一堆麻烦:极值怎么设、怎么跟踪变动;通道中段刚好压在缺失的 K 线上怎么办;左极值在周五、右极值在周一,中间休市没柱,边界怎么算。 本篇先用标准类和面向对象两种方式,以三个指定极值实现通道绘制,并封装成指标。你可以直接把代码挂到图表上,验证边界在跳空处的取值逻辑。 外汇与贵金属杠杆高、跳空频繁,这类通道边界在休市段的插值仅作参考,实际触发概率随品种流动性变化。

◍ 用局部极值钉住通道锚点

通道在图表上至少得靠三个极值才能定型。这里说的极值,指函数在指定范围内的最大或最小值,达到极值的点就是极值点;取最小叫最小值点,取最大叫最大值点。数学上更严格的是局部极值:在最大值点或最小值点,函数值要大于或小于所有相邻点的值。 绘制通道真正要用的就是局部极值。图 1 里用红色水平线标了三个——两个最大值点、一个最小值点,矩形点只是示意。不是所有极值都值得标,只挑最关键的。对烛形或条形图来说,用“分形”来定义极值很顺手:当左右若干相邻柱体严格递减或递增时,那个转折点就是分形极值。 我们不做自动通道绘制工具,极值位置就按图 1 的方式人工定:在时间轴和价格轴上各占一个坐标。MT5 里最合适的载体是价格标签(Price Label)这种特殊图形对象,它自带时间和价格坐标属性,能把图表上的极值 unambiguous 地锁死,后续拖拽或回看都不会漂。 外汇与贵金属波动大、杠杆高,人工锚定极值时建议先在 M1~H1 多周期对照,避免把噪声分形当成通道支点。

「极值容器从 CObject 派生怎么写」

做价格极值分析,第一步是把每个拐点(时间+价格)装进一个可排序、可落盘的对象里。这里直接继承 MT5 标准库里的 CObject,少写一堆底层管理代码,只在比较逻辑和全局变量读写上自己做文章。 Compare 方法是列表排序的关键。它拿当前对象的日期时间和传入节点的日期时间比大小:_mode 大于 0 时走升序,当前时间更晚就返回 1,更早返回 -1;_mode 不大于 0 则反向。这意味着同一组极值,换个 _mode 就能在列表里从「老到新」翻成「新到老」。 持久化走全局变量而不是文件。SaveExtremum / LoadExtremum 接收两个变量名,分别存日期时间(转 double)和价格,成功返回 true。实盘里如果 EA 重启,靠这两个全局变量名就能把上一次的极值点拉回来接着算,不用重新扫历史。 下面这段代码是类声明加两个核心实现的原文,逐行拆一下能直接抄进 MT5 的 include 里用。

MQL5 / C++
class TExtremum : class="kw">public CObject
{
class="kw">private:
  class="type">class="kw">datetime  extr_datetime;          class=class="str">"cmt">// data/time in an extremum point
  class="type">class="kw">double    extr_price;             class=class="str">"cmt">// price in an extremum point
  
class="kw">protected:
  class="kw">virtual class="type">int  Compare(class="kw">const CObject* _node, class="type">int _mode = class="num">0) class="kw">const;
class="kw">public:
  class="type">void      TExtremum();            class=class="str">"cmt">// constructor
  class="type">void      ~TExtremum();           class=class="str">"cmt">// destructor
  class="type">void      SetExtremum(class="type">class="kw">datetime _time, class="type">class="kw">double _price);  class=class="str">"cmt">// change date/time and price in an extremum point
  class="type">void      SetDateTime(class="type">class="kw">datetime _time);                 class=class="str">"cmt">// change date/time in an extremum point
  class="type">void      SetPrice(class="type">class="kw">double _price);  class=class="str">"cmt">// change price in an extremum point
class="kw">public:
  class="type">class="kw">datetime  GetDateTime() class="kw">const;    class=class="str">"cmt">// get date/time in an extremum point
  class="type">class="kw">double    GetPrice() class="kw">const;       class=class="str">"cmt">// get price in an extremum point
class="kw">public:
  class="kw">virtual class="type">bool  SaveExtremum(class="type">class="kw">string _dt_name, class="type">class="kw">string _p_name);   class=class="str">"cmt">// save extremum
  class="kw">virtual class="type">bool  LoadExtremum(class="type">class="kw">string _dt_name, class="type">class="kw">string _p_name);   class=class="str">"cmt">// load extremum
  class="kw">virtual class="type">bool  DeleteExtremum(class="type">class="kw">string _dt_name, class="type">class="kw">string _p_name); class=class="str">"cmt">// class="kw">delete extremum
};
class=class="str">"cmt">//---------------------------------------------------------------------
class=class="str">"cmt">//  Comparing two extremums by time:
class=class="str">"cmt">//---------------------------------------------------------------------
class="type">int TExtremum::Compare(class="kw">const CObject* _node, class="type">int _mode = class="num">0) class="kw">const
{
  class="type">class="kw">datetime  temp = ((TExtremum* )_node).GetDateTime();
  class="type">class="kw">datetime  curr = GetDateTime();
  if(curr > temp)
  {
    class="kw">return(_mode > class="num">0 ? class="num">1 : -class="num">1);
  }
  else if(curr < temp)
  {
    class="kw">return(_mode > class="num">0 ? -class="num">1 : class="num">1);
  }
  class="kw">return(class="num">0);
}
class=class="str">"cmt">//---------------------------------------------------------------------
class=class="str">"cmt">//  Save extremum(date/time):
class=class="str">"cmt">//---------------------------------------------------------------------
class="type">bool TExtremum::SaveExtremum(class="type">class="kw">string _dt_name, class="type">class="kw">string _p_name)
{
  class="type">class="kw">datetime  dt_result = GlobalVariableSet(_dt_name, (class="type">class="kw">double)extr_datetime);
  class="type">class="kw">datetime  p_result = GlobalVariableSet(_p_name, (class="type">class="kw">double) extr_price);

用全局变量把极值塞回对象里

把跨周期或跨 EA 算出的极值带回本地对象,靠的是全局变量而非文件或参数传递。MQL5 里 GlobalVariableGet 能在不同程序间共享 double,时间点和价格各存一个变量名即可。 上面这段 LoadExtremum 先声明 dt_temp、p_temp 两个临时量,分别用 GlobalVariableGet 按传入的 _dt_name、_p_name 取数,result 用 &= 做短路式合并,两个都成功才继续。 若 result 非 false,就把 dt_temp 强转成 datetime 赋给 extr_datetime,p_temp 直接给 extr_price,返回 true;任一失败则返回 false。实盘里若全局变量被手动清掉,LoadExtremum 会静默返回 false,建议在调用处对 false 做日志,否则极值点丢失时策略可能用旧值交易,外汇和贵金属品种波动快,这类静默失败风险偏高。

MQL5 / C++
  if(dt_result != class="num">0 && p_result != class="num">0)
  {
    class="kw">return(true);
  }
  class="kw">return(class="kw">false);
}
class=class="str">"cmt">//---------------------------------------------------------------------
class=class="str">"cmt">//  Load extremum(date/time):
class=class="str">"cmt">//---------------------------------------------------------------------
class="type">bool TExtremum::LoadExtremum(class="type">class="kw">string _dt_name, class="type">class="kw">string _p_name)
{
  class="type">class="kw">double  dt_temp, p_temp;
  class="type">bool    result = GlobalVariableGet(_dt_name, dt_temp);
  result &= GlobalVariableGet(_p_name, p_temp);
  if(result != class="kw">false)
  {
    extr_datetime = (class="type">class="kw">datetime)dt_temp;
    extr_price = p_temp;
    class="kw">return(true);
  }
  class="kw">return(class="kw">false);
}

◍ 用链表类兜住多通道极值

想把非线性回归通道画多几条,极值的存储就不能写死数量。直接从标准 CList 继承出 TExtremumList,等于拿到一个按时间排队的极值容器,类型和个数都不受限。 主方法是 AddExtremum,塞进新极值后立刻按时间点排序。FindExtremum 则靠 GetFirstNode / GetNextNode 遍历,命中返回索引,没命中返回 -1;CList 内部那个当前指针,首次没调移动方法时默认指第一个节点,这点容易在调试时踩坑。 全局变量命名由 MakeDTimeName / MakePriceName 拼出,例如 "MainChannel_EURUSD_5_DTime_Extr1",对应通道名 MainChannel、品种 EURUSD、5 分钟图、编号 1。编号按时间升序从 1 起算,实质是排序后索引加 1。外汇与贵金属波动剧烈,这类通道仅作概率参考,实盘前务必在 MT5 用历史数据验证。 下面这段是类声明与三个核心方法的原文,逐行看能直接抄进 ExtremumClasses.mqh 里编译。

MQL5 / C++
class TExtremumList : class="kw">public CList
{
class="kw">private:
  class="type">class="kw">string              channel_prefix;      class=class="str">"cmt">// channel name(prefix)
  ENUM_TIMEFRAMES     chart_timeframe;     class=class="str">"cmt">// current timeframe
  class="type">class="kw">string              chart_symbol;       class=class="str">"cmt">// work symbols of the chart
class="kw">protected:
  class="type">class="kw">string  MakeDTimeName(class="type">int _nmb);     class=class="str">"cmt">// get name for saving/reading data/time of an extremum
  class="type">class="kw">string  MakePriceName(class="type">int _nmb);     class=class="str">"cmt">// get name for saving/reading price of an extremum
class="kw">public:
  class="type">void    TExtremumList();             class=class="str">"cmt">// конструктор
  class="type">void   ~TExtremumList();             class=class="str">"cmt">// деструктор
  class="type">void    SetChannelParams(class="type">class="kw">string _pref, class="type">class="kw">string _symbol = NULL, ENUM_TIMEFRAMES _curr_tf = PERIOD_CURRENT);
  class="type">void    AddExtremum(class="type">class="kw">datetime _time, class="type">class="kw">double  _price);
  class="type">void    DeleteAllExtremum();
  class="type">void    SaveExtremumList();
  class="type">void    LoadExtremumList();
  class="type">int     FindExtremum(class="type">class="kw">datetime _dt);  class=class="str">"cmt">// search extremum by specified time
class="kw">public:
  class="type">class="kw">datetime GetDateTime(class="type">int _index);
  class="type">class="kw">double   GetPrice(class="type">int _index);
};
class="type">void TExtremumList::AddExtremum(class="type">class="kw">datetime _time, class="type">class="kw">double  _price)
{
class=class="str">"cmt">//  Create extremum:
  TExtremum*     extr = new TExtremum();
  extr.SetExtremum(_time, _price);
class=class="str">"cmt">//  Add it in the list:
  Add(extr);
class=class="str">"cmt">//  Sort:
  Sort(class="num">1);
}
class="type">int TExtremumList::FindExtremum(class="type">class="kw">datetime _dt)
{
  class="type">int            k = class="num">0;
  TExtremum*     extr = (TExtremum*)(GetFirstNode());
  class="kw">while(extr != NULL)
  {
    if(extr.GetDateTime() == _dt)
    {
      class="kw">return(k);
    }
    extr = (TExtremum*)(GetNextNode());
  }
  class="kw">return(-class="num">1);                    class=class="str">"cmt">// extremum not found
}
class="type">class="kw">string TExtremumList::MakeDTimeName(class="type">int _nmb)
{
  class="type">class="kw">string  name;
  StringConcatenate(name, channel_prefix, "_", channel_symbol, "_", channel_timeframe, "_DTime_Extr", _nmb);
  class="kw">return(name);
}
class="type">class="kw">string TExtremumList::MakePriceName( class="type">int _nmb )
{
  class="type">class="kw">string  name;

常见问题

默认通道只绑定可见区间的锚点,超出视图后极值丢失。用局部极值容器把锚点存进全局变量,视图外也能续上。
遍历可见K线取最高/最低价作为锚点,写入派生自基础对象的极值容器,再回填到通道对象即可自动贴合。
可以。小布能直接读取品种页上的通道与极值数据,帮你列出每个通道的锚点价位和越界状态,省去手动核对。
用链表类兜住多通道极值,动态增删不固定大小,比写死数组稳妥,也方便回查某个通道的历史极值。
贵金属波动大、插针多,直接连收盘价易变形。建议用局部极值而非收盘价锚定,并留意外汇贵金属高风险,通道仅作概率参考。