价格行为分析工具包开发(第十四部分):抛物线转向与反转工具·进阶篇
📈

价格行为分析工具包开发(第十四部分):抛物线转向与反转工具·进阶篇

(2/3)· 当PSAR点翻越价格,你的EA能否在反转临界点自动识别并退出?

含代码示例实战向 第 2/3 篇

不少交易者把抛物线转向只当画图辅助,真到写EA时却卡在信号阈值和前两根K线确认逻辑上。PSAR翻面若不加一致性过滤,震荡市会连续打脸。这一篇把反转工具的MQL5实现拆开给你看。

「SAR 翻转与实体过滤的买卖判定」

这段逻辑只在每根 K 线收盘后跑一次,用 Parabolic SAR 点阵位置切换配合当根阳阴线来给方向。核心是先取前三根 K 线的开收与 SAR 值:open2/close2 是倒数第三根,sar0~sar2 分别对应最近三根的点位。 买信号要求当前收高于开(阳线)且 sar0 已落到收盘价下方,同时前两根 sar1、sar2 都高于各自收盘价——也就是 SAR 刚从价格上方翻到下方。代码里还数了 countBearish(前两根 SAR 在价上的次数)并算 sar1 与 sar2 的绝对差 dotGap,用 MaxDotGapPercentage 换算成基于 close0 的阈值 gapThreshold 做过滤。 卖信号对称:阴线且 sar0 在收上方,sar1、sar2 在收下方,countBullish 与 dotGap 同样受 MinConsecutiveDots 和 gapThreshold 约束才返 SELL_SIGNAL。两个分支都不满足就返 NO_SIGNAL。 外汇与贵金属波动剧烈,SAR 点距过滤参数若设太宽,在镑美 M5 上可能一晚触发多次假信号;建议把 MaxDotGapPercentage 先按 0.3 试,再上 MT5 用策略测试器看近 3 个月回放。

MQL5 / C++
class="type">class="kw">double open2 = openArray[class="num">2], close2 = closeArray[class="num">2];

class=class="str">"cmt">// Check for BUY signal:
if((close0 > open0) && (sar0 < close0) &&
   (sar1 > close1) && (sar2 > close2))
   {
     class="type">int countBearish = class="num">0;
     if(sar1 > close1) countBearish++;
     if(sar2 > close2) countBearish++;
     class="type">class="kw">double dotGap = MathAbs(sar1 - sar2);
     class="type">class="kw">double gapThreshold = (MaxDotGapPercentage / class="num">100.0) * close0;
     if(countBearish >= MinConsecutiveDots && dotGap <= gapThreshold)
        class="kw">return BUY_SIGNAL;
   }

class=class="str">"cmt">// Check for SELL signal:
if((close0 < open0) && (sar0 > close0) &&
   (sar1 < close1) && (sar2 < close2))
   {
     class="type">int countBullish = class="num">0;
     if(sar1 < close1) countBullish++;
     if(sar2 < close2) countBullish++;
     class="type">class="kw">double dotGap = MathAbs(sar1 - sar2);
     class="type">class="kw">double gapThreshold = (MaxDotGapPercentage / class="num">100.0) * close0;
     if(countBullish >= MinConsecutiveDots && dotGap <= gapThreshold)
        class="kw">return SELL_SIGNAL;
   }

class="kw">return NO_SIGNAL;
}
class="type">bool IsNewBar()
  {
  class="type">class="kw">datetime times[];
  if(CopyTime(_Symbol, _Period, class="num">1, class="num">1, times) <= class="num">0)
     {
       Print("Failed to retrieve bar time in IsNewBar().");
       class="kw">return false;
     }
  if(times[class="num">0] != lastBarTime)
     {
       lastBarTime = times[class="num">0];
       class="kw">return true;
     }
  class="kw">return false;
  }
class="type">int OnInit()
  {
  class=class="str">"cmt">// Create the built-in Parabolic SAR indicator handle
  sarHandle = iSAR(_Symbol, _Period, SARStep, SARMaximum);
  if(sarHandle == INVALID_HANDLE)
     {
       Print("Error creating PSAR handle");
       class="kw">return INIT_FAILED;
     }
  Print("SAR EA initialized successfully.");
  class="kw">return INIT_SUCCEEDED;
  }
