您应当知道的 MQL5 向导技术(第 40 部分):抛物线止损和反转(PSAR)·综合运用
「用回溯斜率框出 SAR 通道的上下轨」
把抛物线 SAR 的趋势方向和价格通道叠在一起,信号逻辑不算难懂,但真写进代码,复杂度往往比看图高出一个量级。先别急着拼形态,单独抽一个 Channel 函数,用回溯柱数定义通道上下限,后面接信号会干净很多。 很多人本能想用分形指标去抓通道的高低点,分形能用,但依赖它容易把解法绑死。更通适的做法是看整段回溯期的收盘价斜率——回溯周期直接复用均线周期,参数少,模型反而更扛不同品种。 拿到斜率后,先在回溯窗里找出最高点,再扫一遍其余高点,挑出第二个最贴合这条主斜率的高点;低点侧同理,找最低和次低中贴合斜率的那两个。四点连出两条线,通道就出来了。机械回溯的毛病是历史窗口常缺关键拐点,硬连容易出野线,所以上下轨各用两点延长,而不是单线拍死。 看涨时价格落在通道下半、SAR 翻多;看跌则价格在上半、SAR 翻空。判定半区很简单:取四条延长线在当前柱的预估值做平均。EURJPY 用输入 32、2023 年日线回测能跑出形态信号,外汇和贵金属波动剧烈,这套仅作概率参考,实盘前务必在 MT5 用历史数据自测。 下面这段 MQL5 是 Channel 函数核心:先建四个长度 2 的 vector 存高低价及索引,初值填当前 High(0)/Low(0);for 循环扫 m_ma_period 根 K 线更新极值;再算收盘价斜率,给上下轨尺度赋斜率绝对值,为后续找次高/次低点留底。 bool Channel(ENUM_POSITION_TYPE T) { vector _max,_max_i; vector _min,_min_i; _max.Init(2); _max.Fill(High(0)); _max_i.Init(2); _max_i.Fill(0.0); _min.Init(2); _min.Fill(Low(0)); _min_i.Init(2); _min_i.Fill(0.0); for(int i=0;i<m_ma_period;i++) { if(High(i) > _max[0]) { _max[0] = High(i); _max_i[0] = i; } if(Low(i) < _min[0]) { _min[0] = Low(i); _min_i[0] = i; } } double _slope = (Close(0) - Close(m_ma_period-1))/m_ma_period; double _upper_scale = fabs(_slope); double _lower_scale = fabs(_slope); for(int i=0;i<m_ma_period;i++) { if(i == _max_i[0])
class="type">bool Channel(class="type">ENUM_POSITION_TYPE T) { vector _max,_max_i; vector _min,_min_i; _max.Init(class="num">2); _max.Fill(High(class="num">0)); _max_i.Init(class="num">2); _max_i.Fill(class="num">0.0); _min.Init(class="num">2); _min.Fill(Low(class="num">0)); _min_i.Init(class="num">2); _min_i.Fill(class="num">0.0); for(class="type">int i=class="num">0;i<m_ma_period;i++) { if(High(i) > _max[class="num">0]) { _max[class="num">0] = High(i); _max_i[class="num">0] = i; } if(Low(i) < _min[class="num">0]) { _min[class="num">0] = Low(i); _min_i[class="num">0] = i; } } class="type">class="kw">double _slope = (Close(class="num">0) - Close(m_ma_period-class="num">1))/m_ma_period; class="type">class="kw">double _upper_scale = fabs(_slope); class="type">class="kw">double _lower_scale = fabs(_slope); for(class="type">int i=class="num">0;i<m_ma_period;i++) { if(i == _max_i[class="num">0])
◍ 峰谷斜率容差内的次高次低点捕获
这段逻辑在已定位的首个峰值 _max[0] 与谷值 _min[0] 之后,继续在 m_ma_period 根 K 线里扫描「同向且斜率接近」的次级拐点。它分别用 High(i) 和 Low(i) 计算相对于首拐点的局部斜率 _i_slope,只有当符号与基准 _slope 一致、且绝对偏差小于 _upper_scale / _lower_scale 时才记录次峰 _max[1] 或次谷 _min[1]。 斜率容差过滤是关键:若 _upper_scale 设得过小(如 0.0001 点/根),在 XAUUSD 的 5 分钟图上可能一整段趋势都捕不到次高点;放宽到 0.0005 则更容易抓到回撤中的二级结构,但伪信号概率会上升。外汇与贵金属杠杆高,这类结构识别仅作形态参考,实盘须自担风险。 扫描完高低两端后,代码初始化一个长度为 4 的 vector _projections,显然是为后续存放上下拐点的投影坐标做准备。开 MT5 把 _upper_scale、_lower_scale 和 m_ma_period 三个参数拉出来调一遍,能直接看到次拐点灵敏度变化。
{ class="kw">continue;
}
else
{ class="type">class="kw">double _i_slope = (High(i) - _max[class="num">0])/(i - _max_i[class="num">0]);
if((_i_slope > class="num">0.0 && _slope > class="num">0.0)||(_i_slope < class="num">0.0 && _slope < class="num">0.0))
{ if(fabs(_i_slope-_slope) < _upper_scale)
{ _max[class="num">1] = High(i);
_max_i[class="num">1] = i;
}
}
}
}
for(class="type">int i=class="num">0;i<m_ma_period;i++)
{ if(i == _min_i[class="num">0])
{ class="kw">continue;
}
else
{ class="type">class="kw">double _i_slope = (Low(i) - _min[class="num">0])/(i - _min_i[class="num">0]);
if((_i_slope > class="num">0.0 && _slope > class="num">0.0)||(_i_slope < class="num">0.0 && _slope < class="num">0.0))
{ if(fabs(_i_slope-_slope) < _lower_scale)
{ _min[class="num">1] = Low(i);
_min_i[class="num">1] = i;
}
}
}
}
vector _projections;
_projections.Init(class="num">4);投影均值与通道过滤的离场判定
上面这段逻辑把前两个极值点的斜率投影算出来,拼成一个四条线的 _projections 数组,再用它们的均值去做多空离场闸门。 _projections[0] 到 [3] 分别是两段区间的 max、min 按各自索引乘 _slope 后外推的位置;Mean() 取四者平均,相当于一条动态中轨。 多头持仓时,若最新收盘价同时低于 MA(m_ma_period) 收盘以及该投影均值,函数直接 return(true) 倾向触发平仓;空头则反过来,收盘价高于两者才放行。外汇与贵金属波动剧烈,这种双重低于/高于的过滤只能降低假信号概率,不保证出场在最优位。 Pattern_6 另走一路:买权看 Base(StartIndex()) 是否低于该 K 线 Low,卖权看是否高于 High,满足才交给我们前面写的 Channel(T) 再判一次,相当于用通道边界做二次确认。
_projections[class="num">0] = _max[class="num">0] + (_max_i[class="num">0]*_slope); _projections[class="num">1] = _min[class="num">0] + (_min_i[class="num">0]*_slope); _projections[class="num">2] = _max[class="num">1] + (_max_i[class="num">1]*_slope); _projections[class="num">3] = _min[class="num">1] + (_min_i[class="num">1]*_slope); if(T == POSITION_TYPE_BUY && Close(class="num">0) < Close(m_ma_period) && Close(class="num">0) < _projections.Mean()) { class="kw">return(true); } else if(T == POSITION_TYPE_SELL && Close(class="num">0) > Close(m_ma_period) && Close(class="num">0) > _projections.Mean()) { class="kw">return(true); } class="kw">return(class="kw">false); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check for Pattern class="num">6. | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CSignalSAR::IsPattern_6(class="type">ENUM_POSITION_TYPE T) { if(T == POSITION_TYPE_BUY && Base(StartIndex()) < Low(StartIndex())) { class="kw">return(Channel(T)); } else if(T == POSITION_TYPE_SELL && Base(StartIndex()) > High(StartIndex())) { class="kw">return(Channel(T)); } class="kw">return(class="kw">false); }
_projections[class="num">0] = _max[class="num">0] + (_max_i[class="num">0]*_slope); _projections[class="num">1] = _min[class="num">0] + (_min_i[class="num">0]*_slope); _projections[class="num">2] = _max[class="num">1] + (_max_i[class="num">1]*_slope); _projections[class="num">3] = _min[class="num">1] + (_min_i[class="num">1]*_slope); if(T == POSITION_TYPE_BUY && Close(class="num">0) < Close(m_ma_period) && Close(class="num">0) < _projections.Mean()) { class="kw">return(true); } else if(T == POSITION_TYPE_SELL && Close(class="num">0) > Close(m_ma_period) && Close(class="num">0) > _projections.Mean()) { class="kw">return(true); } class="kw">return(class="kw">false); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check for Pattern class="num">6. | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CSignalSAR::IsPattern_6(class="type">ENUM_POSITION_TYPE T) { if(T == POSITION_TYPE_BUY && Base(StartIndex()) < Low(StartIndex())) { class="kw">return(Channel(T)); } else if(T == POSITION_TYPE_SELL && Base(StartIndex()) > High(StartIndex())) { class="kw">return(Channel(T)); } class="kw">return(class="kw">false); }
「父类参数比手写更省事」
前面三篇已经拆了抛物线 SAR 的 7 种形态,剩下 3 种留到后续配合成交量背离、高周期倒置和 RSI 重叠再讲。这一节里最该动手的不是看信号,而是改你手里的代码:之前两篇示范里用了 'm_patterns_used' 这个重复参数,其实父类早就有 'm_patterns_usage',它直接管导入映射,能少写不少继承代码。 把那两篇附的 SignalWZ_40.mqh 和 wz_40.mq5(ZIP 里分别是 26.24 KB 和 8.39 KB)下下来,全局替换掉自定义信号类里的 'm_patterns_used',编译跑一遍,你会看到映射干净一截。外汇与贵金属波动剧烈,这类改动能省事但改错映射就是静默漏信号,务必在策略测试器用历史数据核对。 我们在做形态系列时,是冲着每个形态的理想阈值去优化的,这跟交易者凭经验手设阈值相反。多形态混用时阈值更难交叉验证,建议提前给各形态分配权重;单形态可容忍直接优化,前文已列过输入映射值。价格通道当信号类不常见,图表上肉眼好判上下轨,代码里判定则是另一回事,留待下回拆。