价格行为分析工具包开发(第二十一部分):市场结构反转检测工具·进阶篇
🔄

价格行为分析工具包开发(第二十一部分):市场结构反转检测工具·进阶篇

(2/3)·震荡市里超半数枢轴点可能是假信号,这套EA把噪声压到5%以内

实战向 第 2/3 篇
很多交易者把每一个枢轴点突破都当反转开仓,结果在锯齿行情里反复止损。把ATR绑进摆动点过滤器,能让阈值随波动率呼吸,只在真实结构破位时才亮信号。

「用 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 打出来,你就能估出该品种平均多久翻一次脸。

MQL5 / C++
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 策略测试器用历史数据验证触发逻辑。

MQL5 / C++
  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 里(本小节未列),先把这个骨架里的参数和变量名吃透,调参时才不会改错地方。外汇与贵金属波动剧烈,实盘前务必在模拟盘验证参数敏感性。

MQL5 / C++
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 验证箭头出现位置。

MQL5 / C++
   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% 以上,调它比调均线周期更影响信号密度。

MQL5 / C++
  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)
  {
让小布替你跑这套
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到实时反转标记与噪声比统计,把重复劳动交给小布,你专注决策。

常见问题

它把微小波动也当成高低点,缺乏波动率感知,容易被whipsaws困住造成频繁止损。
m放大波动率深度,f在波动加剧时进一步放宽阈值,二者共同决定忽略多少根干扰K线。
小布盯盘已内置基于ATR的枢轴过滤与反转标注,无需自行编译,切换品种即可看信号面板。
将深度阈值与ATR估算的波动率挂钩,按正态近似调参,使两倍深度区间内单根K线成极值的概率低于1/(2d+1)。
先出现更低低点,随后下一个波动低点突破该谷值,且前一个市场结构已被确认破位才标记。