价格行为分析工具包开发(第十三部分):RSI 哨兵工具·综合运用
(3/3)·手动找 RSI 背离又慢又漏?这篇把检测、标记与摘要一次性写进 EA 收尾
「RSI背离检测的参数与数据抓取骨架」
做价格行为结合动能的背离扫描,第一道关是先定好摆动识别的宽松度。把摆幅最小百分比差设到 0.05,意味着价格只要相对前段有 5 个基点的反向位移就可能被记为摆动点;RSI 两点差阈值压到 1.0,是为了在震荡市里别漏掉弱背离。 默认不开启超卖过滤(InpUseRSIThreshold=false),也就是牛背离不强制要求前一段 RSI 进 30 以下。真要严判底部背离,再把开关打开,配合 30/70 这两条线用。 OnInit 里用 iRSI 拿句柄,周期取当前图表 _Period、收盘价计算;句柄无效直接 INIT_FAILED,这步不过后面全白跑。 OnTick 用 iTime 比对 1 号 bar 时间,相同就 return,保证每根收完的 K 线只算一次。随后把 RSI、高低收、时间四类数据按 InpLookback 长度拷进数组,ArraySetAsSeries 设成 true 才能用 [0] 代表最新 bar。任何 Copy 返回 ≤0 都打印错误并退出,避免脏数据进分析。
input class="type">class="kw">double InpMinSwingDiffPct = class="num">0.05; class=class="str">"cmt">// Lower minimum % difference to qualify as a swing input class="type">class="kw">double InpMinRSIDiff = class="num">1.0; class=class="str">"cmt">// Lower minimum difference in RSI between swing points class=class="str">"cmt">// Optional RSI threshold filter for bullish divergence(disabled by class="kw">default) input class="type">bool InpUseRSIThreshold = class="kw">false; class=class="str">"cmt">// If true, require earlier RSI swing to be oversold for bullish divergence input class="type">class="kw">double InpRSIOversold = class="num">30; class=class="str">"cmt">// RSI oversold level input class="type">class="kw">double InpRSIOverbought = class="num">70; class=class="str">"cmt">// RSI overbought level(if needed for bearish) class="type">int OnInit() { class=class="str">"cmt">// Create the RSI indicator handle with the specified period rsiHandle = iRSI(_Symbol, _Period, InpRSIPeriod, PRICE_CLOSE); if(rsiHandle == INVALID_HANDLE) { Print("Error creating RSI handle"); class="kw">return(INIT_FAILED); } class="kw">return(INIT_SUCCEEDED); } class="type">void OnTick() { class=class="str">"cmt">// Process only once per new closed candle by comparing bar times class="type">class="kw">datetime currentBarTime = iTime(_Symbol, _Period, class="num">1); if(currentBarTime == lastBarTime) class="kw">return; lastBarTime = currentBarTime; class=class="str">"cmt">// Copy RSI data for a given lookback period ArrayResize(rsiBuffer, InpLookback); ArraySetAsSeries(rsiBuffer, true); if(CopyBuffer(rsiHandle, class="num">0, class="num">0, InpLookback, rsiBuffer) <= class="num">0) { Print("Error copying RSI data"); class="kw">return; } class=class="str">"cmt">// Copy price data(lows, highs, closes) and time data for analysis ArrayResize(lowBuffer, InpLookback); ArrayResize(highBuffer, InpLookback); ArrayResize(closeBuffer, InpLookback); ArraySetAsSeries(lowBuffer, true); ArraySetAsSeries(highBuffer, true); ArraySetAsSeries(closeBuffer, true); if(CopyLow(_Symbol, _Period, class="num">0, InpLookback, lowBuffer) <= class="num">0 || CopyHigh(_Symbol, _Period, class="num">0, InpLookback, highBuffer) <= class="num">0 || CopyClose(_Symbol, _Period, class="num">0, InpLookback, closeBuffer) <= class="num">0) { Print("Error copying price data"); class="kw">return; } ArrayResize(timeBuffer, InpLookback); ArraySetAsSeries(timeBuffer, true); if(CopyTime(_Symbol, _Period, class="num">0, InpLookback, timeBuffer) <= class="num">0) { Print("Error copying time data"); class="kw">return; }
摆动点判定与背离信号的分水岭
摆点显著性靠的是左右邻域的相对幅度门槛,而不是单纯看高低。IsSignificantSwingLow 以当前低点为中心,向左 left 根、向右 right 根扫描,只要任一侧出现更低且偏离超过 InpMinSwingDiffPct(百分比)的低点,就否定当前摆点。 高低点两套函数逻辑对称:IsSignificantSwingHigh 把比较方向反过来,用 highBuffer 找局部最大。注意左右扫描都做了边界保护——左侧 i<0 跳过,右侧 i>=g_totalBars 直接 break,避免越界读缓冲。 背离部分只取排序后最近的两个摆动高点。常规看跌背离要求价更高、RSI 更低且差值 >= InpMinRSIDiff;隐藏看跌背离则价更低、RSI 更高。外汇与贵金属波动剧烈,这类信号只代表反转概率倾向,实盘须自担高风险。 把 InpMinSwingDiffPct 设到 0.3~0.5 之间,在 XAUUSD 的 M15 上能过滤掉大量毛刺摆点;设太小会把噪音当结构。
g_totalBars = InpLookback; class=class="str">"cmt">// (Further processing follows here...) 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 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 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; } class="type">bool IsSignificantSwingHigh(class="type">int index, class="type">int left, class="type">int right) { class="type">class="kw">double currentHigh = highBuffer[index]; class=class="str">"cmt">// Check left side for local maximum 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((currentHigh - highBuffer[i]) / currentHigh) * class="num">100.0; if(highBuffer[i] > currentHigh && pctDiff > InpMinSwingDiffPct) class="kw">return class="kw">false; } class=class="str">"cmt">// Check right side for local maximum 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((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=class="str">"cmt">// --- Bearish Divergence(class="kw">using swing highs) if(ArraySize(swingHighs) >= class="num">2) { ArraySort(swingHighs); class=class="str">"cmt">// Ensure ascending order: index class="num">0 is most recent class="type">int recent = swingHighs[class="num">0]; class="type">int previous = swingHighs[class="num">1]; class=class="str">"cmt">// Regular Bearish Divergence: Price makes a higher high class="kw">while RSI makes a lower high if(highBuffer[recent] > highBuffer[previous] && rsiBuffer[recent] < rsiBuffer[previous] && (rsiBuffer[previous] - rsiBuffer[recent]) >= InpMinRSIDiff) { Print("Regular Bearish Divergence detected at bar ", recent); DisplaySignal("RegBearish Divergence", recent); } class=class="str">"cmt">// Hidden Bearish Divergence: Price makes a lower high class="kw">while RSI makes a higher high else if(highBuffer[recent] < highBuffer[previous] && rsiBuffer[recent] > rsiBuffer[previous] && (rsiBuffer[recent] - rsiBuffer[previous]) >= InpMinRSIDiff) {
◍ 牛背离与信号去重的代码骨架
在摆荡低点数组 swingLows 中,先取最近两个低点做比较。常规牛背离要求价格走出更低的低点,而 RSI 反而抬高,且两者差值不小于 InpMinRSIDiff;隐藏牛背离则相反,价格高点抬升、RSI 走低。 代码里对常规信号额外加了一道过滤:若开启 InpUseRSIThreshold,则前一次 RSI 摆动必须处于 InpRSIOversold 超卖线以下才放行。这一步能砍掉一批弱势反弹中的假信号。 DisplaySignal 函数负责抑制刷屏。它遍历已记录信号,若同类型信号与当前 bar 距离小于 InpMinBarsBetweenSignals,直接 return 退出,避免相邻 K 线重复报警。 对于含「Reg」字样的常规信号,脚本会在图表左上角(距边 10/20 像素)创建或更新一个名为 LatestSignal 的 OBJ_LABEL 文本,颜色白。你可在 MT5 里改 clrWhite 或坐标值,把提示放到不挡 K 线的位置。外汇与贵金属波动剧烈,背离仅代表概率倾斜,实盘须自担高风险。
if(ArraySize(swingLows) >= class="num">2) { ArraySort(swingLows); class=class="str">"cmt">// Ensure 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 RSI swing to 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("HiddenBullishDivergence", recent); } } class="type">void DisplaySignal(class="type">class="kw">string signalText, class="type">int barIndex) { class=class="str">"cmt">// Prevent duplicate signals on the same or nearby bars 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; } class=class="str">"cmt">// Update a label for the latest regular signal 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); }
「箭头标记与信号回测落库」
给图表打信号不能只写文字标签,箭头对象才是肉眼盯盘时的第一锚点。下面这段逻辑在发现 Bullish 字样的信号时,用 Wingdings 编码 233 画向上青绿箭头,位置落在对应 K 线最低价减去 InpArrowOffset 个 point;Bearish 则取编码 234 向下红箭头,挂在最高价加偏移处。
class=class="str">"cmt">// 先给标签写信号文字 ObjectSetString(class="num">0, labelName, OBJPROP_TEXT, signalText); } class=class="str">"cmt">// 创建箭头对象在图表上标记信号 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 上箭头 arrowColor = clrLime; arrowPrice = lowBuffer[barIndex] - (InpArrowOffset * point); } else if(StringFind(signalText, "Bearish") != -class="num">1) { arrowCode = class="num">234; class=class="str">"cmt">// Wingdings 下箭头 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">// 记录信号明细供后续回测 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="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;
ObjectSetString(class="num">0, labelName, OBJPROP_TEXT, signalText); } class=class="str">"cmt">// Create an arrow object to mark the signal on the chart 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 details for later backtesting 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="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;
逐根回测信号的命中率怎么算
信号回测不能只看生成了多少根,得把每根信号推后 InpEvalBars 根 K 线,用那时的收盘价和信号触发价比对错。下面这段循环就是干这事:遍历 signals 数组,算 evalIndex = barIndex - InpEvalBars,越界就跳过,否则取 closeAll[evalIndex] 作为评估收盘价。 分类靠字符串匹配:带 Bullish 且含 Reg 的常规多头信号,若 evalClose > signalPrice 记 regCorrect++;带 Hidden 的隐藏背离,同样逻辑记 hidCorrect++。Bearish 方向反过来,evalClose < signalPrice 才算对。最后 totalEvaluated 累计所有有效评估。 三个准确率分开算:overallAccuracy 是 (regCorrect+hidCorrect)/totalEvaluated*100,regAccuracy 和 hidAccuracy 各自除以 regEval、hidEval。若分母为 0 直接给 0.0,避免除零崩脚本。跑完用 Print 把总数、评估数和整体准确率(保留两位小数)打到日志,EURUSD 这类高杠杆品种回测前先认清外汇高风险。
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) { 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), "%");
◍ 把信号统计打印到日志里
这段输出逻辑直接把两类信号的评估样本量与准确率落到 MT5 Experts 日志,方便你跑完回测后快速比对。 Regular 与 Hidden 分开打印,字段依次是总数、已评估数、保留两位小数后的准确率百分比;用 DoubleToString(...,2) 控制精度,避免浮点噪声干扰肉眼判断。 在外汇与贵金属这种高杠杆、高波动市场,信号准确率只是概率参考,任何一次统计都不能当作方向保证,开 MT5 按 F4 编译后看日志才是硬验证。
Print("Regular Signals: ", regTotal, " | Evaluated: ", regEval, " | Accuracy: ", DoubleToString(regAccuracy, class="num">2), "%"); Print("Hidden Signals: ", hidTotal, " | Evaluated: ", hidEval, " | Accuracy: ", DoubleToString(hidAccuracy, class="num">2), "%");
「拖进图表跑一遍信号对不对」
EA 在 MetaEditor 里编译通过后,直接拖到 MT5 图表上跑,第一步务必用模拟账户,外汇和贵金属杠杆品种爆仓速度远超股票,真金白银试错不划算。 想肉眼复核信号,就在「指标」面板里调出 RSI,参数必须和 EA 内部写死的周期、价位线一致,否则图表和策略会对不上。我们在一分钟周期上抓到过一次常规看涨背离,RSI 底背离配合价格新低,信号在副图里看得非常直白。 另一组在 Boom 500 上跑的测试,价格行为形态叠加 RSI 双重确认,给出了卖出信号;用回测 GIF 复看时,隐藏延续信号和常规信号混在一起,少数没拿到确认的单子该过滤就过滤,别全吃。 图表上能看到的确认案例:1 分钟周期常规背离、Boom 500 双确认空单、回测中隐藏+常规信号并存,这三组现象够你开 MT5 照着复现一遍。
把 RSI 背离工具接进你的验证流程
RSI 背离对比工具在系列里排第 13 号,发布于 14/02/25,定位是把价格走势与 RSI 背离做同屏比对,属于 Lynnchris 工具箱里偏价格行为验证的一环。前面 12 个工具从图表投影仪到 FibVWAP,覆盖叠加、表格预测、布林带+RSI+ATR 的 EA、外部资金流等,说明这条工具线一直在往「多源交叉」走。 测试里出现过有希望的结果和积极趋势,但作者明确点出:每个信号入场前都必须和你的整体策略交叉核对,工具只负责监控和确认,不替代判断。外汇与贵金属杠杆高、滑点跳空频繁,盲目跟信号可能放大亏损。 下一步值得做的是把外部库接入做精确摆动点识别,这能直接抬升背离信号的命中概率。建议你在 MT5 里加载 RSI_DIVERGENCE.mq5,先把 RSI 周期和背离阈值按自己品种调一遍,再决定是否进实盘观察。