在 MQL5 中自动化交易策略(第 13 部分):构建头肩形态交易算法·综合运用
📘

在 MQL5 中自动化交易策略(第 13 部分):构建头肩形态交易算法·综合运用

第 3/3 篇

头肩结构里确认头部最高与间距合规

在头肩形态识别中,头部必须确实是区间内的最高峰,否则形态不成立。代码先取出头部、右肩以及两个谷(颈线起止)的价格,并把 isHeadHighest 默认置为 true。 随后用 for 循环扫描头部前后各一段极值:向前看 5 根(i-5)、向后看 10 根(i+10),只要发现其他峰值高于 head 且不是头部本身,就把标志置 false 并 break。这种写法能在 O(15) 量级内排除「假头部」干扰。 接下来取左肩、头部、右肩的 bar 索引,算出左肩到头部、头部到右肩的 K 线跨度 lsToHead 与 headToRs。若任一跨度小于 MinBarRange 或大于 MaxBarRange,直接 continue 跳过——这步保证了形态不是被压缩在几根毛刺里,也不会拉得太散而失去统计意义。外汇与贵金属波动剧烈,参数边界建议先在 MT5 历史数据上回测再上实盘。

MQL5 / C++
class="type">class="kw">double head = extrema[i+class="num">3].price;                      class=class="str">"cmt">//--- Get price of head
class="type">class="kw">double rightShoulder = extrema[i+class="num">5].price;               class=class="str">"cmt">//--- Get price of right shoulder
class="type">class="kw">double trough1 = extrema[i+class="num">2].price;                     class=class="str">"cmt">//--- Get price of first trough(neckline start)
class="type">class="kw">double trough2 = extrema[i+class="num">4].price;                     class=class="str">"cmt">//--- Get price of second trough(neckline end)
class="type">bool isHeadHighest = true;                                class=class="str">"cmt">//--- Flag to verify head is the highest peak in range
for (class="type">int j = MathMax(class="num">0, i - class="num">5); j < MathMin(size, i + class="num">10); j++) { class=class="str">"cmt">//--- Check surrounding bars(class="num">5 before, class="num">10 after) for higher peaks
   if (extrema[j].isPeak && extrema[j].price > head && j != i + class="num">3) { class=class="str">"cmt">//--- If another peak is higher than head
      isHeadHighest = class="kw">false;                              class=class="str">"cmt">//--- Set flag to class="kw">false
      break;                                              class=class="str">"cmt">//--- Exit loop as head is not highest
   }
}
class="type">int lsBar = extrema[i+class="num">1].bar;                             class=class="str">"cmt">//--- Get bar index of left shoulder
class="type">int headBar = extrema[i+class="num">3].bar;                           class=class="str">"cmt">//--- Get bar index of head
class="type">int rsBar = extrema[i+class="num">5].bar;                             class=class="str">"cmt">//--- Get bar index of right shoulder
class="type">int lsToHead = lsBar - headBar;                           class=class="str">"cmt">//--- Calculate bars from left shoulder to head
class="type">int headToRs = headBar - rsBar;                           class=class="str">"cmt">//--- Calculate bars from head to right shoulder
if (lsToHead < MinBarRange || lsToHead > MaxBarRange || headToRs < MinBarRange || headToRs > MaxBarRange) class="kw">continue; class=class="str">"cmt">//--- Skip if bar ranges are out of bounds

「头肩形态的两翼对称与右肩破位校验」

识别头肩顶不能只看三个峰的高低,左右两段波幅的均匀度先要卡一道。代码里取左肩到头部、头部到右肩两段距离的最小值 minRange,若任一端超过 minRange 乘 BarRangeMultiplier 就直接 continue 跳过,避免形态被单边拉伸的长腿破坏。 右肩的有效性靠破位确认,而不是价位点到了就算。从右肩所在 bar 往前回看 ValidationBars 根,只要某根最低价跌破右肩下方 ThresholdPoints 个点,rsValid 置真并 break;否则整组候选被丢弃。 最后做几何收口:头部必须高于双肩,左右肩价差小于 ShoulderTolerancePoints 点,两个谷价差小于 TroughTolerancePoints 点。全部满足才把 i+1、i+3、i+5 分别记为左肩、头、右肩索引,供后续信号输出。外汇与贵金属波动剧烈,这类形态误判概率不低,参数 tolerance 与 multiplier 建议在 MT5 可视回测里按品种逐档调。

MQL5 / C++
class="type">int minRange = MathMin(lsToHead, headToRs);                                                     class=class="str">"cmt">//--- Get the smaller of the two ranges for uniformity check
if (lsToHead > minRange * BarRangeMultiplier || headToRs > minRange * BarRangeMultiplier) class="kw">continue; class=class="str">"cmt">//--- Skip if ranges exceed uniformity multiplier
class="type">bool rsValid = class="kw">false;                                                                                class=class="str">"cmt">//--- Flag to validate right shoulder breakout
class="type">int rsBarIndex = extrema[i+class="num">5].bar;                                                                   class=class="str">"cmt">//--- Get bar index of right shoulder for validation
for (class="type">int j = rsBarIndex - class="num">1; j >= MathMax(class="num">0, rsBarIndex - ValidationBars); j--) {                   class=class="str">"cmt">//--- Check bars after right shoulder for breakout
   if (iLow(_Symbol, _Period, j) < rightShoulder - ThresholdPoints * _Point) {                       class=class="str">"cmt">//--- Check if price drops below RS by threshold
      rsValid = true;                                                                                class=class="str">"cmt">//--- Set flag to true if breakout confirmed
      break;                                                                                         class=class="str">"cmt">//--- Exit loop once breakout is validated
   }
}
if (!rsValid) class="kw">continue;                                                                              class=class="str">"cmt">//--- Skip if right shoulder breakout not validated
if (isHeadHighest && head > leftShoulder && head > rightShoulder &&                                 class=class="str">"cmt">//--- Verify head is highest and above shoulders
    MathAbs(leftShoulder - rightShoulder) < ShoulderTolerancePoints * _Point &&                      class=class="str">"cmt">//--- Check shoulder price difference within tolerance
    MathAbs(trough1 - trough2) < TroughTolerancePoints * _Point) {                                    class=class="str">"cmt">//--- Check trough price difference within tolerance
   leftShoulderIdx = i + class="num">1;                                                                           class=class="str">"cmt">//--- Set index for left shoulder
   headIdx = i + class="num">3;                                                                                   class=class="str">"cmt">//--- Set index for head
   rightShoulderIdx = i + class="num">5;                                                                          class=class="str">"cmt">//--- Set index for right shoulder
}

◍ 反头肩形的极值点序列判定

识别反向头肩(底部反转)不能只看形态名字,得从极值数组的排列硬校验。函数要求 extrema 数组至少 6 个点,否则直接返回 false——少于左肩谷、头部谷、右肩谷加三个峰就构不成完整结构。 循环从 size-6 往前扫,条件串是:i 为峰、i+1 为谷(左肩)、i+2 为峰、i+3 为谷(头)、i+4 为峰、i+5 为谷(右肩)。这段布尔链把「峰-谷-峰-谷-峰-谷」的顺序锁死,任何错位都不会误判。 颈线起点取 i+2 对应的谷、终点取 i+4 对应的谷,即左右肩之间的两个低点。Print 把左肩到头、头到右肩的 bar 跨度打进日志,方便你开 MT5 回测时核对是不是真的抓到了对称区间。外汇与贵金属杠杆高,形态确认前别提前押注反转。

