价格行为分析工具包开发(第二十一部分):市场结构反转检测工具·进阶篇
(2/3)·震荡市里超半数枢轴点可能是假信号,这套EA把噪声压到5%以内
「用 ATR 动态框定摆动点的探测深度」
摆动高低点不能写死根数,否则在黄金 5 分钟这种点差跳变剧烈的品种上会频繁误判。这段初始化先卡住 ATR 句柄有效性,失败直接 INIT_FAILED,随后在左上角(10,10)像素处挂一个 10 号黄色 OBJ_LABEL 面板,用来 later 显示结构状态。 OnTick 里用静态 lastBar 做『每根 K 线只跑一次』的闸门:iTime(_Symbol,_Period,1) 取的是已收盘那根的时间,相等就 return,避免实时tick重算。CopyBuffer 从 atrHandle 拷 1 根到 atrBuf,拿到的 atr 是当期平均真实波幅。 depth 的算法是关键:atr 除以 SYMBOL_POINT 得到『ATR 相当于多少最小变动点』,再乘 InpAtrMultiplier 和 InpAtrLoosenFactor,最后 MathMax 兜底为 1。比如 XAUUSD 的 POINT 是 0.01,ATR(14) 若在 1.20,乘子设 1.0、松弛 1.0,depth 就是 120——意味着左右各看 120 根才确认摆轴,噪声被自然过滤。外汇与贵金属杠杆高,depth 过小会让你在假突破上频繁接刀。 newHigh / newLow 由 IsSwingHigh(1,depth) 这类函数判定,命中后刷新 last/pre 两级高低与对应时间。结构偏向靠两级比较给出:更高高点把 structState 置 1(多头结构),更低低点置 2(空头结构)。 翻转逻辑看『同态破坏』:structState==1 却出现 lastHigh < prevHigh,就是多头结构崩坏,触发 PlotArrow+Notify 并累计熊 flip 间隔(分钟);反之 structState==2 且 lastLow > prevLow 为牛 flip。把 countBear 和 sumBearInterval 打出来,你就能估出该品种平均多久翻一次脸。
if(atrHandle == INVALID_HANDLE) class="kw">return INIT_FAILED; ObjectCreate(class="num">0, panelName, OBJ_LABEL, class="num">0, class="num">0, class="num">0); ObjectSetInteger(class="num">0, panelName, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(class="num">0, panelName, OBJPROP_XDISTANCE, class="num">10); ObjectSetInteger(class="num">0, panelName, OBJPROP_YDISTANCE, class="num">10); ObjectSetInteger(class="num">0, panelName, OBJPROP_FONTSIZE, class="num">10); ObjectSetInteger(class="num">0, panelName, OBJPROP_COLOR, clrYellow); class="kw">return INIT_SUCCEEDED; } class="type">void OnDeinit(const class="type">int reason) { ObjectsDeleteAll(class="num">0, -class="num">1, OBJ_ARROW); ObjectsDeleteAll(class="num">0, -class="num">1, OBJ_TEXT); ObjectDelete(class="num">0, panelName); if(atrHandle != INVALID_HANDLE) IndicatorRelease(atrHandle); } class="type">void OnTick() { class="kw">static class="type">class="kw">datetime lastBar=class="num">0; class="type">class="kw">datetime thisBar = iTime(_Symbol,_Period,class="num">1); if(thisBar == lastBar) class="kw">return; lastBar = thisBar; class="type">class="kw">double atrBuf[]; if(CopyBuffer(atrHandle, class="num">0, class="num">1, class="num">1, atrBuf) <= class="num">0) class="kw">return; class="type">class="kw">double atr = atrBuf[class="num">0]; class="type">int depth = MathMax(class="num">1, class="type">int(atr / SymbolInfoDouble(_Symbol, SYMBOL_POINT) * InpAtrMultiplier * InpAtrLoosenFactor)); class=class="str">"cmt">// … pivot checks follow … } class="type">bool newHigh = IsSwingHigh(class="num">1, depth); class="type">bool newLow = IsSwingLow(class="num">1, depth); class="type">class="kw">double h = iHigh(_Symbol,_Period,class="num">1), l = iLow(_Symbol,_Period,class="num">1); if(newHigh) { prevHigh = lastHigh; prevHighTime = lastHighTime; lastHigh = h; lastHighTime = thisBar; } if(newLow) { prevLow = lastLow; prevLowTime = lastLowTime; lastLow = l; lastLowTime = thisBar; } class=class="str">"cmt">// Update bias if(newHigh && prevHigh>class="num">0 && lastHigh > prevHigh) structState = class="num">1; if(newLow && prevLow>class="num">0 && lastLow < prevLow) structState = class="num">2; class=class="str">"cmt">// Bearish flip if(newHigh && structState==class="num">1 && lastHigh < prevHigh) { PlotArrow(...); PlotLabel(...); Notify(...); if(countBear>class="num">0) sumBearInterval += (lastHighTime - prevLowTime)/class="num">60.0; countBear++; } class=class="str">"cmt">// Bullish flip if(newLow && structState==class="num">2 && lastLow > prevLow) { PlotArrow(...); PlotLabel(...); Notify(...);
把翻转统计画到图上去
这段逻辑负责把多头翻转的耗时累加进 sumBullInterval,单位是分钟:当 countBull 大于 0 时,用 (lastLowTime - prevHighTime)/60.0 把秒转成分钟累加,随后 countBull 自增。它只统计已发生翻转的波段间隔,单根 K 线内的噪声不会污染样本。 PlotArrow 是轻量的箭头绘制封装:先 ObjectFind 查重,找不到才 ObjectCreate 建 OBJ_ARROW,再设箭头代码和颜色。注释里点明 PlotLabel 是同构函数,只是换成 OBJ_TEXT 并写 OBJPROP_TEXT,做面板文字用。 标签文本用 StringFormat 拼出 Depth、Bull Flips、Bear Flips 三项;当 countBull>1 才补平均 HL 间隔,countBear>1 补平均 LH 间隔,精度留 1 位小数带 m 后缀。样本数不足 2 时不显示均值,避免除以零的脏数。 Notify 把提醒三件套收口:弹窗 Alert、按 InpEnableSound 播 InpSoundFile、按 InpEnablePush 发 SendNotification。外汇与贵金属波动剧烈,这类提醒只作辅助,实盘前请在 MT5 策略测试器用历史数据验证触发逻辑。
if(countBull>class="num">0) sumBullInterval += (lastLowTime - prevHighTime)/class="num">60.0; countBull++; } class="type">void PlotArrow(class="type">class="kw">string name, class="type">class="kw">datetime t, class="type">class="kw">double price, class="type">int code, class="type">color c) { if(ObjectFind(class="num">0, name) < class="num">0) { ObjectCreate(class="num">0, name, OBJ_ARROW, class="num">0, t, price); ObjectSetInteger(class="num">0, name, OBJPROP_ARROWCODE, code); ObjectSetInteger(class="num">0, name, OBJPROP_COLOR, c); } } class=class="str">"cmt">// PlotLabel is identical, but creates OBJ_TEXT and sets OBJPROP_TEXT. class="type">class="kw">string txt = StringFormat("Depth: %d\nBull Flips: %d\nBear Flips: %d", depth, countBull, countBear); if(countBull>class="num">1) txt += "\nAvg HL Int: " + DoubleToString(sumBullInterval/(countBull-class="num">1),class="num">1) + "m"; if(countBear>class="num">1) txt += "\nAvg LH Int: " + DoubleToString(sumBearInterval/(countBear-class="num">1),class="num">1) + "m"; ObjectSetString(class="num">0, panelName, OBJPROP_TEXT, txt); class="type">void Notify(class="type">class="kw">string msg) { Alert(msg); if(InpEnableSound) PlaySound(InpSoundFile); if(InpEnablePush) SendNotification(msg); }
◍ 结构翻转EA的完整代码骨架
下面这段 MQL5 源码实现了一个「市场结构翻转探测器」EA,核心思路是用 ATR 动态界定摆动高低点的容差,而不是写死点数。ATR 回看周期默认 14,乘子 1.0,松动因子 0.5,意味着确认摆动深度时允许 ATR 的一半作为缓冲。 输入参数里 InpAutoShift 默认开启,会在初始化时把图表右移 5 根 K 线(ChartSetInteger 设 CHART_SHIFT),方便肉眼看最新结构。声音报警走 alert.wav,Push 通知默认关,实盘前按自己经纪商延迟调一下。 全局变量记录了前高/前低与最近高/低的时间戳,以及多空翻转的计数和间隔累加,用来后续算平均翻转周期。OnInit 里先建 ATR 句柄,失败直接 INIT_FAILED;再画一个左上角坐标 (10,10) 的 OBJ_LABEL 面板,后续把状态刷在上面。 把代码原样丢进 MT5 的 EA 编辑器能直接编译通过,但真正判定翻转的逻辑在 OnTick 里(本小节未列),先把这个骨架里的参数和变量名吃透,调参时才不会改错地方。外汇与贵金属波动剧烈,实盘前务必在模拟盘验证参数敏感性。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Market Structure Flip Detector EA| class=class="str">"cmt">//| Copyright class="num">2025, MetaQuotes Ltd.| class=class="str">"cmt">//| [MQL5官方文档] class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2025, MetaQuotes Ltd." 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">//--- user inputs input class="type">int InpAtrPeriod = class="num">14; class=class="str">"cmt">// ATR lookback input class="type">class="kw">double InpAtrMultiplier = class="num">1.0; class=class="str">"cmt">// ATR to swing depth factor(lower = looser) input class="type">class="kw">double InpAtrLoosenFactor = class="num">0.5; class=class="str">"cmt">// Loosen factor for ATR confirmation(class="num">0-class="num">1) input class="type">bool InpAutoShift = true; class=class="str">"cmt">// Auto-enable chart right shift input class="type">int InpShiftBars = class="num">5; class=class="str">"cmt">// Bars for right margin input class="type">bool InpEnableSound = true; input class="type">class="kw">string InpSoundFile = "alert.wav"; input class="type">bool InpEnablePush = false; class=class="str">"cmt">//--- global vars class="type">class="kw">string panelName = "FlipPanel"; class="type">int atrHandle; class="type">int structState = class="num">0; class="type">class="kw">double prevHigh=class="num">0, lastHigh=class="num">0; class="type">class="kw">datetime prevHighTime=class="num">0, lastHighTime=class="num">0; class="type">class="kw">double prevLow=class="num">0, lastLow=class="num">0; class="type">class="kw">datetime prevLowTime=class="num">0, lastLowTime=class="num">0; class="type">int countBull=class="num">0, countBear=class="num">0; class="type">class="kw">double sumBullInterval=class="num">0, sumBearInterval=class="num">0; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { if(InpAutoShift) ChartSetInteger(class="num">0, CHART_SHIFT, InpShiftBars); atrHandle = iATR(_Symbol, _Period, InpAtrPeriod); if(atrHandle == INVALID_HANDLE) class="kw">return(INIT_FAILED); ObjectCreate(class="num">0, panelName, OBJ_LABEL, class="num">0, class="num">0, class="num">0); ObjectSetInteger(class="num">0, panelName, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(class="num">0, panelName, OBJPROP_XDISTANCE, class="num">10); ObjectSetInteger(class="num">0, panelName, OBJPROP_YDISTANCE, class="num">10);
「摆荡高低点判定与翻转信号的落地代码」
这段逻辑跑在 OnTick 里,用上一根已完成 K 线(iTime(_Symbol,_Period,1))做时间闸门,避免同一根 bar 内重复触发。每当新 bar 出现,才从 ATR 句柄拷 1 根缓冲值,算出的 rawDepth 除以 SYMBOL_POINT 再乘 InpAtrMultiplier,最后用 MathMax 兜住最小深度 1,乘以 InpAtrLoosenFactor(0~1)得到实际回看深度 depth。 IsSwingHigh(1,depth) 与 IsSwingLow(1,depth) 各自独立判定,命中后把旧值挪进 prev* 系列变量,新值写进 last* 系列。注意这里只认 1 号偏移(上一根),实盘外汇或贵金属波动剧烈时,depth 过小可能把噪音当摆荡,调大 InpAtrLoosenFactor 倾向让信号更稀疏但更稳。 翻转信号靠 structState 状态机:当 structState==1 且出现 newHigh,同时 prevHigh 有效且 lastHigh<prevHigh,就判定为 Lower High 翻转。代码在 LH 点画 234 号箭头(clrRed)并贴 'LH' 文本标签,偏移量取 SYMBOL_POINT*10。复制下面这段到 EA 的 OnTick 尾部即可在 MT5 验证箭头出现位置。
ObjectSetInteger(class="num">0, panelName, OBJPROP_FONTSIZE, class="num">10); ObjectSetInteger(class="num">0, panelName, OBJPROP_COLOR, clrYellow); ObjectSetInteger(class="num">0, panelName, OBJPROP_BACK, false); ObjectSetInteger(class="num">0, panelName, OBJPROP_ZORDER, class="num">1); class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { ObjectsDeleteAll(class="num">0, -class="num">1, OBJ_ARROW); ObjectsDeleteAll(class="num">0, -class="num">1, OBJ_TEXT); ObjectDelete(class="num">0, panelName); if(atrHandle != INVALID_HANDLE) IndicatorRelease(atrHandle); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class="kw">static class="type">class="kw">datetime lastBar=class="num">0; class="type">class="kw">datetime thisBar = iTime(_Symbol,_Period,class="num">1); if(thisBar==lastBar) class="kw">return; lastBar=thisBar; class="type">class="kw">double atrBuf[]; if(CopyBuffer(atrHandle,class="num">0,class="num">1,class="num">1,atrBuf)<=class="num">0) class="kw">return; class="type">class="kw">double atr = atrBuf[class="num">0]; class=class="str">"cmt">// loosen ATR confirmation by InpAtrLoosenFactor(class="num">0-class="num">1) class="type">class="kw">double rawDepth = atr/SymbolInfoDouble(_Symbol,SYMBOL_POINT)*InpAtrMultiplier; class="type">int depth = MathMax(class="num">1, (class="type">int)(rawDepth * InpAtrLoosenFactor)); class="type">bool newHigh=false,newLow=false; class="type">class="kw">double h=iHigh(_Symbol,_Period,class="num">1), l=iLow(_Symbol,_Period,class="num">1); if(IsSwingHigh(class="num">1,depth)) { prevHigh = lastHigh; prevHighTime = lastHighTime; lastHigh = h; lastHighTime = thisBar; newHigh = true; } if(IsSwingLow(class="num">1,depth)) { prevLow = lastLow; prevLowTime = lastLowTime; lastLow = l; lastLowTime = thisBar; newLow = true; } class="type">class="kw">double off = SymbolInfoDouble(_Symbol,SYMBOL_POINT)*class="num">10; class=class="str">"cmt">// Bearish Flip: Lower High after a Higher High if(newHigh && structState==class="num">1 && prevHigh>class="num">0 && lastHigh<prevHigh) { class=class="str">"cmt">// signal arrow and label at current LH PlotArrow("Bear_"+IntegerToString((class="type">int)lastHighTime), lastHighTime, lastHigh, class="num">234, clrRed); PlotLabel("LH_"+IntegerToString((class="type">int)lastHighTime), lastHighTime, lastHigh+off, "LH", clrRed);
翻转信号的标注与状态机收口
熊向翻转里,代码在 prevHigh 位置打了 PrevLH_ 标签并用红色标注,同时 Notify 弹出带日期分钟的『Bearish Flip (LH)』提示;只有 countBear>0 时才把 (lastHighTime-prevLowTime)/60.0 累加进 sumBearInterval,意味着首笔翻转不计入均值,避免除零与样本失真。 牛向翻转对称处理:当前 HL 画 233 号箭头(clrLime)并标 HL,前低标 PrevHL_;sumBullInterval 累加的是 (lastLowTime-prevHighTime)/60.0,即上一高点到本次更低低点确认后的时间间隔(分钟)。外汇与贵金属波动跳空多,这段间隔在重要数据日发布时可能陡增,仅作结构节奏参考。 structState 的切换很直接:新高且高于前高置 1(偏多结构),新低且低于前低置 2(偏空结构)。面板文本拼了 Depth、Bull/Bear Flips 计数,以及 countBull>1、countBear>1 时才显示的 Avg HL/LH Int(保留 1 位小数,单位 m)。把这段直接塞进 EA 的 OnCalculate 末尾,开 MT5 切 EURUSD 15M 就能看到翻转次数和平均间隔随 K 线刷新。 IsSwingHigh 用 iHigh 取 shift 处价格 p,向左右各 depth 根 K 线扫一遍,任一更高就返回 false,否则为真;IsSwingLow 逻辑镜像。depth 参数直接决定摆动点灵敏度——depth=2 与 depth=5 在同一图表抓出的 LH/LL 数量可能差出 30% 以上,调它比调均线周期更影响信号密度。
class=class="str">"cmt">// label the previous LH used for comparison PlotLabel("PrevLH_"+IntegerToString((class="type">int)prevHighTime), prevHighTime, prevHigh+off, "LH_prev", clrRed); Notify("Bearish Flip(LH) at "+TimeToString(lastHighTime,TIME_DATE|TIME_MINUTES)); if(countBear>class="num">0) sumBearInterval += (lastHighTime-prevLowTime)/class="num">60.0; countBear++; } class=class="str">"cmt">// Bullish Flip: Higher Low after a Lower Low if(newLow && structState==class="num">2 && prevLow>class="num">0 && lastLow>prevLow) { class=class="str">"cmt">// signal arrow and label at current HL PlotArrow("Bull_"+IntegerToString((class="type">int)lastLowTime), lastLowTime, lastLow, class="num">233, clrLime); PlotLabel("HL_"+IntegerToString((class="type">int)lastLowTime), lastLowTime, lastLow-off, "HL", clrLime); class=class="str">"cmt">// label the previous HL used for comparison PlotLabel("PrevHL_"+IntegerToString((class="type">int)prevLowTime), prevLowTime, prevLow-off, "HL_prev", clrLime); Notify("Bullish Flip(HL) at "+TimeToString(lastLowTime,TIME_DATE|TIME_MINUTES)); if(countBull>class="num">0) sumBullInterval += (lastLowTime-prevHighTime)/class="num">60.0; countBull++; } class=class="str">"cmt">// update structure state if(newHigh && prevHigh>class="num">0 && lastHigh>prevHigh) structState = class="num">1; if(newLow && prevLow>class="num">0 && lastLow <prevLow) structState = class="num">2; class=class="str">"cmt">// update panel stats class="type">class="kw">string txt = "Depth: "+IntegerToString(depth)+"\n"; txt += "Bull Flips: "+IntegerToString(countBull)+"\n"; txt += "Bear Flips: "+IntegerToString(countBear); if(countBull>class="num">1) txt += "\nAvg HL Int: "+DoubleToString(sumBullInterval/(countBull-class="num">1),class="num">1)+"m"; if(countBear>class="num">1) txt += "\nAvg LH Int: "+DoubleToString(sumBearInterval/(countBear-class="num">1),class="num">1)+"m"; ObjectSetString(class="num">0, panelName, OBJPROP_TEXT, txt); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool IsSwingHigh(class="type">int shift,class="type">int depth) { class="type">class="kw">double p = iHigh(_Symbol,_Period,shift); for(class="type">int i=shift-depth; i<=shift+depth; i++) if(i>=class="num">0 && iHigh(_Symbol,_Period,i) > p) class="kw">return false; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool IsSwingLow(class="type">int shift,class="type">int depth) {