价格行为分析工具包开发(第十三部分):RSI 哨兵工具·进阶篇
(2/3)·手动扫背离又慢又漏?这篇进阶 EA 把 RSI 常规与隐藏背离画成箭头
「摆动低点上的两类牛背离判定」
在 RSI 背离扫描里,牛侧逻辑只处理 swingLows 数组长度 ≥ 2 的情况。数组经 ArraySort 升序后,索引 0 为最近摆动低点、索引 1 为前一处,两点的 lowBuffer 与 rsiBuffer 差值即构成判定基础。 常规牛背离要求价格走出更低低点(lowBuffer[recent] < lowBuffer[previous]),而 RSI 反而更高(rsiBuffer[recent] > rsiBuffer[previous]),且 RSI 差值不小于 InpMinRSIDiff。若开启 InpUseRSIThreshold,还会额外限定前一处 RSI 必须 ≤ InpRSIOversold,才允许 Print 与 DisplaySignal 输出 RegBullish Divergence。 隐藏牛背离走 else 分支:价格更高低点(lowBuffer[recent] > lowBuffer[previous])配合 RSI 更低低点,RSI 差值同样需跨过 InpMinRSIDiff 门槛,信号标记 HiddenBullish Divergence。外汇与贵金属杠杆高,背离仅示概率倾向,实盘须自验。 IsSignificantSwingLow 用 left/right 窗口核验局部最低:左右任一端出现比 currentLow 低且偏离超 InpMinSwingDiffPct% 的 bar 即判非显著低点。以 0.1% 的最小摆动阈值跑 EURUSD M5,显著摆动低点数量可能比 0.5% 设定多出 3~4 倍,直接影响背离信号密度。
if(ArraySize(swingLows) >= class="num">2) { ArraySort(swingLows); class=class="str">"cmt">// ascending order: index class="num">0 is most recent class="type">int recent = swingLows[class="num">0]; class="type">int previous = swingLows[class="num">1]; class=class="str">"cmt">// Regular Bullish Divergence: Price makes a lower low class="kw">while RSI makes a higher low if(lowBuffer[recent] < lowBuffer[previous] && rsiBuffer[recent] > rsiBuffer[previous] && (rsiBuffer[recent] - rsiBuffer[previous]) >= InpMinRSIDiff) { class=class="str">"cmt">// Optionally require the earlier swing&class="macro">#x27;s RSI be oversold if(!InpUseRSIThreshold || rsiBuffer[previous] <= InpRSIOversold) { Print("Regular Bullish Divergence detected at bar ", recent); DisplaySignal("RegBullish Divergence", recent); } } class=class="str">"cmt">// Hidden Bullish Divergence: Price makes a higher low class="kw">while RSI makes a lower low else if(lowBuffer[recent] > lowBuffer[previous] && rsiBuffer[recent] < rsiBuffer[previous] && (rsiBuffer[previous] - rsiBuffer[recent]) >= InpMinRSIDiff) { Print("Hidden Bullish Divergence detected at bar ", recent); DisplaySignal("HiddenBullish Divergence", recent); } } class="type">bool IsSignificantSwingLow(class="type">int index, class="type">int left, class="type">int right) { class="type">class="kw">double currentLow = lowBuffer[index]; class=class="str">"cmt">// Check left side for a local minimum condition for(class="type">int i = index - left; i < index; i++) { if(i < class="num">0) class="kw">continue; class="type">class="kw">double pctDiff = MathAbs((lowBuffer[i] - currentLow) / currentLow) * class="num">100.0; if(lowBuffer[i] < currentLow && pctDiff > InpMinSwingDiffPct) class="kw">return class="kw">false; } class=class="str">"cmt">// Check right side for a local minimum condition for(class="type">int i = index + class="num">1; i <= index + right; i++) { if(i >= g_totalBars) class="kw">break; class="type">class="kw">double pctDiff = MathAbs((lowBuffer[i] - currentLow) / currentLow) * class="num">100.0; if(lowBuffer[i] < currentLow && pctDiff > InpMinSwingDiffPct) class="kw">return class="kw">false; } class="kw">return true; }
显著摆动高点判定与信号去重绘制
显著摆动高点的识别不是简单看左右两根 K 线更低,而是用百分比阈值过滤噪音。函数 IsSignificantSwingHigh 以当前柱 highBuffer[index] 为基准,向左回溯 left 根、向右前瞻 right 根,若任一侧出现高于当前高点且偏离幅度超过 InpMinSwingDiffPct(百分比)的柱,就直接返回 false,判定失败。 左端循环里先用 i<0 做边界保护,避免历史不足时数组越界;右端则用 i>=g_totalBars 跳出,两者都依赖外部传入的图表总柱数。pctDiff 的计算方式是 MathAbs((currentHigh - highBuffer[i]) / currentHigh) * 100.0,这意味着阈值参数直接决定摆动的「显著」程度——设 0.1 可能在大周期黄金上频繁触发,设 0.5 则只抓大级别拐点。 DisplaySignal 负责把信号画到图上,但先做了同柱去重:遍历已有 signals 数组,若类型包含相同 signalText 且柱距小于 InpMinBarsBetweenSignals 就直接 return。这一步能压住震荡市里的重复箭头噪音。 对于带 "Reg" 字样的常规信号,代码在左上角(距边 10、20 像素)创建名为 LatestSignal 的 OBJ_LABEL 文本对象,颜色白。若 ObjectCreate 失败会 Print 报错并返回,实盘加载后可在 MT5 终端日志里抓这类创建异常。外汇与贵金属波动剧烈、杠杆风险高,任何信号都只是概率参考,需结合仓位管理。
class="type">bool IsSignificantSwingHigh(class="type">int index, class="type">int left, class="type">int right) { class="type">class="kw">double currentHigh = highBuffer[index]; for(class="type">int i = index - left; i < index; i++) { if(i < class="num">0) class="kw">continue; class="type">class="kw">double pctDiff = MathAbs((currentHigh - highBuffer[i]) / currentHigh) * class="num">100.0; if(highBuffer[i] > currentHigh && pctDiff > InpMinSwingDiffPct) class="kw">return class="kw">false; } for(class="type">int i = index + class="num">1; i <= index + right; i++) { if(i >= g_totalBars) class="kw">break; class="type">class="kw">double pctDiff = MathAbs((currentHigh - highBuffer[i]) / currentHigh) * class="num">100.0; if(highBuffer[i] > currentHigh && pctDiff > InpMinSwingDiffPct) class="kw">return class="kw">false; } class="kw">return true; } class="type">void DisplaySignal(class="type">class="kw">string signalText, class="type">int barIndex) { for(class="type">int i = class="num">0; i < ArraySize(signals); i++) { if(StringFind(signals[i].type, signalText) != -class="num">1) if(MathAbs(signals[i].barIndex - barIndex) < InpMinBarsBetweenSignals) class="kw">return; } if(StringFind(signalText, "Reg") != -class="num">1) { class="type">class="kw">string labelName = "LatestSignal"; if(ObjectFind(class="num">0, labelName) == -class="num">1) { if(!ObjectCreate(class="num">0, labelName, OBJ_LABEL, class="num">0, class="num">0, class="num">0)) { Print("Failed to create LatestSignal label"); class="kw">return; } ObjectSetInteger(class="num">0, labelName, OBJPROP_CORNER, class="num">0); ObjectSetInteger(class="num">0, labelName, OBJPROP_XDISTANCE, class="num">10); ObjectSetInteger(class="num">0, labelName, OBJPROP_YDISTANCE, class="num">20); ObjectSetInteger(class="num">0, labelName, OBJPROP_COLOR, clrWhite); }
◍ 给信号挂箭头并记账
这一段负责把判定的形态在图上画出来,同时把信号塞进数组留待后续统计。箭头用 OBJ_ARROW 对象,Wingdings 码 233 画向上箭头、234 画向下箭头,偏移量由外部参数 InpArrowOffset 乘上 SYMBOL_POINT 决定,避免箭头压在 K 线上。 多头信号箭头落在 lowBuffer[barIndex] 下方,空头落在 highBuffer[barIndex] 上方,颜色分别用 clrLime 和 clrRed。创建前先用 ObjectFind 查重,同名对象已存在就跳过,防止重复 K 线刷新时叠一堆箭头。 信号记录用结构体 SignalInfo 存类型、bar 索引、时间和触发价,ArrayResize 后追加到 signals[]。Regular 类含 "Reg" 子串、Hidden 类含 "Hidden" 子串,UpdateSignalCountLabel 遍历数组分别计数——这两类在背离策略里出现频率差异,可能反映当前趋势动能的收敛或扩张。 外汇与贵金属杠杆高,这类图形标记只作辅助,信号失效概率不低,实盘前务必在 MT5 策略测试器跑一遍看箭头位置和计数是否符合预期。
ObjectSetString(class="num">0, labelName, OBJPROP_TEXT, signalText); } class=class="str">"cmt">// Create an arrow object for the signal. class="type">class="kw">string arrowName = "Arrow_" + signalText + "_" + IntegerToString(barIndex); if(ObjectFind(class="num">0, arrowName) < class="num">0) { class="type">int arrowCode = class="num">0; class="type">class="kw">double arrowPrice = class="num">0.0; class="type">class="kw">color arrowColor = clrWhite; class="type">class="kw">double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); if(StringFind(signalText, "Bullish") != -class="num">1) { arrowCode = class="num">233; class=class="str">"cmt">// Wingdings up arrow arrowColor = clrLime; arrowPrice = lowBuffer[barIndex] - (InpArrowOffset * point); } else if(StringFind(signalText, "Bearish") != -class="num">1) { arrowCode = class="num">234; class=class="str">"cmt">// Wingdings down arrow arrowColor = clrRed; arrowPrice = highBuffer[barIndex] + (InpArrowOffset * point); } if(!ObjectCreate(class="num">0, arrowName, OBJ_ARROW, class="num">0, timeBuffer[barIndex], arrowPrice)) { Print("Failed to create arrow object ", arrowName); class="kw">return; } ObjectSetInteger(class="num">0, arrowName, OBJPROP_COLOR, arrowColor); ObjectSetInteger(class="num">0, arrowName, OBJPROP_ARROWCODE, arrowCode); } class=class="str">"cmt">// Record the signal for evaluation. SignalInfo sig; sig.type = signalText; sig.barIndex = barIndex; sig.signalTime = timeBuffer[barIndex]; if(StringFind(signalText, "Bullish") != -class="num">1) sig.signalPrice = lowBuffer[barIndex]; else sig.signalPrice = highBuffer[barIndex]; ArrayResize(signals, ArraySize(signals) + class="num">1); signals[ArraySize(signals) - class="num">1] = sig; UpdateSignalCountLabel(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| UpdateSignalCountLabel: Updates a label showing signal counts | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void UpdateSignalCountLabel() { class="type">int regCount = class="num">0, hidCount = class="num">0; for(class="type">int i = class="num">0; i < ArraySize(signals); i++) { if(StringFind(signals[i].type, "Reg") != -class="num">1) regCount++; else if(StringFind(signals[i].type, "Hidden") != -class="num">1) hidCount++; }
「回测后给信号贴准确率标签」
背离信号抓出来只是一半活,另一半是看它事后到底灵不灵。下面这段把常规(Regular)和隐藏(Hidden)信号数量直接画在图表左上角,距离左边 10 像素、上边 40 像素,用黄字标出,开盘就能看见本轮统计基数。 评估函数 EvaluateSignalsAndPrint 先把整段收盘价用 CopyClose 拉进数组,再按 InpEvalBars 往前推这根信号对应的验证 bar。比如设 InpEvalBars=20,那信号 bar 往前数 20 根收盘价比信号价高, Bullish 类就算蒙对一次。 regCorrect 和 hidCorrect 分别累加两类信号里「收盘越过了 signalPrice」的次数,regTotal / hidTotal 是总数。外汇与贵金属杠杆高,历史命中率只代表样本内概率,换周期或品种可能明显衰减,实盘前务必自己跑一遍。 别把计数面板当圣杯 面板里的 Regular/Hidden 计数只是回测样本内的正确次数,没扣除点差和滑点。MT5 里把模型切到「每个即时价」重跑,数字往往会掉几个百分点。
class="type">class="kw">string countText = "Regular Signals: " + IntegerToString(regCount) + "\nHidden Signals: " + IntegerToString(hidCount); class="type">class="kw">string countLabel = "SignalCount"; if(ObjectFind(class="num">0, countLabel) == -class="num">1) { if(!ObjectCreate(class="num">0, countLabel, OBJ_LABEL, class="num">0, class="num">0, class="num">0)) { Print("Failed to create SignalCount label"); class="kw">return; } ObjectSetInteger(class="num">0, countLabel, OBJPROP_CORNER, class="num">0); ObjectSetInteger(class="num">0, countLabel, OBJPROP_XDISTANCE, class="num">10); ObjectSetInteger(class="num">0, countLabel, OBJPROP_YDISTANCE, class="num">40); ObjectSetInteger(class="num">0, countLabel, OBJPROP_COLOR, clrYellow); } ObjectSetString(class="num">0, countLabel, OBJPROP_TEXT, countText); } class=class="str">"cmt">//+--------------------------------------------------------------------+ class=class="str">"cmt">//| EvaluateSignalsAndPrint: After backtesting, prints signal accuracy | class=class="str">"cmt">//+--------------------------------------------------------------------+ class="type">void EvaluateSignalsAndPrint() { class="type">class="kw">double closeAll[]; class="type">int totalBars = CopyClose(_Symbol, _Period, class="num">0, WHOLE_ARRAY, closeAll); if(totalBars <= class="num">0) { Print("Error copying complete close data for evaluation"); class="kw">return; } ArraySetAsSeries(closeAll, true); class="type">int totalEvaluated = class="num">0, regTotal = class="num">0, hidTotal = class="num">0; class="type">int regEval = class="num">0, hidEval = class="num">0; class="type">int regCorrect = class="num">0, hidCorrect = class="num">0; for(class="type">int i = class="num">0; i < ArraySize(signals); i++) { class="type">int evalIndex = signals[i].barIndex - InpEvalBars; if(evalIndex < class="num">0) class="kw">continue; class="type">class="kw">double evalClose = closeAll[evalIndex]; if(StringFind(signals[i].type, "Bullish") != -class="num">1) { if(StringFind(signals[i].type, "Reg") != -class="num">1) { regTotal++; regEval++; if(evalClose > signals[i].signalPrice) regCorrect++; } else if(StringFind(signals[i].type, "Hidden") != -class="num">1) {
分类型统计背离信号命中率
这段逻辑接在信号枚举之后,把常规(Regular)与隐藏(Hidden)两类背离分开计数,再按多空方向比对收盘价与信号触发价,得出各自的命中次数。 对于看涨信号,若评估收盘价高于 signalPrice 则记一次正确;看跌信号则要求收盘价低于 signalPrice。regCorrect、hidCorrect 分别累加,regEval、hidEval 记录进入评估的样本量,totalEvaluated 统计总评估数。 最后用三行除法算出整体、常规、隐藏三类准确率,并以 Print 输出到专家日志:整体准确率 = (regCorrect+hidCorrect)/totalEvaluated*100,常规与隐藏各自除以对应 eval 基数。若某类样本为 0,准确率直接给 0.0 避免除零。 在 MT5 策略测试器里跑完,打开 Experts 标签就能看到 '----- Backtest Signal Evaluation -----' 开头的几行文本,里面 Total Signals Generated 是 ArraySize(signals) 的原始信号数,Evaluated 是实际可比对的条数,两者差异往往来自信号之后 K 线不足导致无法取 evalClose。外汇与贵金属波动剧烈,回测命中率仅代表历史样本概率,实盘可能明显偏离。
hidTotal++;
hidEval++;
if(evalClose > signals[i].signalPrice)
hidCorrect++;
}
totalEvaluated++;
}
else
if(StringFind(signals[i].type, "Bearish") != -class="num">1)
{
if(StringFind(signals[i].type, "Reg") != -class="num">1)
{
regTotal++;
regEval++;
if(evalClose < signals[i].signalPrice)
regCorrect++;
}
else
if(StringFind(signals[i].type, "Hidden") != -class="num">1)
{
hidTotal++;
hidEval++;
if(evalClose < signals[i].signalPrice)
hidCorrect++;
}
totalEvaluated++;
}
}
class="type">class="kw">double overallAccuracy = (totalEvaluated > class="num">0) ? (class="type">class="kw">double)(regCorrect + hidCorrect) / totalEvaluated * class="num">100.0 : class="num">0.0;
class="type">class="kw">double regAccuracy = (regEval > class="num">0) ? (class="type">class="kw">double)regCorrect / regEval * class="num">100.0 : class="num">0.0;
class="type">class="kw">double hidAccuracy = (hidEval > class="num">0) ? (class="type">class="kw">double)hidCorrect / hidEval * class="num">100.0 : class="num">0.0;
Print("----- Backtest Signal Evaluation -----");
Print("Total Signals Generated: ", ArraySize(signals));
Print("Signals Evaluated: ", totalEvaluated);
Print("Overall Accuracy: ", DoubleToString(overallAccuracy, class="num">2), "%");
Print("Regular Signals: ", regTotal, " | Evaluated: ", regEval, " | Accuracy: ", DoubleToString(regAccuracy, class="num">2), "%");
Print("Hidden Signals: ", hidTotal, " | Evaluated: ", hidEval, " | Accuracy: ", DoubleToString(hidAccuracy, class="num">2), "%");
}
class=class="str">"cmt">//+------------------------------------------------------------------+◍ RSI背离脚本的骨架与可调旋钮
这套 RSI 背离检测脚本把逻辑拆成了六块:头部声明与输入参数、指标初始化、逐根 K 线取数、摆动点识别、背离判定、信号绘制与回测评估。读源码时先盯输入参数段,因为后面所有过滤阈值都从那里来。 头部用 #property strict 强制严格编译,能挡掉一批隐式类型转换的坑。输入参数里 InpRSIPeriod 默认 14,InpLookback 默认扫 100 根 K 线,InpEvalBars 设 5 根——意思是信号触发后等 5 根收盘再回头算准确率。 摆动点识别靠 IsSignificantSwingLow / IsSignificantSwingHigh,用左右各 1 根(InpSwingLeft、InpSwingRight 默认值)加百分比差过滤毛刺。背离判定分常规与隐藏:看跌背离查价格 HH 而 RSI LH;看涨则反过来,且可开 InpUseRSIThreshold 锁死早段 RSI 在超卖区。 信号落地由 DisplaySignal() 画箭头并写全局数组,反初始化时 EvaluateSignalsAndPrint() 拉全量收盘价,按 InpEvalBars 间距复算常规/隐藏信号准确率。外汇与贵金属波动剧烈、杠杆风险高,回测准确率仅反映历史样本,实盘可能显著衰减。 想验证就把自己 MT5 里的 RSI Divergence 脚本输入段改成下面这份,把 InpSwingLeft/Right 从 1 调到 2~3,看图表摆动点噪音是否明显减少。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| RSI Divergence.mql5 | class=class="str">"cmt">//| Copyright class="num">2025, Christian Benjamin | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "class="num">2025, MetaQuotes Software Corp." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.0" class="macro">#class="kw">property strict class=class="str">"cmt">//---- Input parameters input class="type">int InpRSIPeriod = class="num">14; class=class="str">"cmt">// RSI period input class="type">int InpSwingLeft = class="num">1; class=class="str">"cmt">// Bars to the left for swing detection(relaxed) input class="type">int InpSwingRight = class="num">1; class=class="str">"cmt">// Bars to the right for swing detection(relaxed) input class="type">int InpLookback = class="num">100; class=class="str">"cmt">// Number of bars to scan for divergence input class="type">int InpEvalBars = class="num">5; class=class="str">"cmt">// Bars after which to evaluate a signal input class="type">int InpMinBarsBetweenSignals = class="num">1; class=class="str">"cmt">// Minimum bars between same-type signals(allows frequent re-entry) input class="type">class="kw">double InpArrowOffset = class="num">3.0; class=class="str">"cmt">// Arrow offset(in points) for display