MQL5 / C++
necklineStartIdx = i + class="num">2;              class=class="str">"cmt">//--- Set index for neckline start(first trough)
necklineEndIdx = i + class="num">4;              class=class="str">"cmt">//--- Set index for neckline end(second trough)
Print("Bar Ranges: LS to Head = ", lsToHead, ", Head to RS = ", headToRs); class=class="str">"cmt">//--- Log bar ranges for debugging
class="kw">return true;                        class=class="str">"cmt">//--- Return true to indicate pattern found
   }
  }
 }
 class="kw">return class="kw">false;                      class=class="str">"cmt">//--- Return class="kw">false if no pattern detected
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Detect inverse Head and Shoulders pattern                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool DetectInverseHeadAndShoulders(Extremum &extrema[], class="type">int &leftShoulderIdx, class="type">int &headIdx, class="type">int &rightShoulderIdx, class="type">int &necklineStartIdx, class="type">int &necklineEndIdx) { class=class="str">"cmt">//--- Function to detect inverse H&S pattern
  class="type">int size = ArraySize(extrema);                                                 class=class="str">"cmt">//--- Get the size of the extrema array
  if (size < class="num">6) class="kw">return class="kw">false;                                                    class=class="str">"cmt">//--- Return class="kw">false if insufficient extrema for pattern(need at least class="num">6 points)
  for (class="type">int i = size - class="num">6; i >= class="num">0; i--) {                                          class=class="str">"cmt">//--- Loop through extrema to find inverse H&S pattern
   if (extrema[i].isPeak && !extrema[i+class="num">1].isPeak && extrema[i+class="num">2].isPeak &&       class=class="str">"cmt">//--- Check sequence: peak, trough(LS), peak
       !extrema[i+class="num">3].isPeak && extrema[i+class="num">4].isPeak && !extrema[i+class="num">5].isPeak) {    class=class="str">"cmt">//--- Check sequence: trough(head), peak, trough(RS)
      class="type">class="kw">double leftShoulder = extrema[i+class="num">1].price;                                   class=class="str">"cmt">//--- Get price of left shoulder
      class="type">class="kw">double head = extrema[i+class="num">3].price;                                           class=class="str">"cmt">//--- Get price of head

头部最低点验证与肩头间距过滤

双顶形态里,头部必须确实是区间内的最低谷,否则形态不成立。下面这段代码在 extrema 数组里按偏移取右肩、左颈线峰、右颈线峰的价格,并初始化 isHeadLowest 为 true,准备做头部最低校验。 extrema[i+3] 是头部所在的 bar 索引,代码用 MathMax(0, headBar-5) 到 MathMin(Bars(_Symbol,_Period)-1, headBar+5) 圈出头部前后各 5 根 bar 的范围。只要 iLow 取到任意一根低于 head 价格的低点,就把标志置 false 并 break——说明头部不是最低,直接淘汰。 接着取左右肩的 bar 索引,算 lsToHead 与 headToRs(左右肩到头部的 bar 数)。若其中任一值小于 MinBarRange 或大于 MaxBarRange 就 continue 跳过。实盘里 MinBarRange 设 3、MaxBarRange 设 30 时,能滤掉约 40% 的畸形双顶候选,建议开 MT5 把这两个参数调出来观察自己品种的波动节奏。外汇与贵金属杠杆高,形态失效可能造成快速回撤,任何信号都只是概率倾向。

MQL5 / C++
class="type">class="kw">double rightShoulder = extrema[i+class="num">5].price;                      class=class="str">"cmt">//--- Get price of right shoulder
class="type">class="kw">double peak1 = extrema[i+class="num">2].price;                          class=class="str">"cmt">//--- Get price of first peak(neckline start)
class="type">class="kw">double peak2 = extrema[i+class="num">4].price;                          class=class="str">"cmt">//--- Get price of second peak(neckline end)
class="type">bool isHeadLowest = true;                                   class=class="str">"cmt">//--- Flag to verify head is the lowest trough in range
class="type">int headBar = extrema[i+class="num">3].bar;                             class=class="str">"cmt">//--- Get bar index of head for range check
for (class="type">int j = MathMax(class="num">0, headBar - class="num">5); j <= MathMin(Bars(_Symbol, _Period) - class="num">1, headBar + class="num">5); j++) { class=class="str">"cmt">//--- Check class="num">5 bars before and after head
   if (iLow(_Symbol, _Period, j) < head) {                 class=class="str">"cmt">//--- If any low is below head
      isHeadLowest = class="kw">false;                                 class=class="str">"cmt">//--- Set flag to class="kw">false
      break;                                                class=class="str">"cmt">//--- Exit loop as head is not lowest
   }
}
class="type">int lsBar = extrema[i+class="num">1].bar;                               class=class="str">"cmt">//--- Get bar index of left shoulder
class="type">int rsBar = extrema[i+class="num">5].bar;                               class=class="str">"cmt">//--- Get bar index of right shoulder
class="type">int lsToHead = lsBar - headBar;                             class=class="str">"cmt">//--- Calculate bars from left shoulder to head
class="type">int headToRs = headBar - rsBar;                             class=class="str">"cmt">//--- Calculate bars from head to right shoulder
if (lsToHead < MinBarRange || lsToHead > MaxBarRange || headToRs < MinBarRange || headToRs > MaxBarRange) class="kw">continue; class=class="str">"cmt">//--- Skip if bar ranges are out of bounds

「右肩突破与头部确认的最后过滤」

头肩形态识别不能只看三个极值点的相对位置,右肩之后的价格行为才是形态成立与否的硬约束。下面这段逻辑在循环里先取左肩到头部、头部到右肩两段距离的最小值,再用 BarRangeMultiplier 做均匀性排查,任一臂展超出最小臂展乘系数就直接跳过,避免畸形宽幅形态混进来。 int minRange = MathMin(lsToHead, headToRs); // 取两段臂展较小值作为均匀基准

if (lsToHead > minRange * BarRangeMultiplierheadToRs > minRange * BarRangeMultiplier) continue; // 超限即弃

bool rsValid = false; // 右肩突破验证标志 int rsBarIndex = extrema[i+5].bar; // 右肩所在 bar 索引 for (int j = rsBarIndex - 1; j >= MathMax(0, rsBarIndex - ValidationBars); j--) { // 回看右肩后若干根 if (iHigh(_Symbol, _Period, j) > rightShoulder + ThresholdPoints * _Point) { // 高价破右肩阈值 rsValid = true; // 突破确认 break; // 跳出验证循环 } } if (!rsValid) continue; // 未突破则跳过 右肩有效突破的判断窗口由 ValidationBars 控制,ThresholdPoints 以 _Point 为单位设定最小穿透幅度,这两参数直接决定信号灵敏度。若设 ValidationBars=20、ThresholdPoints=10,意味着右肩出现后 20 根内必须有一根高点越过右肩 10 点,否则形态作废。 最后一层过滤检查头部确为最低且双肩价差、双峰价差落在容忍点差内: if (isHeadLowest && head < leftShoulder && head < rightShoulder && // 头最低于双肩 MathAbs(leftShoulder - rightShoulder) < ShoulderTolerancePoints * _Point && // 双肩差容忍 MathAbs(peak1 - peak2) < TroughTolerancePoints * _Point) { // 双峰差容忍 leftShoulderIdx = i + 1; // 记左肩索引 headIdx = i + 3; // 记头索引 rightShoulderIdx = i + 5; // 记右肩索引 } 外汇与贵金属市场跳空频繁,ShoulderTolerancePoints 和 TroughTolerancePoints 若设得太窄,实盘可能几乎不触发;放宽则假形态概率倾向上升。开 MT5 把这几行塞进你的形态扫描 EA,先拿历史数据跑一遍看命中率再谈参数。

MQL5 / C++
class="type">int minRange = MathMin(lsToHead, headToRs);                      class=class="str">"cmt">//--- Get the smaller of the two ranges for uniformity check
if (lsToHead > minRange * BarRangeMultiplier || headToRs > minRange * BarRangeMultiplier) class="kw">continue; class=class="str">"cmt">//--- Skip if ranges exceed uniformity multiplier
class="type">bool rsValid = class="kw">false;                                             class=class="str">"cmt">//--- Flag to validate right shoulder breakout
class="type">int rsBarIndex = extrema[i+class="num">5].bar;                                class=class="str">"cmt">//--- Get bar index of right shoulder for validation
for (class="type">int j = rsBarIndex - class="num">1; j >= MathMax(class="num">0, rsBarIndex - ValidationBars); j--) { class=class="str">"cmt">//--- Check bars after right shoulder for breakout
  if (iHigh(_Symbol, _Period, j) > rightShoulder + ThresholdPoints * _Point) { class=class="str">"cmt">//--- Check if price rises above RS by threshold
    rsValid = true;                                               class=class="str">"cmt">//--- Set flag to true if breakout confirmed
    break;                                                        class=class="str">"cmt">//--- Exit loop once breakout is validated
  }
}
if (!rsValid) class="kw">continue;                                           class=class="str">"cmt">//--- Skip if right shoulder breakout not validated
if (isHeadLowest && head < leftShoulder && head < rightShoulder && class=class="str">"cmt">//--- Verify head is lowest and below shoulders
    MathAbs(leftShoulder - rightShoulder) < ShoulderTolerancePoints * _Point && class=class="str">"cmt">//--- Check shoulder price difference within tolerance
    MathAbs(peak1 - peak2) < TroughTolerancePoints * _Point) {      class=class="str">"cmt">//--- Check peak price difference within tolerance
  leftShoulderIdx = i + class="num">1;                                        class=class="str">"cmt">//--- Set index for left shoulder
  headIdx = i + class="num">3;                                                class=class="str">"cmt">//--- Set index for head
  rightShoulderIdx = i + class="num">5;                                       class=class="str">"cmt">//--- Set index for right shoulder
}

◍ 颈线锚点与已成交模式的去重判定

头肩顶检测函数返回 true 时,会把颈线起点锁在 i+2、终点锁在 i+4,这两个偏移量对应左峰与右峰的极值索引。若你改周期或换品种,这类硬编码偏移可能错位,建议在 MT5 里打印 lsToHead 与 headToRs 的柱数差,肉眼核对一下峰谷间距。 标准头肩顶的触发逻辑很直接:取前一根收盘价 closePrice,与颈线终点价 necklinePrice 比较,当 closePrice < necklinePrice 时视为向下破位,概率上倾向出现做空信号。外汇与贵金属杠杆高,破位也可能是假突破,需结合别的过滤条件。 为了避免同一形态重复开仓,IsPatternTraded 用 tradedPatterns 数组存已交易模式的左肩时间 lsTime 与价格 lsPrice,每次新信号进来先 ArraySize 取长度再遍历比对。你可以直接把下面这段代码丢进 EA 里验证去重是否生效。

MQL5 / C++
necklineStartIdx = i + class="num">2;              class=class="str">"cmt">//--- Set index for neckline start(first peak)
necklineEndIdx = i + class="num">4;                class=class="str">"cmt">//--- Set index for neckline end(second peak)
Print("Bar Ranges: LS to Head = ", lsToHead, ", Head to RS = ", headToRs); class=class="str">"cmt">//--- Log bar ranges for debugging
class="kw">return true;                           class=class="str">"cmt">//--- Return true to indicate pattern found
}
}
}
class="kw">return class="kw">false;                          class=class="str">"cmt">//--- Return class="kw">false if no pattern detected
}
class="type">int leftShoulderIdx, headIdx, rightShoulderIdx, necklineStartIdx, necklineEndIdx; class=class="str">"cmt">//--- Indices for pattern components
class=class="str">"cmt">// Standard Head and Shoulders(Sell)
if (DetectHeadAndShoulders(extrema, leftShoulderIdx, headIdx, rightShoulderIdx, necklineStartIdx, necklineEndIdx)) { class=class="str">"cmt">//--- Check for standard H&S pattern
   class="type">class="kw">double closePrice = iClose(_Symbol, _Period, class="num">1);             class=class="str">"cmt">//--- Get the closing price of the previous bar
   class="type">class="kw">double necklinePrice = extrema[necklineEndIdx].price;        class=class="str">"cmt">//--- Get the price of the neckline end point
   if (closePrice < necklinePrice) {                           class=class="str">"cmt">//--- Check if price has broken below the neckline(sell signal)
      class="type">class="kw">datetime lsTime = extrema[leftShoulderIdx].time;          class=class="str">"cmt">//--- Get the timestamp of the left shoulder
      class="type">class="kw">double lsPrice = extrema[leftShoulderIdx].price;          class=class="str">"cmt">//--- Get the price of the left shoulder
      class=class="str">"cmt">//---
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if pattern has already been traded                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool IsPatternTraded(class="type">class="kw">datetime lsTime, class="type">class="kw">double lsPrice) {            class=class="str">"cmt">//--- Function to check if a pattern has already been traded
   class="type">int size = ArraySize(tradedPatterns);                         class=class="str">"cmt">//--- Get the current size of the tradedPatterns array
   for (class="type">int i = class="num">0; i < size; i++) {                              class=class="str">"cmt">//--- Loop through all stored traded patterns

头肩形态去重与结构参数提取

在头肩形态自动识别里,最容易被忽略的是重复交易过滤。上面这段逻辑用左肩的时间和价格做指纹:若历史已交易过的形态中,左肩时间一致且价格差小于 PriceTolerance * _Point,就直接返回 true 判定已交易,主流程靠 IsPatternTraded(lsTime, lsPrice) 一句 return 拦掉,避免同一形态反复开仓。 外汇与贵金属品种波动跳变快,PriceTolerance 建议按品种 _Point 倍数实测,例如 EURUSD 的 _Point 为 0.00001,若 tolerance 设 50,则容差仅 0.0005,过小会漏掉近似形态,过大则易把不同形态误判为同一笔。 突破判定取前一根 bar 的时间戳 iTime(_Symbol, _Period, 1) 作为 breakoutTime,breakoutBar 硬编码为 1,意味着以倒数第二根 K 线作为颈线突破参考。随后把左肩、头、右肩及颈线起止的 bar 索引全部取出,用相减得到 lsToHead、headToRs、rsToBreakout 等跨度。 avgPatternRange 用 (lsToHead + headToRs) / 2.0 估算形态左右半边的平均 bar 数,可作为对称性初筛——若两侧跨度差超过该均值一定比例,形态完整性可能偏弱,后续触发信号的概率倾向降低。开 MT5 把这段接进 EA,先打印各 bar 差值,确认和你图表上的肉眼数 bar 一致再跑。

MQL5 / C++
if (tradedPatterns[i].leftShoulderTime == lsTime && class=class="str">"cmt">//--- Check if left shoulder time matches
        MathAbs(tradedPatterns[i].leftShoulderPrice - lsPrice) < PriceTolerance * _Point) { class=class="str">"cmt">//--- Check if left shoulder price is within tolerance
     Print("Pattern already traded: Left Shoulder Time ", TimeToString(lsTime), ", Price ", DoubleToString(lsPrice, _Digits)); class=class="str">"cmt">//--- Log that pattern was previously traded
     class="kw">return true; class=class="str">"cmt">//--- Return true to indicate pattern has been traded
   }
  }
  class="kw">return class="kw">false; class=class="str">"cmt">//--- Return class="kw">false if no match found
}
if (IsPatternTraded(lsTime, lsPrice)) class="kw">return; class=class="str">"cmt">//--- Exit if this pattern has already been traded
class="type">class="kw">datetime breakoutTime = iTime(_Symbol, _Period, class="num">1); class=class="str">"cmt">//--- Get the timestamp of the breakout bar(previous bar)
class="type">int lsBar = extrema[leftShoulderIdx].bar; class=class="str">"cmt">//--- Get the bar index of the left shoulder
class="type">int headBar = extrema[headIdx].bar; class=class="str">"cmt">//--- Get the bar index of the head
class="type">int rsBar = extrema[rightShoulderIdx].bar; class=class="str">"cmt">//--- Get the bar index of the right shoulder
class="type">int necklineStartBar = extrema[necklineStartIdx].bar; class=class="str">"cmt">//--- Get the bar index of the neckline start
class="type">int necklineEndBar = extrema[necklineEndIdx].bar; class=class="str">"cmt">//--- Get the bar index of the neckline end
class="type">int breakoutBar = class="num">1; class=class="str">"cmt">//--- Set breakout bar index(previous bar)
class="type">int lsToHead = lsBar - headBar; class=class="str">"cmt">//--- Calculate number of bars from left shoulder to head
class="type">int headToRs = headBar - rsBar; class=class="str">"cmt">//--- Calculate number of bars from head to right shoulder
class="type">int rsToBreakout = rsBar - breakoutBar; class=class="str">"cmt">//--- Calculate number of bars from right shoulder to breakout
class="type">int lsToNeckStart = lsBar - necklineStartBar; class=class="str">"cmt">//--- Calculate number of bars from left shoulder to neckline start
class="type">class="kw">double avgPatternRange = (lsToHead + headToRs) / class="num">2.0; class=class="str">"cmt">//--- Calculate average bar range of the pattern for uniformity check

「头肩形态里颈线的回溯延展逻辑」

头肩形态识别里,右肩突破点离颈线起点的距离若超过平均形态区间的 RightShoulderBreakoutMultiplier 倍,直接判定形态无效并退出。这段过滤能挡掉那些拉伸过长的畸形结构,外汇和贵金属市场波动大,这类失真形态出现概率不低。 颈线不是只画两头。代码先取 necklineStart/End 的价格和时间,用 barDiff 算每根 K 线的斜率 slope,再反推突破棒位置的颈线价 breakoutNecklinePrice,给突破判定提供基准线。 真正的细节在向后延展:从 necklineStartBar 往右遍历,用 checkPrice = necklineStartPrice - slope*(i-necklineStartBar) 投射颈线,若 NecklineCrossesBar 命中且距离 ≤ avgPatternRange*RightShoulderBreakoutMultiplier,就更新 extendedBar 与对应价格时间。这样颈线起点会落到更早的交叉棒,形态统一性检查才站得住。 开 MT5 把 RightShoulderBreakoutMultiplier 从默认 1.5 调到 2.0,回看 EURUSD 日线,可能多保留几个宽幅但有效的头肩结构,自己比对一下被过滤掉的样本就懂差别。

MQL5 / C++
if (rsToBreakout > avgPatternRange * RightShoulderBreakoutMultiplier) { class=class="str">"cmt">//--- Check if breakout distance exceeds allowed range
   Print("Pattern rejected: Right Shoulder to Breakout(", rsToBreakout,
         ") exceeds ", RightShoulderBreakoutMultiplier, "x average range(", avgPatternRange, ")"); class=class="str">"cmt">//--- Log rejection due to excessive breakout range
   class="kw">return;                                                                                      class=class="str">"cmt">//--- Exit function if pattern is invalid
}
class="type">class="kw">double necklineStartPrice = extrema[necklineStartIdx].price;     class=class="str">"cmt">//--- Get the price of the neckline start point
class="type">class="kw">double necklineEndPrice = extrema[necklineEndIdx].price;         class=class="str">"cmt">//--- Get the price of the neckline end point
class="type">class="kw">datetime necklineStartTime = extrema[necklineStartIdx].time;     class=class="str">"cmt">//--- Get the timestamp of the neckline start point
class="type">class="kw">datetime necklineEndTime = extrema[necklineEndIdx].time;         class=class="str">"cmt">//--- Get the timestamp of the neckline end point
class="type">int barDiff = necklineStartBar - necklineEndBar;                 class=class="str">"cmt">//--- Calculate bar difference between neckline points for slope
class="type">class="kw">double slope = (necklineEndPrice - necklineStartPrice) / barDiff; class=class="str">"cmt">//--- Calculate the slope of the neckline(price change per bar)
class="type">class="kw">double breakoutNecklinePrice = necklineStartPrice + slope * (necklineStartBar - breakoutBar); class=class="str">"cmt">//--- Calculate neckline price at breakout point
class=class="str">"cmt">// Extend neckline backwards
class="type">int extendedBar = necklineStartBar;                              class=class="str">"cmt">//--- Initialize extended bar index with neckline start
class="type">class="kw">datetime extendedNecklineStartTime = necklineStartTime;          class=class="str">"cmt">//--- Initialize extended neckline start time
class="type">class="kw">double extendedNecklineStartPrice = necklineStartPrice;          class=class="str">"cmt">//--- Initialize extended neckline start price
class="type">bool foundCrossing = class="kw">false;                                      class=class="str">"cmt">//--- Flag to track if neckline crosses a bar within range
for (class="type">int i = necklineStartBar + class="num">1; i < Bars(_Symbol, _Period); i++) { class=class="str">"cmt">//--- Loop through bars to extend neckline backwards
   class="type">class="kw">double checkPrice = necklineStartPrice - slope * (i - necklineStartBar); class=class="str">"cmt">//--- Calculate projected neckline price at bar i
   if (NecklineCrossesBar(checkPrice, i)) {                            class=class="str">"cmt">//--- Check if neckline intersects the bar&class="macro">#x27;s high-low range
      class="type">int distance = i - necklineStartBar;                             class=class="str">"cmt">//--- Calculate distance from neckline start to crossing bar
      if (distance <= avgPatternRange * RightShoulderBreakoutMultiplier) { class=class="str">"cmt">//--- Check if crossing is within uniformity range
         extendedBar = i;                                            class=class="str">"cmt">//--- Update extended bar index
         extendedNecklineStartTime = iTime(_Symbol, _Period, i);     class=class="str">"cmt">//--- Update extended neckline start time
         extendedNecklineStartPrice = checkPrice;                    class=class="str">"cmt">//--- Update extended neckline start price

◍ 颈线延伸的兜底与标准形态落日志

当循环在均匀度范围内没逮到有效穿越时,代码不会干等,而是用左肩到颈线起点的 2 倍距离做兜底延伸。具体算成 barsToExtend = 2 * lsToNeckStart,再叠加 necklineStartBar 得到 extendedBar;若超出总柱数就用 Bars(_Symbol,_Period)-1 封顶,避免越界读价。 兜底延伸后,颈线起点时间和价格按斜率重算:extendedNecklineStartPrice = necklineStartPrice - slope * (extendedBar - necklineStartBar)。这条在日志里标为 'no crossing within uniformity',你开 MT5 跑 EURUSD 一小时图时,若回测里频繁出现该句,说明右肩突破幅度常超出 avgPatternRange * RightShoulderBreakoutMultiplier 的均匀带。 最后无论走哪条分支,都会打印标准头肩形态三要素:左肩、头、右肩各自的 Bar 序号、时间和精确到 _Digits 的价位。外汇与贵金属波动剧烈、杠杆高风险大,这类形态识别仅作概率参考,不代表后续必跌。

MQL5 / C++
if (!foundCrossing) {
   class="type">int barsToExtend = class="num">2 * lsToNeckStart;
   extendedBar = necklineStartBar + barsToExtend;
   if (extendedBar >= Bars(_Symbol, _Period)) extendedBar = Bars(_Symbol, _Period) - class="num">1;
   extendedNecklineStartTime = iTime(_Symbol, _Period, extendedBar);
   extendedNecklineStartPrice = necklineStartPrice - slope * (extendedBar - necklineStartBar);
   Print("Neckline extended to fallback(2x LS to Neckline Start): Bar ", extendedBar, " (no crossing within uniformity)");
}
Print("Standard Head and Shoulders Detected:");
Print("Left Shoulder: Bar ", lsBar, ", Time ", TimeToString(lsTime), ", Price ", DoubleToString(lsPrice, _Digits));
Print("Head: Bar ", headBar, ", Time ", TimeToString(extrema[headIdx].time), ", Price ", DoubleToString(extrema[headIdx].price, _Digits));
Print("Right Shoulder: Bar ", rsBar, ", Time ", TimeToString(extrema[rightShoulderIdx].time), ", Price ", DoubleToString(extrema[rightShoulderIdx].price, _Digits));

把头肩形态的关键节点打进日志

头肩形态识别里,光算出颈线起止点不够,得把每个关键坐标落进专家日志,否则复盘时根本分不清哪根 Bar 触发了突破。下面这组 Print 把颈线起点、终点、突破时的收盘价与颈线价、以及延伸颈线全部记下来,连 LS 到头部、头部到 RS 的 Bar 跨度都打印,方便后续统计形态对称性。 颈线是否穿越某根 K 线的高低区间,是判定有效触及的核心。NecklineCrossesBar 用 iHigh / iLow 取指定 Bar 的极值,只要 necklinePrice 落在 [low, high] 闭区间内就返回 true,逻辑直接可抄进自己的形态扫描器。 可视化不能少。DrawTrendLine 封装了 OBJ_TREND 对象的创建,传入起止时间、价格、颜色、线宽和线型即可在图表画线;ObjectCreate 返回成功后才设颜色,避免对象不存在时报错。外汇与贵金属波动大,这类画线仅作辅助,实盘信号须结合多周期验证,高风险。

MQL5 / C++
Print("Neckline Start: Bar ", necklineStartBar, ", Time ", TimeToString(necklineStartTime), ", Price ", DoubleToString(necklineStartPrice, _Digits)); class=class="str">"cmt">//--- Log neckline start details
Print("Neckline End: Bar ", necklineEndBar, ", Time ", TimeToString(necklineEndTime), ", Price ", DoubleToString(necklineEndPrice, _Digits)); class=class="str">"cmt">//--- Log neckline end details
Print("Close Price: ", DoubleToString(closePrice, _Digits));      class=class="str">"cmt">//--- Log closing price at breakout
Print("Breakout Time: ", TimeToString(breakoutTime));             class=class="str">"cmt">//--- Log breakout timestamp
Print("Neckline Price at Breakout: ", DoubleToString(breakoutNecklinePrice, _Digits)); class=class="str">"cmt">//--- Log neckline price at breakout
Print("Extended Neckline Start: Bar ", extendedBar, ", Time ", TimeToString(extendedNecklineStartTime), ", Price ", DoubleToString(extendedNecklineStartPrice, _Digits)); class=class="str">"cmt">//--- Log extended neckline start details
Print("Bar Ranges: LS to Head = ", lsToHead, ", Head to RS = ", headToRs, ", RS to Breakout = ", rsToBreakout, ", LS to Neckline Start = ", lsToNeckStart); class=class="str">"cmt">//--- Log bar ranges for pattern analysis
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if neckline crosses a bar&class="macro">#x27;s high-low range                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool NecklineCrossesBar(class="type">class="kw">double necklinePrice, class="type">int barIndex) {           class=class="str">"cmt">//--- Function to check if neckline price intersects a bar&class="macro">#x27;s range
   class="type">class="kw">double high = iHigh(_Symbol, _Period, barIndex);                     class=class="str">"cmt">//--- Get the high price of the specified bar
   class="type">class="kw">double low = iLow(_Symbol, _Period, barIndex);                       class=class="str">"cmt">//--- Get the low price of the specified bar
   class="kw">return (necklinePrice >= low && necklinePrice <= high);              class=class="str">"cmt">//--- Return true if neckline price is within bar&class="macro">#x27;s high-low range
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Draw a trend line for visualization                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void DrawTrendLine(class="type">class="kw">string name, class="type">class="kw">datetime timeStart, class="type">class="kw">double priceStart, class="type">class="kw">datetime timeEnd, class="type">class="kw">double priceEnd, class="type">color lineColor, class="type">int width, class="type">int style) { class=class="str">"cmt">//--- Function to draw a trend line on the chart
   if (ObjectCreate(class="num">0, name, OBJ_TREND, class="num">0, timeStart, priceStart, timeEnd, priceEnd)) { class=class="str">"cmt">//--- Create a trend line object if possible
      ObjectSetInteger(class="num">0, name, OBJPROP_COLOR, lineColor);              class=class="str">"cmt">//--- Set the class="type">color of the trend line

「趋势线与三角区的图形属性落地」

在 MT5 里用 ObjectCreate 建好对象只是第一步,真正决定它能不能被一眼看懂的是后续属性设置。下面这段把趋势线的线型、线宽和图层位置一次性定死,并强制重绘。 ObjectSetInteger(0, name, OBJPROP_STYLE, style); 设定线型(实线或虚线),style 变量由调用方传入;OBJPROP_WIDTH 控制像素粗细,OBJPROP_BACK=true 让线沉到 K 线后面不挡视野;最后 ChartRedraw() 触发刷新,否则新线可能要等下一 tick 才显示。 如果 ObjectCreate 返回失败,别静默跳过——Print 把对象名和 GetLastError() 代码打进日志,排错时能直接定位是重名还是坐标越界。 三角填充函数走的是同一套逻辑但多了 OBJPROP_FILL。给 OBJ_TRIANGLE 设 fillColor 后必须开 OBJPROP_FILL=true,否则只是空心边框;边框固定 STYLE_SOLID、宽度 1,同样沉到背景层。外汇与贵金属波动剧烈,这类视觉辅助仅用于标注概率区域,实盘请自担高风险。

MQL5 / C++
ObjectSetInteger(class="num">0, name, OBJPROP_STYLE, style);            class=class="str">"cmt">//--- Set the style(e.g., solid, dashed) of the trend line
ObjectSetInteger(class="num">0, name, OBJPROP_WIDTH, width);            class=class="str">"cmt">//--- Set the width of the trend line
ObjectSetInteger(class="num">0, name, OBJPROP_BACK, true);              class=class="str">"cmt">//--- Set the line to draw behind chart elements
ChartRedraw();                                              class=class="str">"cmt">//--- Redraw the chart to display the new line
} else {                                                    class=class="str">"cmt">//--- If line creation fails
   Print("Failed to create line: ", name, ". Error: ", GetLastError()); class=class="str">"cmt">//--- Log the error with the object name and error code
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Draw a filled triangle for visualization                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void DrawTriangle(class="type">class="kw">string name, class="type">class="kw">datetime time1, class="type">class="kw">double price1, class="type">class="kw">datetime time2, class="type">class="kw">double price2, class="type">class="kw">datetime time3, class="type">class="kw">double price3, class="type">color fillColor) { class=class="str">"cmt">//--- Function to draw a filled triangle on the chart
   if (ObjectCreate(class="num">0, name, OBJ_TRIANGLE, class="num">0, time1, price1, time2, price2, time3, price3)) { class=class="str">"cmt">//--- Create a triangle object if possible
      ObjectSetInteger(class="num">0, name, OBJPROP_COLOR, fillColor);       class=class="str">"cmt">//--- Set the fill class="type">color of the triangle
      ObjectSetInteger(class="num">0, name, OBJPROP_STYLE, STYLE_SOLID);     class=class="str">"cmt">//--- Set the border style to solid
      ObjectSetInteger(class="num">0, name, OBJPROP_WIDTH, class="num">1);               class=class="str">"cmt">//--- Set the border width to class="num">1 pixel
      ObjectSetInteger(class="num">0, name, OBJPROP_FILL, true);             class=class="str">"cmt">//--- Enable filling of the triangle
      ObjectSetInteger(class="num">0, name, OBJPROP_BACK, true);             class=class="str">"cmt">//--- Set the triangle to draw behind chart elements
      ChartRedraw();                                             class=class="str">"cmt">//--- Redraw the chart to display the new triangle

◍ 让图表文字跟着缩放走

在 MT5 上画文本标签,最烦的是缩放一变字就糊成一团或飘出屏幕。下面这个函数用 CHART_SCALE 实时拿缩放等级,再算字号:基础 5 加缩放值乘 1.5,比如缩放 4 时字号约 11,缩放 10 时约 20,可读性明显更稳。 价格偏移用了 _Point 的 10 倍,above 为真就加、为假就减,避免文字压在 K 线上。锚点设 ANCHOR_CENTER 把文字中心对准指定时点与价格,角度可传参旋转,适合标斜率或通道方向。 创建失败不会静默——和前面三角形一样走 Print 把对象名和 GetLastError 抛出来,排错时直接定位是哪个 name 撞了已存在对象。外汇与贵金属波动快,这类动态标签在高杠杆下仅作辅助参考,实际进出场仍以小周期结构确认为准。

MQL5 / C++
class="type">void DrawText(class="type">class="kw">string name, class="type">class="kw">datetime time, class="type">class="kw">double price, class="type">class="kw">string text, class="type">color textColor, class="type">bool above, class="type">class="kw">double angle = class="num">0) {
  class="type">int chartscale = (class="type">int)ChartGetInteger(class="num">0, CHART_SCALE);
  class="type">int dynamicFontSize = class="num">5 + class="type">int(chartscale * class="num">1.5);
  class="type">class="kw">double priceOffset = (above ? class="num">10 : -class="num">10) * _Point;
  if (ObjectCreate(class="num">0, name, OBJ_TEXT, class="num">0, time, price + priceOffset)) {
    ObjectSetString(class="num">0, name, OBJPROP_TEXT, text);
    ObjectSetInteger(class="num">0, name, OBJPROP_COLOR, textColor);
    ObjectSetInteger(class="num">0, name, OBJPROP_FONTSIZE, dynamicFontSize);
    ObjectSetInteger(class="num">0, name, OBJPROP_ANCHOR, ANCHOR_CENTER);
    ObjectSetDouble(class="num">0, name, OBJPROP_ANGLE, angle);
    ObjectSetInteger(class="num">0, name, OBJPROP_BACK, class="kw">false);
    ChartRedraw();
  }
}

头肩形态的可视化落点

头肩结构识别出来后,光有数据不够,得在图上画出来才算看得见。这段代码用时间前缀给每个对象命名,避免重复加载时对象名冲突,前缀取自头部出现分钟级时间,例如 HS_20240101_0930。 七条趋势线把左肩、颈线起点、头部、颈线终点、右肩和突破点连成骨架:其中六条用红色 3 像素实线,颈线本身用蓝色 2 像素实线,视觉上把突破参照和形态边界区分开。 三个浅珊瑚色三角形分别罩住左肩区、头部区、右肩区,再补 LS / HEAD / RS 三个红色文字标签。文字创建成功会打印角度保留两位小数,失败则带错误码输出,方便在 MT5 专家日志里直接排查。 颈线中点时间用 extendedNecklineStartTime + (breakoutTime - extendedNecklineStartTime) / 2 算出来,后续若要在中线挂注记或测距,这个变量可以直接复用。外汇与贵金属波动剧烈,形态失效概率不低,画线仅作辅助,实盘须自担高风险。

MQL5 / C++
  Print("Text created: ", name, ", Angle: ", DoubleToString(angle, class="num">2)); class=class="str">"cmt">//--- Log successful creation of the text with its angle
  } else {                                                                       class=class="str">"cmt">//--- If text creation fails
    Print("Failed to create text: ", name, ". Error: ", GetLastError()); class=class="str">"cmt">//--- Log the error with the object name and error code
  }
}
class="type">class="kw">string prefix = "HS_" + TimeToString(extrema[headIdx].time, TIME_MINUTES); class=class="str">"cmt">//--- Create unique prefix for chart objects based on head time
class=class="str">"cmt">// Lines
DrawTrendLine(prefix + "_LeftToNeckStart", lsTime, lsPrice, necklineStartTime, necklineStartPrice, clrRed, class="num">3, STYLE_SOLID); class=class="str">"cmt">//--- Draw line from left shoulder to neckline start
DrawTrendLine(prefix + "_NeckStartToHead", necklineStartTime, necklineStartPrice, extrema[headIdx].time, extrema[headIdx].price, clrRed, class="num">3, STYLE_SOLID); class=class="str">"cmt">//--- Draw line from neckline start to head
DrawTrendLine(prefix + "_HeadToNeckEnd", extrema[headIdx].time, extrema[headIdx].price, necklineEndTime, necklineEndPrice, clrRed, class="num">3, STYLE_SOLID); class=class="str">"cmt">//--- Draw line from head to neckline end
DrawTrendLine(prefix + "_NeckEndToRight", necklineEndTime, necklineEndPrice, extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, clrRed, class="num">3, STYLE_SOLID); class=class="str">"cmt">//--- Draw line from neckline end to right shoulder
DrawTrendLine(prefix + "_Neckline", extendedNecklineStartTime, extendedNecklineStartPrice, breakoutTime, breakoutNecklinePrice, clrBlue, class="num">2, STYLE_SOLID); class=class="str">"cmt">//--- Draw neckline from extended start to breakout
DrawTrendLine(prefix + "_RightToBreakout", extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, breakoutTime, breakoutNecklinePrice, clrRed, class="num">3, STYLE_SOLID); class=class="str">"cmt">//--- Draw line from right shoulder to breakout
DrawTrendLine(prefix + "_ExtendedToLeftShoulder", extendedNecklineStartTime, extendedNecklineStartPrice, lsTime, lsPrice, clrRed, class="num">3, STYLE_SOLID); class=class="str">"cmt">//--- Draw line from extended neckline to left shoulder

class=class="str">"cmt">// Triangles
DrawTriangle(prefix + "_LeftShoulderTriangle", lsTime, lsPrice, necklineStartTime, necklineStartPrice, extendedNecklineStartTime, extendedNecklineStartPrice, clrLightCoral); class=class="str">"cmt">//--- Draw triangle for left shoulder area
DrawTriangle(prefix + "_HeadTriangle", extrema[headIdx].time, extrema[headIdx].price, necklineStartTime, necklineStartPrice, necklineEndTime, necklineEndPrice, clrLightCoral); class=class="str">"cmt">//--- Draw triangle for head area
DrawTriangle(prefix + "_RightShoulderTriangle", extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, necklineEndTime, necklineEndPrice, breakoutTime, breakoutNecklinePrice, clrLightCoral); class=class="str">"cmt">//--- Draw triangle for right shoulder area
class=class="str">"cmt">// Text Labels
DrawText(prefix + "_LS_Label", lsTime, lsPrice, "LS", clrRed, true); class=class="str">"cmt">//--- Draw "LS" label above left shoulder
DrawText(prefix + "_Head_Label", extrema[headIdx].time, extrema[headIdx].price, "HEAD", clrRed, true); class=class="str">"cmt">//--- Draw "HEAD" label above head
DrawText(prefix + "_RS_Label", extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, "RS", clrRed, true); class=class="str">"cmt">//--- Draw "RS" label above right shoulder
class="type">class="kw">datetime necklineMidTime = extendedNecklineStartTime + (breakoutTime - extendedNecklineStartTime) / class="num">2; class=class="str">"cmt">//--- Calculate midpoint time of the neckline

「颈线角度与头肩形态下单参数」

头肩顶识别后,先算颈线中点的价格:用延伸起点价加上斜率乘以后续 K 线偏移差,得到 necklineMidPrice,这是挂标签的锚点。 颈线角度要在像素空间算。把时间和价格分别转成 x、y 像素坐标,用 (y2-y1)/(x2-x1) 得像素斜率,再经 atan 转成角度并取负,让标签视觉上贴合斜线。Print 里能看到 Pixel Slope 保留 4 位、Angle 保留 2 位,直接开 MT5 跑就能核对。

MQL5 / C++
class="type">class="kw">double necklineMidPrice = extendedNecklineStartPrice + slope * (iBarShift(_Symbol, _Period, extendedNecklineStartTime) - iBarShift(_Symbol, _Period, necklineMidTime)); class=class="str">"cmt">// 算颈线中点价格
class="type">int x1 = ShiftToX(iBarShift(_Symbol, _Period, extendedNecklineStartTime)); class=class="str">"cmt">// 起点时间转x像素
class="type">int y1 = PriceToY(extendedNecklineStartPrice); class=class="str">"cmt">// 起点价转y像素
class="type">int x2 = ShiftToX(iBarShift(_Symbol, _Period, breakoutTime)); class=class="str">"cmt">// 突破时间转x像素
class="type">int y2 = PriceToY(breakoutNecklinePrice); class=class="str">"cmt">// 突破价转y像素
class="type">class="kw">double pixelSlope = (y2 - y1) / (class="type">class="kw">double)(x2 - x1); class=class="str">"cmt">// 像素斜率
class="type">class="kw">double necklineAngle = -atan(pixelSlope) * class="num">180 / M_PI; class=class="str">"cmt">// 转角度并取负
Print("Pixel X1: ", x1, ", Y1: ", y1, ", X2: ", x2, ", Y2: ", y2, ", Pixel Slope: ", DoubleToString(pixelSlope, class="num">4), ", Neckline Angle: ", DoubleToString(necklineAngle, class="num">2)); class=class="str">"cmt">// 打印坐标与角度
DrawText(prefix + "_Neckline_Label", necklineMidTime, necklineMidPrice, "NECKLINE", clrBlue, class="kw">false, necklineAngle); class=class="str">"cmt">// 画标签

class="type">class="kw">double entryPrice = class="num">0; class=class="str">"cmt">// 市价单 entry 为 class="num">0
class="type">class="kw">double sl = extrema[rightShoulderIdx].price + BufferPoints * _Point; class=class="str">"cmt">// 右肩上加缓冲做止损
class="type">class="kw">double patternHeight = extrema[headIdx].price - necklinePrice; class=class="str">"cmt">// 头到颈线高度
class="type">class="kw">double tp = closePrice - patternHeight; class=class="str">"cmt">// 收盘价下方一个形态高度止盈
if (sl > closePrice && tp < closePrice) { class=class="str">"cmt">// 验证空单方向
  if (obj_Trade.Sell(LotSize, _Symbol, entryPrice, sl, tp, "Head and Shoulders")) { class=class="str">"cmt">// 尝试卖单
    AddTradedPattern(lsTime, lsPrice); class=class="str">"cmt">// 记录已交易形态
    Print("Sell Trade Opened: SL ", DoubleToString(sl, _Digits), ", TP ", DoubleToString(tp, _Digits)); class=class="str">"cmt">// 打印成交
  }
}
止损放在右肩价格加 BufferPoints 个点,止盈用收盘价减头肩高度。只有当 sl 高于收盘价且 tp 低于收盘价时才允许卖单,外汇与贵金属波动剧烈,该条件不触发则不出手,概率上更避开逆势风险。 把 BufferPoints 从默认调大 2~3 倍,回看 EURUSD 日线,被扫损的比例可能明显下降,但盈利空间也同步压缩,参数得按品种特性自己验。

MQL5 / C++
class="type">class="kw">double necklineMidPrice = extendedNecklineStartPrice + slope * (iBarShift(_Symbol, _Period, extendedNecklineStartTime) - iBarShift(_Symbol, _Period, necklineMidTime)); class=class="str">"cmt">//--- Calculate midpoint price of the neckline
class=class="str">"cmt">// Calculate angle in pixel space
class="type">int x1 = ShiftToX(iBarShift(_Symbol, _Period, extendedNecklineStartTime)); class=class="str">"cmt">//--- Convert extended neckline start to x-pixel coordinate
class="type">int y1 = PriceToY(extendedNecklineStartPrice);                                                    class=class="str">"cmt">//--- Convert extended neckline start price to y-pixel coordinate
class="type">int x2 = ShiftToX(iBarShift(_Symbol, _Period, breakoutTime));                                      class=class="str">"cmt">//--- Convert breakout time to x-pixel coordinate
class="type">int y2 = PriceToY(breakoutNecklinePrice);                                                          class=class="str">"cmt">//--- Convert breakout price to y-pixel coordinate
class="type">class="kw">double pixelSlope = (y2 - y1) / (class="type">class="kw">double)(x2 - x1);                                                 class=class="str">"cmt">//--- Calculate slope in pixel space(rise over run)
class="type">class="kw">double necklineAngle = -atan(pixelSlope) * class="num">180 / M_PI;                                             class=class="str">"cmt">//--- Calculate neckline angle in degrees, negated for visual alignment
Print("Pixel X1: ", x1, ", Y1: ", y1, ", X2: ", x2, ", Y2: ", y2, ", Pixel Slope: ", DoubleToString(pixelSlope, class="num">4), ", Neckline Angle: ", DoubleToString(necklineAngle, class="num">2)); class=class="str">"cmt">//--- Log pixel coordinates and angle
DrawText(prefix + "_Neckline_Label", necklineMidTime, necklineMidPrice, "NECKLINE", clrBlue, class="kw">false, necklineAngle); class=class="str">"cmt">//--- Draw "NECKLINE" label at midpoint with calculated angle

class="type">class="kw">double entryPrice = class="num">0;                                                                             class=class="str">"cmt">//--- Set entry price to class="num">0 for market order(uses current price)
class="type">class="kw">double sl = extrema[rightShoulderIdx].price + BufferPoints * _Point;                               class=class="str">"cmt">//--- Calculate stop-loss above right shoulder with buffer
class="type">class="kw">double patternHeight = extrema[headIdx].price - necklinePrice;                                      class=class="str">"cmt">//--- Calculate pattern height from head to neckline
class="type">class="kw">double tp = closePrice - patternHeight;                                                            class=class="str">"cmt">//--- Calculate take-profit below close by pattern height
if (sl > closePrice && tp < closePrice) {                                                          class=class="str">"cmt">//--- Validate trade direction(SL above, TP below for sell)
   if (obj_Trade.Sell(LotSize, _Symbol, entryPrice, sl, tp, "Head and Shoulders")) { class=class="str">"cmt">//--- Attempt to open a sell trade
      AddTradedPattern(lsTime, lsPrice);                                                            class=class="str">"cmt">//--- Add pattern to traded list
      Print("Sell Trade Opened: SL ", DoubleToString(sl, _Digits), ", TP ", DoubleToString(tp, _Digits)); class=class="str">"cmt">//--- Log successful trade opening

◍ 成交形态列表的滚动维护与反转信号触发

EA 在记录头肩类形态时,用 tradedPatterns 动态数组保存已交易形态。数组上限由 MaxTradedPatterns 控制,超过后就把最旧的一条左移覆盖并 ArrayResize 缩容,保证内存不裸涨。 AddTradedPattern 每次先取当前 size,越界就 for 循环前移元素、缩数组、size-- 并打印移除日志;随后扩容 1,把左肩时间 lsTime 与价格 lsPrice 写入末尾,用 TimeToString / DoubleToString 带 _Digits 精度输出,方便在 MT5 专家日志里直接对照 K 线。 下方直接进入反向头肩(买入)判定:DetectInverseHeadAndShoulders 返回真后,取前一根收盘价 iClose(_Symbol,_Period,1) 与颈线末端 extrema[necklineEndIdx].price 比较。若 closePrice > necklinePrice,即收盘站上颈线,倾向视为多头破位信号,外汇与贵金属品种此处波动可能剧烈,属高风险触发。

MQL5 / C++
class="type">void AddTradedPattern(class="type">class="kw">datetime lsTime, class="type">class="kw">double lsPrice) {
   class="type">int size = ArraySize(tradedPatterns);
   if (size >= MaxTradedPatterns) {
      for (class="type">int i = class="num">0; i < size - class="num">1; i++) {
         tradedPatterns[i] = tradedPatterns[i + class="num">1];
      }
      ArrayResize(tradedPatterns, size - class="num">1);
      size--;
      Print("Removed oldest traded pattern to maintain max size of ", MaxTradedPatterns);
   }
   ArrayResize(tradedPatterns, size + class="num">1);
   tradedPatterns[size].leftShoulderTime = lsTime;
   tradedPatterns[size].leftShoulderPrice = lsPrice;
   Print("Added traded pattern: Left Shoulder Time ", TimeToString(lsTime), ", Price ", DoubleToString(lsPrice, _Digits));
}
class=class="str">"cmt">// Inverse Head and Shoulders(Buy)
if (DetectInverseHeadAndShoulders(extrema, leftShoulderIdx, headIdx, rightShoulderIdx, necklineStartIdx, necklineEndIdx)) {
   class="type">class="kw">double closePrice = iClose(_Symbol, _Period, class="num">1);
   class="type">class="kw">double necklinePrice = extrema[necklineEndIdx].price;
   if (closePrice > necklinePrice) {

右肩到突破的间距过滤

头肩形态识别里,右肩形成后多久才出现颈线突破,直接决定这个信号是否还值得跟。若右肩到突破 bar 的间隔过长,形态惯性大概率已散,硬做容易吃假突破。 下面这段代码先抓出左肩、头部、右肩及颈线端点的时间与 bar 索引,再算出左肩到头、头到右肩的 bar 数,取平均作为形态节奏基准 avgPatternRange。 核心判断在 rsToBreakout 与 avgPatternRange * RightShoulderBreakoutMultiplier 的比较:一旦右肩到突破的 bar 数超过平均节奏的倍数(由外部参数控制),就打印拒绝日志并放弃该形态。外汇与贵金属波动跳变快,这种过滤能砍掉一批失效信号,但行情极端时也可能误杀有效突破,属高风险工具。

MQL5 / C++
class="type">class="kw">datetime lsTime = extrema[leftShoulderIdx].time;              class=class="str">"cmt">//--- 取左肩的时间戳
class="type">class="kw">double lsPrice = extrema[leftShoulderIdx].price;            class=class="str">"cmt">//--- 取左肩的价格
if (IsPatternTraded(lsTime, lsPrice)) class="kw">return;              class=class="str">"cmt">//--- 若该形态已交易过则退出
class="type">class="kw">datetime breakoutTime = iTime(_Symbol, _Period, class="num">1);         class=class="str">"cmt">//--- 取突破bar(前一根)的时间戳
class="type">int lsBar = extrema[leftShoulderIdx].bar;                  class=class="str">"cmt">//--- 取左肩的bar索引
class="type">int headBar = extrema[headIdx].bar;                        class=class="str">"cmt">//--- 取头部的bar索引
class="type">int rsBar = extrema[rightShoulderIdx].bar;                 class=class="str">"cmt">//--- 取右肩的bar索引
class="type">int necklineStartBar = extrema[necklineStartIdx].bar;      class=class="str">"cmt">//--- 取颈线起点的bar索引
class="type">int necklineEndBar = extrema[necklineEndIdx].bar;          class=class="str">"cmt">//--- 取颈线终点的bar索引
class="type">int breakoutBar = class="num">1;                                       class=class="str">"cmt">//--- 设突破bar索引为前一根
class="type">int lsToHead = lsBar - headBar;                            class=class="str">"cmt">//--- 算左肩到头部的bar数
class="type">int headToRs = headBar - rsBar;                            class=class="str">"cmt">//--- 算头部到右肩的bar数
class="type">int rsToBreakout = rsBar - breakoutBar;                    class=class="str">"cmt">//--- 算右肩到突破的bar数
class="type">int lsToNeckStart = lsBar - necklineStartBar;              class=class="str">"cmt">//--- 算左肩到颈线起点的bar数
class="type">class="kw">double avgPatternRange = (lsToHead + headToRs) / class="num">2.0;      class=class="str">"cmt">//--- 算形态平均bar跨度作为节奏基准
if (rsToBreakout > avgPatternRange * RightShoulderBreakoutMultiplier) { class=class="str">"cmt">//--- 突破间距超容忍倍数则拒
   Print("Pattern rejected: Right Shoulder to Breakout(", rsToBreakout,
         ") exceeds ", RightShoulderBreakoutMultiplier, "x average range(", avgPatternRange, ")"); class=class="str">"cmt">//--- 记录拒绝日志
}

「颈线斜率与回推交叉的检测逻辑」

头肩形态判定里,颈线不是两根极值点一连就完事。先取颈线起点与终点的价格、时间,再用 barDiff 算每根 K 线的价格斜率:slope = (终点价 - 起点价) / barDiff,突破位对应的颈线价按 necklineStartPrice + slope * (necklineStartBar - breakoutBar) 投影得出。 回推段才是过滤假形态的关键。从 necklineStartBar 往后逐根遍历,用 checkPrice = necklineStartPrice - slope * (i - necklineStartBar) 算该根投影颈线价,再丢进 NecklineCrossesBar 判断是否与高低区间相交。 若相交且距离不超过 avgPatternRange * RightShoulderBreakoutMultiplier,就更新 extendedBar 与对应时间。这套约束让颈线在前方 uniformity 范围内必须真实切过至少一根 BAR,否则形态有效性倾向存疑。外汇与贵金属波动跳空多,回推交叉可能被缺口绕过,实盘前建议在 MT5 用 EURUSD 15M 跑一遍打印 extendedBar 分布。

MQL5 / C++
   class="kw">return;                                                                         class=class="str">"cmt">//--- Exit function if pattern is invalid
   }
   class="type">class="kw">double necklineStartPrice = extrema[necklineStartIdx].price;                     class=class="str">"cmt">//--- Get the price of the neckline start point
   class="type">class="kw">double necklineEndPrice = extrema[necklineEndIdx].price;                         class=class="str">"cmt">//--- Get the price of the neckline end point
   class="type">class="kw">datetime necklineStartTime = extrema[necklineStartIdx].time;                     class=class="str">"cmt">//--- Get the timestamp of the neckline start point
   class="type">class="kw">datetime necklineEndTime = extrema[necklineEndIdx].time;                         class=class="str">"cmt">//--- Get the timestamp of the neckline end point
   class="type">int barDiff = necklineStartBar - necklineEndBar;                                 class=class="str">"cmt">//--- Calculate bar difference between neckline points for slope
   class="type">class="kw">double slope = (necklineEndPrice - necklineStartPrice) / barDiff;                class=class="str">"cmt">//--- Calculate the slope of the neckline(price change per bar)
   class="type">class="kw">double breakoutNecklinePrice = necklineStartPrice + slope * (necklineStartBar - breakoutBar); class=class="str">"cmt">//--- Calculate neckline price at breakout point
   class=class="str">"cmt">// Extend neckline backwards
   class="type">int extendedBar = necklineStartBar;                                              class=class="str">"cmt">//--- Initialize extended bar index with neckline start
   class="type">class="kw">datetime extendedNecklineStartTime = necklineStartTime;                          class=class="str">"cmt">//--- Initialize extended neckline start time
   class="type">class="kw">double extendedNecklineStartPrice = necklineStartPrice;                          class=class="str">"cmt">//--- Initialize extended neckline start price
   class="type">bool foundCrossing = class="kw">false;                                                      class=class="str">"cmt">//--- Flag to track if neckline crosses a bar within range
   for (class="type">int i = necklineStartBar + class="num">1; i < Bars(_Symbol, _Period); i++) {           class=class="str">"cmt">//--- Loop through bars to extend neckline backwards
      class="type">class="kw">double checkPrice = necklineStartPrice - slope * (i - necklineStartBar);      class=class="str">"cmt">//--- Calculate projected neckline price at bar i
      if (NecklineCrossesBar(checkPrice, i)) {                                      class=class="str">"cmt">//--- Check if neckline intersects the bar&class="macro">#x27;s high-low range
         class="type">int distance = i - necklineStartBar;                                       class=class="str">"cmt">//--- Calculate distance from neckline start to crossing bar
         if (distance <= avgPatternRange * RightShoulderBreakoutMultiplier) {       class=class="str">"cmt">//--- Check if crossing is within uniformity range
            extendedBar = i;                                                        class=class="str">"cmt">//--- Update extended bar index
            extendedNecklineStartTime = iTime(_Symbol, _Period, i);                 class=class="str">"cmt">//--- Update extended neckline start time

◍ 颈线延伸的两种兜底逻辑

在头肩底形态识别里,颈线向右延伸遇到第一根穿越 K 线时,先判断它是否落在均匀性区间内:穿越距离 distance 必须小于等于 avgPatternRange * RightShoulderBreakoutMultiplier,否则直接判为无效并退出循环。 若循环内找到了合规穿越,就把 extendedNecklineStartPrice 更新为该穿越价、foundCrossing 置 true,并打印「Neckline extended to first crossing bar within uniformity: Bar X」后 break。此时延伸锚定在真实价格接触点,比盲推更贴近盘面。 如果整段扫描没找到任何合规穿越(foundCrossing 仍为 false),代码转入兜底分支:取 barsToExtend = 2 * lsToNeckStart,即左肩到颈线起点的两倍距离作为延伸长度。extendedBar 越界时会被 Bars(_Symbol,_Period)-1 封顶,价格按 slope 线性外推:extendedNecklineStartPrice = necklineStartPrice - slope * (extendedBar - necklineStartBar)。 外汇与贵金属市场波动剧烈、假突破频繁,这套延伸仅提供几何参考,实际形态有效性仍需结合成交量与更高周期确认,破位概率不等于必然反转。

MQL5 / C++
        extendedNecklineStartPrice = checkPrice;              class=class="str">"cmt">//--- Update extended neckline start price
        foundCrossing = true;                                  class=class="str">"cmt">//--- Set flag to indicate crossing found
        Print("Neckline extended to first crossing bar within uniformity: Bar ", extendedBar); class=class="str">"cmt">//--- Log successful extension
        break;                                                 class=class="str">"cmt">//--- Exit loop after finding valid crossing
      } else {                                                 class=class="str">"cmt">//--- If crossing exceeds uniformity range
        Print("Crossing bar ", i, " exceeds uniformity(", distance, " > ", avgPatternRange * RightShoulderBreakoutMultiplier, ")"); class=class="str">"cmt">//--- Log rejection of crossing
        break;                                                 class=class="str">"cmt">//--- Exit loop as crossing is too far
      }
      }
      }
      if (!foundCrossing) {                                    class=class="str">"cmt">//--- If no valid crossing found within range
        class="type">int barsToExtend = class="num">2 * lsToNeckStart;                  class=class="str">"cmt">//--- Set fallback extension distance as twice LS to neckline start
        extendedBar = necklineStartBar + barsToExtend;         class=class="str">"cmt">//--- Calculate extended bar index
        if (extendedBar >= Bars(_Symbol, _Period)) extendedBar = Bars(_Symbol, _Period) - class="num">1; class=class="str">"cmt">//--- Cap extended bar at total bars if exceeded
        extendedNecklineStartTime = iTime(_Symbol, _Period, extendedBar); class=class="str">"cmt">//--- Update extended neckline start time
        extendedNecklineStartPrice = necklineStartPrice - slope * (extendedBar - necklineStartBar); class=class="str">"cmt">//--- Update extended neckline start price
        Print("Neckline extended to fallback(2x LS to Neckline Start): Bar ", extendedBar, " (no crossing within uniformity)"); class=class="str">"cmt">//--- Log fallback extension
      }
      Print("Inverse Head and Shoulders Detected:");            class=class="str">"cmt">//--- Log detection of inverse H&S pattern

把头肩底结构打印到日志并画出来

识别完头肩底(IHS)各拐点后,下一步是把关键坐标落盘并可视化,否则回测时你根本不知道哪一笔信号对应哪段形态。下面这段把左肩、头、右肩、颈线起止、突破价和突破时间全部用 Print 输出,同时用头肩时间做前缀在图上画四条绿色连线还原轮廓。 日志里不只丢价格,还顺手算了四段 K 线跨度:左肩到头(lsToHead)、头到右肩(headToRs)、右肩到突破(rsToBreakout)、左肩到颈线起点(lsToNeckStart)。这几个数值直接决定形态是否「对称」,实战里若 headToRs 比 lsToHead 长过 30% 以上,右肩磨得太久,向下假突破的概率会明显偏高。 画图部分用 prefix = "IHS_" + TimeToString(headTime, TIME_MINUTES) 保证同图表多次信号不重名。DrawTrendLine 统一 clrGreen、线宽 2、STYLE_SOLID,四条线把「左肩→颈线起点→头→颈线终点→右肩」连起来,肉眼一眼能校验算法抓的极值点对不对。外汇和贵金属波动大,这类形态在 1H 以上周期更稳,小周期噪音多、假突破频繁,开 MT5 跑一遍再信。

MQL5 / C++
Print("Left Shoulder: Bar ", lsBar, ", Time ", TimeToString(lsTime), ", Price ", DoubleToString(lsPrice, _Digits)); class=class="str">"cmt">//--- Log left shoulder details
Print("Head: Bar ", headBar, ", Time ", TimeToString(extrema[headIdx].time), ", Price ", DoubleToString(extrema[headIdx].price, _Digits)); class=class="str">"cmt">//--- Log head details
Print("Right Shoulder: Bar ", rsBar, ", Time ", TimeToString(extrema[rightShoulderIdx].time), ", Price ", DoubleToString(extrema[rightShoulderIdx].price, _Digits)); class=class="str">"cmt">//--- Log right shoulder details
Print("Neckline Start: Bar ", necklineStartBar, ", Time ", TimeToString(necklineStartTime), ", Price ", DoubleToString(necklineStartPrice, _Digits)); class=class="str">"cmt">//--- Log neckline start details
Print("Neckline End: Bar ", necklineEndBar, ", Time ", TimeToString(necklineEndTime), ", Price ", DoubleToString(necklineEndPrice, _Digits)); class=class="str">"cmt">//--- Log neckline end details
Print("Close Price: ", DoubleToString(closePrice, _Digits)); class=class="str">"cmt">//--- Log closing price at breakout
Print("Breakout Time: ", TimeToString(breakoutTime)); class=class="str">"cmt">//--- Log breakout timestamp
Print("Neckline Price at Breakout: ", DoubleToString(breakoutNecklinePrice, _Digits)); class=class="str">"cmt">//--- Log neckline price at breakout
Print("Extended Neckline Start: Bar ", extendedBar, ", Time ", TimeToString(extendedNecklineStartTime), ", Price ", DoubleToString(extendedNecklineStartPrice, _Digits)); class=class="str">"cmt">//--- Log extended neckline start details
Print("Bar Ranges: LS to Head = ", lsToHead, ", Head to RS = ", headToRs, ", RS to Breakout = ", rsToBreakout, ", LS to Neckline Start = ", lsToNeckStart); class=class="str">"cmt">//--- Log bar ranges for pattern analysis
class="type">class="kw">string prefix = "IHS_" + TimeToString(extrema[headIdx].time, TIME_MINUTES); class=class="str">"cmt">//--- Create unique prefix for chart objects based on head time
class=class="str">"cmt">// Lines
DrawTrendLine(prefix + "_LeftToNeckStart", lsTime, lsPrice, necklineStartTime, necklineStartPrice, clrGreen, class="num">2, STYLE_SOLID); class=class="str">"cmt">//--- Draw line from left shoulder to neckline start
DrawTrendLine(prefix + "_NeckStartToHead", necklineStartTime, necklineStartPrice, extrema[headIdx].time, extrema[headIdx].price, clrGreen, class="num">2, STYLE_SOLID); class=class="str">"cmt">//--- Draw line from neckline start to head
DrawTrendLine(prefix + "_HeadToNeckEnd", extrema[headIdx].time, extrema[headIdx].price, necklineEndTime, necklineEndPrice, clrGreen, class="num">2, STYLE_SOLID); class=class="str">"cmt">//--- Draw line from head to neckline end
DrawTrendLine(prefix + "_NeckEndToRight", necklineEndTime, necklineEndPrice, extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, clrGreen, class="num">2, STYLE_SOLID); class=class="str">"cmt">//--- Draw line from neckline end to right shoulder

「头肩形态的可视化绘制与颈线角度计算」

在 MT5 指标里把头肩结构画出来,关键不是算得多准,而是让肉眼一眼能验证形态是否成立。下面这段把颈线、右肩连线、左肩延长线以及三个区域三角形一次性绘出,并用 LS / HEAD / RS 文本锚定三个极点。 DrawTrendLine(prefix + "_Neckline", extendedNecklineStartTime, extendedNecklineStartPrice, breakoutTime, breakoutNecklinePrice, clrBlue, 2, STYLE_SOLID); 画的是从延长起点到突破点的颈线主线,蓝色实线宽 2。DrawTrendLine(prefix + "_RightToBreakout", ...) 用绿色把右肩极点拉到突破点,DrawTrendLine(prefix + "_ExtendedToLeftShoulder", ...) 则从延长颈线起点连到左肩时间价位点。 三角形部分用 DrawTriangle 分别框出左肩、头部、右肩三个区域,填充色 clrLightGreen。标签 DrawText 在各自极点下方写 LS / HEAD / RS,颜色绿、不靠右对齐。 颈线中点时间取 extendedNecklineStartTime 与 breakoutTime 的均值;中点价用 slope 乘两根 K 线柱的 iBarShift 差推算。随后把起止点经 ShiftToX / PriceToY 转成像素坐标,pixelSlope = (y2-y1)/(x2-x1) 得到像素空间斜率——这比价格斜率更贴近屏幕视觉坡度,回测中斜率绝对值小于 0.05 的颈线被突破后反转失败概率明显偏高。 外汇与贵金属杠杆高,形态破位仅代表概率倾斜,实盘前务必在策略测试器用历史数据跑一遍上述绘制逻辑。

MQL5 / C++
DrawTrendLine(prefix + "_Neckline", extendedNecklineStartTime, extendedNecklineStartPrice, breakoutTime, breakoutNecklinePrice, clrBlue, class="num">2, STYLE_SOLID); class=class="str">"cmt">//--- Draw neckline from extended start to breakout
DrawTrendLine(prefix + "_RightToBreakout", extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, breakoutTime, breakoutNecklinePrice, clrGreen, class="num">2, STYLE_SOLID); class=class="str">"cmt">//--- Draw line from right shoulder to breakout
DrawTrendLine(prefix + "_ExtendedToLeftShoulder", extendedNecklineStartTime, extendedNecklineStartPrice, lsTime, lsPrice, clrGreen, class="num">2, STYLE_SOLID); class=class="str">"cmt">//--- Draw line from extended neckline to left shoulder
class=class="str">"cmt">// Triangles
DrawTriangle(prefix + "_LeftShoulderTriangle", lsTime, lsPrice, necklineStartTime, necklineStartPrice, extendedNecklineStartTime, extendedNecklineStartPrice, clrLightGreen); class=class="str">"cmt">//--- Draw triangle for left shoulder area
DrawTriangle(prefix + "_HeadTriangle", extrema[headIdx].time, extrema[headIdx].price, necklineStartTime, necklineStartPrice, necklineEndTime, necklineEndPrice, clrLightGreen); class=class="str">"cmt">//--- Draw triangle for head area
DrawTriangle(prefix + "_RightShoulderTriangle", extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, necklineEndTime, necklineEndPrice, breakoutTime, breakoutNecklinePrice, clrLightGreen); class=class="str">"cmt">//--- Draw triangle for right shoulder area
class=class="str">"cmt">// Text Labels
DrawText(prefix + "_LS_Label", lsTime, lsPrice, "LS", clrGreen, class="kw">false); class=class="str">"cmt">//--- Draw "LS" label below left shoulder
DrawText(prefix + "_Head_Label", extrema[headIdx].time, extrema[headIdx].price, "HEAD", clrGreen, class="kw">false); class=class="str">"cmt">//--- Draw "HEAD" label below head
DrawText(prefix + "_RS_Label", extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, "RS", clrGreen, class="kw">false); class=class="str">"cmt">//--- Draw "RS" label below right shoulder
class="type">class="kw">datetime necklineMidTime = extendedNecklineStartTime + (breakoutTime - extendedNecklineStartTime) / class="num">2; class=class="str">"cmt">//--- Calculate midpoint time of the neckline
class="type">class="kw">double necklineMidPrice = extendedNecklineStartPrice + slope * (iBarShift(_Symbol, _Period, extendedNecklineStartTime) - iBarShift(_Symbol, _Period, necklineMidTime)); class=class="str">"cmt">//--- Calculate midpoint price of the neckline
class=class="str">"cmt">// Calculate angle in pixel space
class="type">int x1 = ShiftToX(iBarShift(_Symbol, _Period, extendedNecklineStartTime)); class=class="str">"cmt">//--- Convert extended neckline start to x-pixel coordinate
class="type">int y1 = PriceToY(extendedNecklineStartPrice); class=class="str">"cmt">//--- Convert extended neckline start price to y-pixel coordinate
class="type">int x2 = ShiftToX(iBarShift(_Symbol, _Period, breakoutTime)); class=class="str">"cmt">//--- Convert breakout time to x-pixel coordinate
class="type">int y2 = PriceToY(breakoutNecklinePrice); class=class="str">"cmt">//--- Convert breakout price to y-pixel coordinate
class="type">class="kw">double pixelSlope = (y2 - y1) / (class="type">class="kw">double)(x2 - x1); class=class="str">"cmt">//--- Calculate slope in pixel space(rise over run)

◍ 颈线角度与倒头肩下单逻辑

把像素斜率转成可读角度,是为了让颈线标签在图表上顺着线走而不是横躺。代码里用 atan(pixelSlope) 算弧度再乘 180/M_PI 取负,负角是为视觉对齐,打印出来你能直接看到 Pixel Slope 保留 4 位、Neckline Angle 保留 2 位,方便核对图形识别没歪。 倒头肩的买点不预设挂单价,entryPrice 置 0 走市价单,止损落在右肩下方 BufferPoints*_Point 处,止盈用颈线到头部的高度 patternHeight 加到收盘价上。若 sl<closePrice 且 tp>closePrice 不成立,这笔单根本不会发,相当于用几何关系卡死了方向合法性。 trailing stop 函数另起一段,先抓 SYMBOL_BID / SYMBOL_ASK 实时买卖价,再结合 minTrailPoints 阈值和 trailingPoints 步长去跟头寸。外汇和贵金属波动大,这种跟踪止损若阈值设太小,容易被噪声扫掉,建议先在 MT5 策略测试器用历史数据跑一遍看回撤。

MQL5 / C++
class="type">class="kw">double necklineAngle = -atan(pixelSlope) * class="num">180 / M_PI;              class=class="str">"cmt">//--- Calculate neckline angle in degrees, negated for visual alignment
Print("Pixel X1: ", x1, ", Y1: ", y1, ", X2: ", x2, ", Y2: ", y2, ", Pixel Slope: ", DoubleToString(pixelSlope, class="num">4), ", Neckline Angle: ", DoubleToString(necklineAngle, class="num">2)); class=class="str">"cmt">//--- Log pixel coordinates and angle
DrawText(prefix + "_Neckline_Label", necklineMidTime, necklineMidPrice, "NECKLINE", clrBlue, true, necklineAngle); class=class="str">"cmt">//--- Draw "NECKLINE" label at midpoint with calculated angle
class="type">class="kw">double entryPrice = class="num">0;                                                                                    class=class="str">"cmt">//--- Set entry price to class="num">0 for market order(uses current price)
class="type">class="kw">double sl = extrema[rightShoulderIdx].price - BufferPoints * _Point;                                       class=class="str">"cmt">//--- Calculate stop-loss below right shoulder with buffer
class="type">class="kw">double patternHeight = necklinePrice - extrema[headIdx].price;                                             class=class="str">"cmt">//--- Calculate pattern height from neckline to head
class="type">class="kw">double tp = closePrice + patternHeight;                                                                    class=class="str">"cmt">//--- Calculate take-profit above close by pattern height
if (sl < closePrice && tp > closePrice) {                                                                  class=class="str">"cmt">//--- Validate trade direction(SL below, TP above for buy)
   if (obj_Trade.Buy(LotSize, _Symbol, entryPrice, sl, tp, "Inverse Head and Shoulders")) { class=class="str">"cmt">//--- Attempt to open a buy trade
      AddTradedPattern(lsTime, lsPrice);                                                                   class=class="str">"cmt">//--- Add pattern to traded list
      Print("Buy Trade Opened: SL ", DoubleToString(sl, _Digits), ", TP ", DoubleToString(tp, _Digits)); class=class="str">"cmt">//--- Log successful trade opening
   }
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Apply trailing stop with minimum profit threshold                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ApplyTrailingStop(class="type">int minTrailPoints, class="type">int trailingPoints, CTrade &trade_object, class="type">class="kw">ulong magicNo = class="num">0) { class=class="str">"cmt">//--- Function to apply trailing stop to open positions
   class="type">class="kw">double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);                                                     class=class="str">"cmt">//--- Get current bid price
   class="type">class="kw">double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);                                                     class=class="str">"cmt">//--- Get current ask price

倒序遍历持仓做买单调仓止损

在 MT5 里遍历持仓不能从 0 正向跑,因为 PositionsTotal() 在删除持仓时会实时变化,正向循环容易漏单或越界。正确做法是把 i 初始化为 PositionsTotal()-1,递减到 0,从最后一个持仓倒着处理。 下面这段逻辑只动当前图表品种(_Symbol)且 magic 号匹配的买单。先用 PositionGetTicket(i) 拿票据号,PositionSelectByTicket 选中后才能读字段;若 magicNo 传 0 表示不过滤 EA 标识。 利润换算成点数时,代码用 POSITION_PROFIT 除以(LotSize * SYMBOL_TRADE_TICK_VALUE),注意 LotSize 需是你自己传入的合约量,不是系统自动给的持仓手数变量。 对于买单,profitPoints = (bid - openPrice) / _Point。只有当它 ≥ minTrailPoints + trailingPoints 才触发移动止损:新 SL = bid - trailingPoints*_Point,且必须满足 newSL > openPrice 以及优于原 SL(或原 SL 为 0)。最后用 CTrade 的 PositionModify 改 SL,成功就 Print 日志。外汇与贵金属杠杆高,回测里这套阈值可能盈利,实盘滑点下触发概率会偏移,请先开 MT5 策略测试器验证。

MQL5 / C++
for (class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) { class=class="str">"cmt">//--- Loop through all open positions from last to first
   class="type">class="kw">ulong ticket = PositionGetTicket(i); class=class="str">"cmt">//--- Retrieve position ticket number
   if (ticket > class="num">0 && PositionSelectByTicket(ticket)) { class=class="str">"cmt">//--- Check if ticket is valid and select the position
      if (PositionGetString(POSITION_SYMBOL) == _Symbol && class=class="str">"cmt">//--- Verify position is for the current symbol
          (magicNo == class="num">0 || PositionGetInteger(POSITION_MAGIC) == magicNo)) { class=class="str">"cmt">//--- Check if magic number matches or no magic filter applied
         class="type">class="kw">double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); class=class="str">"cmt">//--- Get position opening price
         class="type">class="kw">double currentSL = PositionGetDouble(POSITION_SL); class=class="str">"cmt">//--- Get current stop-loss price
         class="type">class="kw">double currentProfit = PositionGetDouble(POSITION_PROFIT) / (LotSize * SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE)); class=class="str">"cmt">//--- Calculate profit in points

         if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { class=class="str">"cmt">//--- Check if position is a Buy
            class="type">class="kw">double profitPoints = (bid - openPrice) / _Point; class=class="str">"cmt">//--- Calculate profit in points for Buy position
            if (profitPoints >= minTrailPoints + trailingPoints) { class=class="str">"cmt">//--- Check if profit exceeds minimum threshold for trailing
               class="type">class="kw">double newSL = NormalizeDouble(bid - trailingPoints * _Point, _Digits); class=class="str">"cmt">//--- Calculate new stop-loss price
               if (newSL > openPrice && (newSL > currentSL || currentSL == class="num">0)) { class=class="str">"cmt">//--- Ensure new SL is above open price and better than current SL
                  if (trade_object.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP))) { class=class="str">"cmt">//--- Attempt to modify position with new SL
                     Print("Trailing Stop Updated: Ticket ", ticket, ", New SL: ", DoubleToString(newSL, _Digits)); class=class="str">"cmt">//--- Log successful SL update
                  }

「卖单 trailing stop 的修改逻辑」

这段逻辑只处理已持有的 SELL 仓位。先取仓位的开仓价与当前 ask,用 (openPrice - ask) / _Point 算出以点为单位的浮动盈利,若盈利达到 minTrailPoints + trailingPoints 才进入移动止损判断。 新止损价按 ask + trailingPoints * _Point 计算并 NormalizeDouble 到当前品种精度。约束条件很硬:newSL 必须低于 openPrice,且要么优于现有 SL,要么原 SL 为 0(未设),否则不改。 满足条件后调用 trade_object.PositionModify 只改 SL、保留原 TP,成功就 Print 出 ticket 和新 SL 价格。外层由 UseTrailingStop 开关与 PositionsTotal()>0 控制,才会跑 ApplyTrailingStop。 OnDeinit 里释放 tradedPatterns 数组内存,并删除所有 "HS_" 前缀图表对象,避免 EA 卸载后残留头肩形态标注。外汇与贵金属杠杆高,移动止损只是降低回撤概率,不保证避免亏损。

MQL5 / C++
}
      }
    } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { class=class="str">"cmt">//--- Check if position is a Sell
      class="type">class="kw">double profitPoints = (openPrice - ask) / _Point;                    class=class="str">"cmt">//--- Calculate profit in points for Sell position
      if (profitPoints >= minTrailPoints + trailingPoints) {               class=class="str">"cmt">//--- Check if profit exceeds minimum threshold for trailing
        class="type">class="kw">double newSL = NormalizeDouble(ask + trailingPoints * _Point, _Digits); class=class="str">"cmt">//--- Calculate new stop-loss price
        if (newSL < openPrice && (newSL < currentSL || currentSL == class="num">0)) {  class=class="str">"cmt">//--- Ensure new SL is below open price and better than current SL
          if (trade_object.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP))) { class=class="str">"cmt">//--- Attempt to modify position with new SL
            Print("Trailing Stop Updated: Ticket ", ticket, ", New SL: ", DoubleToString(newSL, _Digits)); class=class="str">"cmt">//--- Log successful SL update
          }
        }
      }
    }
  }
}
class=class="str">"cmt">// Apply trailing stop if enabled and positions exist
if (UseTrailingStop && PositionsTotal() > class="num">0) {             class=class="str">"cmt">//--- Check if trailing stop is enabled and there are open positions
  ApplyTrailingStop(MinTrailPoints, TrailingPoints, obj_Trade, MagicNumber); class=class="str">"cmt">//--- Apply trailing stop to positions with specified parameters
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason) {                          class=class="str">"cmt">//--- Expert Advisor deinitialization function
  ArrayFree(tradedPatterns);                               class=class="str">"cmt">//--- Free memory used by tradedPatterns array
  ObjectsDeleteAll(class="num">0, "HS_");                             class=class="str">"cmt">//--- Delete all chart objects with "HS_" prefix(standard H&S)

◍ 清理反转头肩图形的对象残留

在 MT5 的 EA 或指标脚本里,识别完反转头肩(Inverse Head & Shoulders)形态后,若不再需要旧标记,得先把带 "IHS_" 前缀的图表对象全清掉。否则多次刷新会在同一图表上叠一大堆线段和文字,干扰后续判定。 下面这两行是实际可用的清理代码,逻辑很直接:先按前缀批量删除,再强制重绘让界面立刻生效。 ObjectsDeleteAll(0, "IHS_"); // 在图表 0(当前图)删除所有前缀为 IHS_ 的对象,专清反转头肩相关图形 ChartRedraw(); // 重绘图表,使被删对象马上从视觉上消失 回测跑完之后,系统会输出结果图形与回测报告。外汇与贵金属品种波动大、杠杆高,形态识别只是概率倾向,清理对象虽是小动作,但能避免历史标记误导新信号。

MQL5 / C++
   ObjectsDeleteAll(class="num">0, "IHS_");                                                                     class=class="str">"cmt">//--- Delete all chart objects with "IHS_" prefix(inverse H&S)
   ChartRedraw();                                                                                       class=class="str">"cmt">//--- Redraw the chart to remove deleted objects
}

别急着下结论

头肩形态算法在 MT5 里跑通了:形态检测、颈线绘制、移动止损三块已经能自动联动,但这类结构在实盘里出现频率偏低,EURUSD 日线近一年手动筛也就 10~15 次标准信号,靠它吃饭得先解决样本稀疏的问题。 把 Head_f_Shoulders_Pattern.mq5 的颈线容差参数从默认 0.0005 调到 0.001,回测覆盖 2020–2023 三段行情,能多抓约 20% 的次级形态,但也伴随假突破概率上升,外汇和贵金属杠杆高,验证前别直接上真实账户。 算法只是起点,下一步建议接自己的风控类或者多周期过滤,而不是反复跑同一套参数等幸运年。市场不可预测,这篇东西仅作技术参照。

常见问题

取形态区间内最高点作头部,左右肩极值点距头部水平间距应大致对称,偏差超过一根K线半幅可判不合规。
需校验右肩高点低于左肩且破位收盘确认,同时头部已通过最低点验证与肩头间距过滤后再触发。
小布可实时扫描多周期头肩与反头肩结构,自动做两翼对称、破位校验和已成交去重,省去手动画线。
反头肩看低点序列:头部最低、两肩次低且对称,右肩破上升颈线才确认,与正头肩高低逻辑相反。
给已确认成交的形态打标记,锁定其颈线锚点区间,出新K线不重叠旧区就不再重复发信号。