在视图内/外绘制通道(基础篇)
「在可见与不可见区间画通道」
MT5 自带的通道类指标通常只在可见 K 线范围内重绘,一旦图表滚动,历史区间的通道边界就会消失,回看形态很不方便。 要实现在视图内外都保留通道,核心是在 OnCalculate 里用全局坐标而非仅限可视窗的索引来算回归线。下面这段演示了用最小二乘拟合一条贯穿全部历史数据的直线通道,不随平移而截断。 外汇与贵金属杠杆高,这类通道只作结构参考,突破不代表趋势确认,需结合量价与多周期过滤。
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 里用。
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 做日志,否则极值点丢失时策略可能用旧值交易,外汇和贵金属品种波动快,这类静默失败风险偏高。
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 里编译。
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;