class="type">void OnTick()
  {
  class=class="str">"cmt">// Process only once per new closed bar
  if(!IsNewBar())
     class="kw">return;

  class=class="str">"cmt">// Retrieve PSAR and price data
  class="type">class="kw">double sarArray[class="num">4];
  if(CopyBuffer(sarHandle, class="num">0, class="num">1, class="num">4, sarArray) < class="num">3) { class=class="str">"cmt">/* error handling */ }
  class="type">class="kw">double openArray[class="num">4], closeArray[class="num">4];
  if(CopyOpen(_Symbol, _Period, class="num">1, class="num">4, openArray) < class="num">3 ||

◍ 挂起信号的等待与假信号甄别

EA 在拿到初步反转意图后不会立刻下单,而是把 pendingSignal 挂起,用 waitCount 记等待次数。每根新 K 线进来 waitCount++,给价格最多 3 根 K 线的确认窗口。 以 BUY 挂起为例:若最新收盘价 closeArray[0] 跌破 pendingReversalLevel(即信号触发时的 PSAR 值),视为反转确认并关闭警报;若连等 3 根 K 线收盘价都未触及该位,则提示可能是假 BUY 信号。SELL 逻辑对称,收盘价站上 PSAR 才确认,否则等待超 3 根即预警。 无挂起信号时,CheckForSignal 用 sarArray、openArray、closeArray 探测新信号;一旦返回 BUY/SELL,便重置 waitCount=0 并把 pendingReversalLevel 存为当前 sarArray[0]。外汇与贵金属波动剧烈,3 根窗口只是概率过滤,不代表信号必然有效,实盘前请在 MT5 用历史数据回测该等待逻辑。

MQL5 / C++
if(CopyClose(_Symbol, _Period, class="num">1, class="num">4, closeArray) < class="num">3) { class=class="str">"cmt">/* error handling */ }

  class=class="str">"cmt">// Pending Signal Logic...
  if(pendingSignal != NO_SIGNAL)
    {
      waitCount++; class=class="str">"cmt">// Increment waiting counter

      if(pendingSignal == BUY_SIGNAL)
        {
         if(closeArray[class="num">0] <= pendingReversalLevel)
           {
             class=class="str">"cmt">// Confirm reversal: Close alert for BUY signal
           }
         else if(waitCount >= class="num">3)
           {
             class=class="str">"cmt">// Warn about a possible fake BUY signal
           }
        }
      else if(pendingSignal == SELL_SIGNAL)
        {
         if(closeArray[class="num">0] >= pendingReversalLevel)
           {
             class=class="str">"cmt">// Confirm reversal: Close alert for SELL signal
           }
         else if(waitCount >= class="num">3)
           {
             class=class="str">"cmt">// Warn about a possible fake SELL signal
           }
        }
      class="kw">return; class=class="str">"cmt">// Wait until pending signal is resolved
    }

  class=class="str">"cmt">// Check for a new reversal signal if no pending signal exists
  SignalType newSignal = CheckForSignal(sarArray, openArray, closeArray);
  if(newSignal != NO_SIGNAL)
    {
      pendingSignal = newSignal;
      waitCount = class="num">0; class=class="str">"cmt">// Reset counter
      pendingReversalLevel = sarArray[class="num">0]; class=class="str">"cmt">// Store current PSAR value

      class=class="str">"cmt">// Alert and optionally draw an arrow based on the new signal
      if(newSignal == BUY_SIGNAL) { class=class="str">"cmt">/* process BUY signal */ }
      else if(newSignal == SELL_SIGNAL) { class=class="str">"cmt">/* process SELL signal */ }
    }

PSAR 信号过滤与箭头绘制的参数骨架

下面这段 MQL5 输入与全局定义,决定了 PSAR 反转信号要不要画箭头、怎么画。SARStep 设 0.1、SARMaximum 设 1.0,是抛物线加速因子的常规起手;MinConsecutiveDots 要求至少 2 根同向 K 线才认趋势,MaxDotGapPercentage 限制相邻点差不超过收盘价的 1.0%,这两个过滤项直接砍掉毛刺反转。 EnableAlerts / EnableSound / EnableArrows 三个布尔开关分开管弹窗、声音和图表箭头;BuyArrowSymbol 用 Wingdings 的 "233"(上箭)、SellArrowSymbol 用 "234"(下箭),ArrowOffsetMultiplier=5 控制箭头离价的偏移倍数,实盘里黄金 M5 常要把它调到 8~10 才不被 K 线吞掉。 pendingSignal 配合 waitCount、pendingReversalLevel 做“待确认”机制:检测到反转先不画,等新收 bar 数累积后再落锤,能降低假突破概率,但也会慢半拍。外汇与贵金属波动剧烈,这套参数仅降低噪声,不消除方向误判风险。

MQL5 / C++
input class="type">class="kw">double SARStep     = class="num">0.1;   class=class="str">"cmt">// Acceleration factor for PSAR
input class="type">class="kw">double SARMaximum = class="num">1;    class=class="str">"cmt">// Maximum acceleration for PSAR
class=class="str">"cmt">// Input parameters for refining the signal based on PSAR dots
input class="type">int    MinConsecutiveDots = class="num">2;           class=class="str">"cmt">// Require at least class="num">2 consecutive bars in one trend before reversal
input class="type">class="kw">double MaxDotGapPercentage  = class="num">1.0;       class=class="str">"cmt">// Maximum allowed gap between consecutive PSAR dots(% of current close)
class=class="str">"cmt">// Input parameters for alerts and arrow drawing
input class="type">bool   EnableAlerts   = true;            class=class="str">"cmt">// Enable popup alerts
input class="type">bool   EnableSound    = true;            class=class="str">"cmt">// Enable sound alerts
input class="type">bool   EnableArrows   = true;            class=class="str">"cmt">// Draw arrows on chart
input class="type">class="kw">string BuyArrowSymbol  = "class="num">233";          class=class="str">"cmt">// Wingdings up arrow(as class="type">class="kw">string)
input class="type">class="kw">string SellArrowSymbol = "class="num">234";          class=class="str">"cmt">// Wingdings down arrow(as class="type">class="kw">string)
input class="type">int    ArrowWidth     = class="num">2;               class=class="str">"cmt">// Arrow thickness
input class="type">class="kw">double ArrowOffsetMultiplier = class="num">5;        class=class="str">"cmt">// Multiplier for arrow placement offset
class=class="str">"cmt">// Global indicator handle for PSAR
class="type">int sarHandle = INVALID_HANDLE;
class=class="str">"cmt">// Global variable to track last processed bar time
class="type">class="kw">datetime lastBarTime = class="num">0;
class=class="str">"cmt">// Enumeration for signal types
enum SignalType
  {
  NO_SIGNAL,
  BUY_SIGNAL,
  SELL_SIGNAL
  };
class=class="str">"cmt">// Global variables for pending signal mechanism
SignalType pendingSignal = NO_SIGNAL;
class="type">int        waitCount     = class="num">0;                 class=class="str">"cmt">// Counts new closed bars since signal detection
class="type">class="kw">double      pendingReversalLevel = class="num">0.0; class=class="str">"cmt">// Stores the PSAR value at signal detection
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| DrawSignalArrow - Draws an arrow object on the chart               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void DrawSignalArrow(class="type">class="kw">string prefix, class="type">class="kw">datetime barTime, class="type">class="kw">double price, class="type">class="kw">color arrowColor, class="type">long arrowCode)
  {
  class="type">class="kw">string arrowName = prefix + "_" + TimeToString(barTime, TIME_SECONDS);
class=class="str">"cmt">// Remove existing object with the same name to prevent duplicates
  if(ObjectFind(class="num">0, arrowName) != -class="num">1)
     ObjectDelete(class="num">0, arrowName);
  if(ObjectCreate(class="num">0, arrowName, OBJ_ARROW, class="num">0, barTime, price))
    {
     ObjectSetInteger(class="num">0, arrowName, OBJPROP_COLOR, arrowColor);
     ObjectSetInteger(class="num">0, arrowName, OBJPROP_ARROWCODE, arrowCode);

「信号判定与K线刷新这两个底层函数」

PSAR 翻转配合价格收线,是这套策略触发箭头的前提。CheckForSignal 把数组下标做了硬性映射:0 是刚收盘的当前候选 bar,1 是上一根,2 是再前一根,读代码时别把索引顺手当成时间顺序的反向。 买信号要求 close0>open0 且 sar0 已落到 close0 下方,同时 sar1、sar2 都在对应收盘价上方——也就是 PSAR 点从价格上方翻到下方,且之前至少连续两根处于空头点阵。代码里用 countBearish 累加 sar1、sar2 高于收盘的次数,再和 MinConsecutiveDots 比;若设 MinConsecutiveDots=2,则必须两根都满足条件才放行。 sar1 与 sar2 的绝对差被 MathAbs 取出,和 (MaxDotGapPercentage/100)*close0 这根阈值比。比如 MaxDotGapPercentage 填 0.5、close0 是 1.2000,点距超过 0.006 就过滤掉,避免点阵拉伸过大时的假翻转。卖信号逻辑对称,close0<open0 且 sar0 站上 close0,前两根点阵在价格下。 IsNewBar 不依赖指标重绘,直接用 CopyTime 取最近一根已闭 bar 的时间戳,和模块级变量 lastBarTime 比对。不等就更新并返回 true,等于就 false。外汇和贵金属波动大、滑点凶,这种基于闭 bar 的触发比每 tick 判断更不容易在盯盘时被毛刺骗进单。 把这两段直接塞进 EA,开 MT5 用欧美对 1H 跑一遍,把 MinConsecutiveDots 从 2 调到 1,看箭头密度怎么变——这是验证信号层有没有写错的最快办法。

MQL5 / C++
SignalType CheckForSignal(const class="type">class="kw">double &sarArray[], const class="type">class="kw">double &openArray[], const class="type">class="kw">double &closeArray)
  {
class=class="str">"cmt">// Mapping indices:
class=class="str">"cmt">// Index class="num">0: Last closed bar(current candidate)
class=class="str">"cmt">// Index class="num">1: Previous bar
class=class="str">"cmt">// Index class="num">2: Bar before previous
   class="type">class="kw">double sar0 = sarArray[class="num">0], sar1 = sarArray[class="num">1], sar2 = sarArray[class="num">2];
   class="type">class="kw">double open0 = openArray[class="num">0], close0 = closeArray[class="num">0];
   class="type">class="kw">double open1 = openArray[class="num">1], close1 = closeArray[class="num">1];
   class="type">class="kw">double open2 = openArray[class="num">2], close2 = closeArray[class="num">2];
class=class="str">"cmt">// Check for BUY signal:
   if((close0 > open0) && (sar0 < close0) &&
       (sar1 > close1) && (sar2 > close2))
     {
       class="type">int countBearish = class="num">0;
       if(sar1 > close1)
         countBearish++;
       if(sar2 > close2)
         countBearish++;
       class="type">class="kw">double dotGap = MathAbs(sar1 - sar2);
       class="type">class="kw">double gapThreshold = (MaxDotGapPercentage / class="num">100.0) * close0;
       if(countBearish >= MinConsecutiveDots && dotGap <= gapThreshold)
         class="kw">return BUY_SIGNAL;
     }
class=class="str">"cmt">// Check for SELL signal:
   if((close0 < open0) && (sar0 > close0) &&
       (sar1 < close1) && (sar2 < close2))
     {
       class="type">int countBullish = class="num">0;
       if(sar1 < close1)
         countBullish++;
       if(sar2 < close2)
         countBullish++;
       class="type">class="kw">double dotGap = MathAbs(sar1 - sar2);
       class="type">class="kw">double gapThreshold = (MaxDotGapPercentage / class="num">100.0) * close0;
       if(countBullish >= MinConsecutiveDots && dotGap <= gapThreshold)
         class="kw">return SELL_SIGNAL;
     }
   class="kw">return NO_SIGNAL;
  }

class="type">bool IsNewBar()
  {
   class="type">class="kw">datetime times[];
   if(CopyTime(_Symbol, _Period, class="num">1, class="num">1, times) <= class="num">0)
     {
       Print("Failed to retrieve bar time in IsNewBar().");
       class="kw">return false;
     }
   if(times[class="num">0] != lastBarTime)
     {
       lastBarTime = times[class="num">0];
       class="kw">return true;
     }
   class="kw">return false;
  }

◍ 把抛物线转向逻辑落进EA骨架

在 MT5 里跑 PSAR 信号,第一步是拿到指标句柄并在初始化阶段就拦掉失败。下面这段 OnInit 用 iSAR 绑定当前品种与周期,步长 SARStep 和上限 SARMaximum 是外部参数,若返回 INVALID_HANDLE 直接 INIT_FAILED,避免后续空跑。 OnDeinit 里除了释放句柄,还用 ObjectsDeleteAll(0, (int)OBJ_ARROW, 0) 清掉图表上的箭头对象。实盘切换周期或移除 EA 时,残留箭头会误导肉眼判断,这个清理不能省。 OnTick 的核心约束是「每根收盘 Bar 只处理一次」:用 IsNewBar() 拦截,否则同根 Bar 内多次报价会重复发信号。CopyBuffer 取最近 4 个 PSAR 值,但只要求 >=3 个有效,说明倒数第 4 根可能尚在成型,逻辑上只用前 3 根做确认。 pendingSignal 机制给信号留了等待窗口:BUY_SIGNAL 挂起后 waitCount 自增,若最新收盘 closeArray[0] 跌破 pendingReversalLevel 就判定反转到位平仓。外汇与贵金属波动大,这种二次确认能过滤部分假突破,但无法消除跳空造成的滑点风险,参数需自己在历史数据上回测。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">// Create the built-in Parabolic SAR indicator handle
   sarHandle = iSAR(_Symbol, _Period, SARStep, SARMaximum);
   if(sarHandle == INVALID_HANDLE)
     {
       Print("Error creating PSAR handle");
       class="kw">return INIT_FAILED;
     }
   Print("SAR EA initialized successfully.");
   class="kw">return INIT_SUCCEEDED;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
   if(sarHandle != INVALID_HANDLE)
      IndicatorRelease(sarHandle);
class=class="str">"cmt">// Remove all arrow objects from the current chart window
   ObjectsDeleteAll(class="num">0, (class="type">int)OBJ_ARROW, class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">// Process only once per new closed bar
   if(!IsNewBar())
      class="kw">return;
class=class="str">"cmt">// Retrieve the last class="num">4 PSAR values(we require at least class="num">3 values)
   class="type">class="kw">double sarArray[class="num">4];
   if(CopyBuffer(sarHandle, class="num">0, class="num">1, class="num">4, sarArray) < class="num">3)
     {
       Print("Failed to retrieve PSAR data.");
       class="kw">return;
     }
class=class="str">"cmt">// Retrieve the last class="num">4 bars&class="macro">#x27; price data(Open and Close)
   class="type">class="kw">double openArray[class="num">4], closeArray[class="num">4];
   if(CopyOpen(_Symbol, _Period, class="num">1, class="num">4, openArray) < class="num">3 ||
      CopyClose(_Symbol, _Period, class="num">1, class="num">4, closeArray) < class="num">3)
     {
       Print("Failed to retrieve price data.");
       class="kw">return;
     }
class=class="str">"cmt">// Process pending signal logic if a signal is waiting confirmation
   if(pendingSignal != NO_SIGNAL)
     {
       waitCount++; class=class="str">"cmt">// Increment the waiting counter
       if(pendingSignal == BUY_SIGNAL)
         {
          if(closeArray[class="num">0] <= pendingReversalLevel)
            {
             Print("Reversal level reached for BUY signal. Close here.");
             if(EnableAlerts)
让小布替你跑这套反转诊断
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到PSAR翻面与动能加速的实时标注,把重复劳动交给小布,你专注决策。

常见问题

不一定。SAR翻面只表示当前趋势动能可能衰减,概率上倾向反转但需前两根K线方向与PSAR位置共同确认,外汇贵金属属高风险市场仍可能假突破。
小布盯盘目前提供PSAR翻转与价差阈值的视觉诊断,不直接执行EA交易;你可参照本文信号逻辑在MQL5中自建,再用小布做盘口复核。
没有统一值,倾向用近期波动率缩放阈值;复盘不同周期可见黄金1H与欧美1H适配区间差异明显,建议用历史样本标定。
iSAR返回单缓冲区指标,索引0即SAR值数组,按时间序列从老到新排列,循环中需注意与K线开盘收盘数组下标对齐。