在 MQL5 中自动化交易策略(第 13 部分):构建头肩形态交易算法·进阶篇
📐

在 MQL5 中自动化交易策略(第 13 部分):构建头肩形态交易算法·进阶篇

(2/3)· 接上篇订单块策略,这一篇把头肩顶底的结构规则拆成代码逻辑,让转折捕捉不再靠肉眼

含代码示例偏理论 第 2/3 篇
不少交易者把头肩形态当成肉眼找三座山就开仓,结果颈线画歪、肩高容忍度为零,假突破吃了一堆止损。用固定阈值和对称容差把形态量化,才能区分真反转和震荡噪音。

「先把图表坐标和形态容器搭起来」

做价格形态识别,第一步不是算指标,而是把 MT5 图表当前的可见范围和缩放状态抓出来。下面这段代码在全局层用 ChartGetInteger / ChartGetDouble 把图表宽高、缩放级别(0–5)、首根可见 bar 索引、可见 bar 数量、可视最低最高价一次性读进变量,后续画线和判定都依赖这些数。 Extremum 结构存极值点:price 记高位或低位价格,isPeak 区分是峰还是谷;TradedPattern 结构只留左肩的时间与价格,用来标记哪些形态已经交易过、避免重复触发。lastBarTime 这个 static 变量是关键,它记住上一根处理过的 bar 时间,新 tick 来时比对一下就能跳过已处理数据,省掉无谓循环。 坐标系转换是两个小函数。BarWidth 用 pow(2, scale) 把缩放级映射成像素宽度——scale=0 时每根 1 像素,scale=5 时 32 像素。ShiftToX 把 bar 序列位移算成画布 x 坐标:chart_first_vis_bar 减去 shift 再乘宽度减 1。开 MT5 把 chart_scale 从 0 拨到 5,你会看到同样 100 根 bar 占的像素从 100 变到 3200,形态标注位置必须跟着这套公式走才不飘。外汇与贵金属波动快、杠杆高,坐标算错可能把信号画到看不见的地方,验证时先打印 ShiftToX(0) 和 chart_prcmax 对照屏幕。

MQL5 / C++
class="type">class="kw">double price;           class=class="str">"cmt">//--- Price at extremum(high for peak, low for trough)
class="type">bool isPeak;            class=class="str">"cmt">//--- True if peak(high), class="kw">false if trough(low)
};
class=class="str">"cmt">// Structure to store traded patterns
class="kw">struct TradedPattern {
   class="type">class="kw">datetime leftShoulderTime;  class=class="str">"cmt">//--- Timestamp of the left shoulder
   class="type">class="kw">double leftShoulderPrice;  class=class="str">"cmt">//--- Price of the left shoulder
};
class=class="str">"cmt">// Global Variables
class="kw">static class="type">class="kw">datetime lastBarTime = class="num">0;             class=class="str">"cmt">//--- Tracks the timestamp of the last processed bar to avoid reprocessing
TradedPattern tradedPatterns[];              class=class="str">"cmt">//--- Array to store details of previously traded patterns
class="type">int chart_width       = (class="type">int)ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS);     class=class="str">"cmt">//--- Width of the chart in pixels for visualization
class="type">int chart_height      = (class="type">int)ChartGetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS);    class=class="str">"cmt">//--- Height of the chart in pixels for visualization
class="type">int chart_scale       = (class="type">int)ChartGetInteger(class="num">0, CHART_SCALE);               class=class="str">"cmt">//--- Zoom level of the chart(class="num">0-class="num">5)
class="type">int chart_first_vis_bar = (class="type">int)ChartGetInteger(class="num">0, CHART_FIRST_VISIBLE_BAR); class=class="str">"cmt">//--- Index of the first visible bar on the chart
class="type">int chart_vis_bars    = (class="type">int)ChartGetInteger(class="num">0, CHART_VISIBLE_BARS);        class=class="str">"cmt">//--- Number of visible bars on the chart
class="type">class="kw">double chart_prcmin   = ChartGetDouble(class="num">0, CHART_PRICE_MIN, class="num">0);              class=class="str">"cmt">//--- Minimum price visible on the chart
class="type">class="kw">double chart_prcmax   = ChartGetDouble(class="num">0, CHART_PRICE_MAX, class="num">0);              class=class="str">"cmt">//--- Maximum price visible on the chart
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Converts the chart scale class="kw">property to bar width/spacing            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int BarWidth(class="type">int scale) { class="kw">return (class="type">int)pow(class="num">2, scale); }                      class=class="str">"cmt">//--- Calculates bar width in pixels based on chart scale(zoom level)
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Converts the bar index(as series) to x in pixels                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int ShiftToX(class="type">int shift) { class="kw">return (chart_first_vis_bar - shift) * BarWidth(chart_scale) - class="num">1; } class=class="str">"cmt">//--- Converts bar index to x-coordinate in pixels on the chart
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Converts the price to y in pixels                                |
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 把价格和K线时间落到像素与Tick上

在 MT5 自定义指标或 EA 里做图形叠加,第一步是把报价映射成画布坐标。下面这个函数用图表可视区的价格上下沿做线性缩放:价格越靠近 chart_prcmax,y 像素越接近 0;靠近 chart_prcmin 则越靠近 chart_height。若上下沿相等直接返回 0,避免除零崩脚本。 初始化阶段先把交易魔法码绑给 CTrade 对象,并把记录已交易形态的数组清成零长度,EA 才能干净启动并返回 INIT_SUCCEEDED。 OnTick 里用 iTime(_Symbol,_Period,0) 取当前柱时间,和 lastBarTime 比对,相同就直接 return——这套写法能把逻辑锁在每根新柱只跑一次,回测和实盘都不会因同根柱多次报价而重复开仓。外汇与贵金属杠杆高,这类重复触发可能瞬间放大滑点损耗。 图表宽高要随窗口缩放实时刷新,ChartGetInteger(0,CHART_WIDTH_IN_PIXELS) 拿到的是设备像素,后续画水平线或形态框都得靠它算右侧留白。

MQL5 / C++
class="type">int PriceToY(class="type">class="kw">double price) {                                                                     class=class="str">"cmt">//--- Function to convert price to y-coordinate in pixels
   if (chart_prcmax - chart_prcmin == class="num">0.0) class="kw">return class="num">0;                                                class=class="str">"cmt">//--- Return class="num">0 if price range is zero to avoid division by zero
   class="kw">return (class="type">int)round(chart_height * (chart_prcmax - price) / (chart_prcmax - chart_prcmin) - class="num">1);   class=class="str">"cmt">//--- Calculate y-pixel position based on price and chart dimensions
}

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit() {                                                                                      class=class="str">"cmt">//--- Expert Advisor initialization function
   obj_Trade.SetExpertMagicNumber(MagicNumber);                                                     class=class="str">"cmt">//--- Set the magic number for trades opened by this EA
   ArrayResize(tradedPatterns, class="num">0);                                                                  class=class="str">"cmt">//--- Initialize tradedPatterns array with zero size
   class="kw">return(INIT_SUCCEEDED);                                                                          class=class="str">"cmt">//--- Return success code to indicate successful initialization
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick() {                                                                                     class=class="str">"cmt">//--- Main tick function executed on each price update
   class="type">class="kw">datetime currentBarTime = iTime(_Symbol, _Period, class="num">0);                                            class=class="str">"cmt">//--- Get the timestamp of the current bar
   if (currentBarTime == lastBarTime) class="kw">return;                                                       class=class="str">"cmt">//--- Exit if the current bar has already been processed
   lastBarTime = currentBarTime;                                                                    class=class="str">"cmt">//--- Update the last processed bar time
   class=class="str">"cmt">// Update chart properties
   chart_width       = (class="type">int)ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS);                              class=class="str">"cmt">//--- Update chart width in pixels

抓取图表状态再找极值

做形态识别前,先把当前图表的可视范围参数刷一遍。下面这段把像素高度、缩放级别、首根可见 bar 索引、可见 bar 数量、可视最低/最高价全部读进变量,避免在错误坐标上算形态。 chart_height = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); // 更新图表像素高度 chart_scale = (int)ChartGetInteger(0, CHART_SCALE); // 更新图表缩放等级 chart_first_vis_bar = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR); // 更新首根可见 bar 索引 chart_vis_bars = (int)ChartGetInteger(0, CHART_VISIBLE_BARS); // 更新可见 bar 数量 chart_prcmin = ChartGetDouble(0, CHART_PRICE_MIN, 0); // 更新图表可视最低价 chart_prcmax = ChartGetDouble(0, CHART_PRICE_MAX, 0); // 更新图表可视最高价 if (PositionsTotal() > 0) return; // 有持仓就退出,防止重复开单 读完后若已有持仓直接 return,这是防止同周期重复触发交易的最简手段。外汇与贵金属杠杆高,多次误触发可能迅速放大回撤。 极值查找函数 FindExtrema 先释放旧数组,再取 Bars(_Symbol,_Period) 总量;若 lookback 超出可用 bar 数则降至 bars-1,防止越界。highs/lows 数组均设为时间序列(最新在索引0),后续按倒序扫峰谷才符合 MT5 的报价结构。 ArrayFree(extrema); // 清空极值数组重新来 int bars = Bars(_Symbol, _Period); // 取当前品种周期总 bar 数 if (lookback >= bars) lookback = bars - 1; // lookback 超界则修正 ouble highs[], lows[]; // 存高低价的中间数组 ArraySetAsSeries(highs, true); // 高数组按时间序列排列 ArraySetAsSeries(lows, true); // 低数组按时间序列排列 开 MT5 按 F4 把这段塞进 EA,改 lookback 从 50 到 200 看峰值数量变化,能直观感受噪声与信号的边界。

MQL5 / C++
  chart_height      = (class="type">int)ChartGetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS); class=class="str">"cmt">//--- Update chart height in pixels
  chart_scale        = (class="type">int)ChartGetInteger(class="num">0, CHART_SCALE);            class=class="str">"cmt">//--- Update chart zoom level
  chart_first_vis_bar = (class="type">int)ChartGetInteger(class="num">0, CHART_FIRST_VISIBLE_BAR); class=class="str">"cmt">//--- Update index of the first visible bar
  chart_vis_bars     = (class="type">int)ChartGetInteger(class="num">0, CHART_VISIBLE_BARS);     class=class="str">"cmt">//--- Update number of visible bars
  chart_prcmin       = ChartGetDouble(class="num">0, CHART_PRICE_MIN, class="num">0);           class=class="str">"cmt">//--- Update minimum visible price on chart
  chart_prcmax       = ChartGetDouble(class="num">0, CHART_PRICE_MAX, class="num">0);           class=class="str">"cmt">//--- Update maximum visible price on chart
  class=class="str">"cmt">// Skip pattern detection if a position is already open
  if (PositionsTotal() > class="num">0) class="kw">return;                                     class=class="str">"cmt">//--- Exit function if there are open positions to avoid multiple trades
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Find extrema in the last N bars                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void FindExtrema(Extremum &extrema[], class="type">int lookback) {                 class=class="str">"cmt">//--- Function to identify peaks and troughs in price history
  ArrayFree(extrema);                                                  class=class="str">"cmt">//--- Clear the extrema array to start fresh
  class="type">int bars = Bars(_Symbol, _Period);                                   class=class="str">"cmt">//--- Get total number of bars available
  if (lookback >= bars) lookback = bars - class="num">1;                           class=class="str">"cmt">//--- Adjust lookback if it exceeds available bars
  class="type">class="kw">double highs[], lows[];                                              class=class="str">"cmt">//--- Arrays to store high and low prices
  ArraySetAsSeries(highs, true);                                       class=class="str">"cmt">//--- Set highs array as time series(newest first)
  ArraySetAsSeries(lows, true);                                        class=class="str">"cmt">//--- Set lows array as time series(newest first)

「用阈值过滤假突破的拐点扫描」

趋势段里最怕把毛刺当反转。这段代码用 CopyHigh / CopyLow 把最近 lookback+1 根 K 线的高低点拉进数组,先以最老两根 K 的高低关系定初始方向:highs[lookback] < highs[lookback-1] 判为上涨段。 核心过滤在 ThresholdPoints * _Point。以上涨段为例,只有当某根低点跌破「前一个极点高位 - 阈值点数×最小变动单位」时,才把上一个 lastHigh 对应的 bar 记为峰(peak),否则新高就持续刷新 lastHigh 与 lastExtremumBar。外汇与贵金属波动大,阈值设太小会频繁误判反转,设太大又容易漏掉真实拐点,属于典型的高风险参数博弈。 落地时把 ThresholdPoints 当成可调旋钮:EURUSD 在 M15 上可先试 15~30 点,XAUUSD 因 _Point 为 0.01 且波幅大,可能要放到 80~150 点才有过滤效果。直接开 MT5 把下面片段塞进 EA 的 OnCalculate 前面部分,改 ThresholdPoints 跑一遍历史,看极点密度是否符合你的肉眼。

MQL5 / C++
  CopyHigh(_Symbol, _Period, class="num">0, lookback + class="num">1, highs);                    class=class="str">"cmt">//--- Copy high prices for lookback period
  CopyLow(_Symbol, _Period, class="num">0, lookback + class="num">1, lows);                        class=class="str">"cmt">//--- Copy low prices for lookback period
  class="type">bool isUpTrend = highs[lookback] < highs[lookback - class="num">1];                  class=class="str">"cmt">//--- Determine initial trend based on first two bars
  class="type">class="kw">double lastHigh = highs[lookback];                                      class=class="str">"cmt">//--- Initialize last high price
  class="type">class="kw">double lastLow = lows[lookback];                                        class=class="str">"cmt">//--- Initialize last low price
  class="type">int lastExtremumBar = lookback;                                         class=class="str">"cmt">//--- Initialize last extremum bar index
  for (class="type">int i = lookback - class="num">1; i >= class="num">0; i--) {                               class=class="str">"cmt">//--- Loop through bars from oldest to newest
    if (isUpTrend) {                                                      class=class="str">"cmt">//--- If currently in an uptrend
      if (highs[i] > lastHigh) {                                          class=class="str">"cmt">//--- Check if current high exceeds last high
        lastHigh = highs[i];                                              class=class="str">"cmt">//--- Update last high price
        lastExtremumBar = i;                                              class=class="str">"cmt">//--- Update last extremum bar index
      } else if (lows[i] < lastHigh - ThresholdPoints * _Point) {         class=class="str">"cmt">//--- Check if current low indicates a reversal(trough)
        class="type">int size = ArraySize(extrema);                                    class=class="str">"cmt">//--- Get current size of extrema array
        ArrayResize(extrema, size + class="num">1);                                   class=class="str">"cmt">//--- Resize array to add new extremum
        extrema[size].bar = lastExtremumBar;                              class=class="str">"cmt">//--- Store bar index of the peak
        extrema[size].time = iTime(_Symbol, _Period, lastExtremumBar);    class=class="str">"cmt">//--- Store timestamp of the peak

◍ 峰谷切换时怎么落笔到数组

这一段处理的是趋势状态机里的「拐点登记」动作。当代码判定一段上行结束、价格开始破前低反抽超过阈值时,先把刚才跟踪到的峰值写进 extrema 数组:记录价格、打上 isPeak 标记,并把趋势旗标 isUpTrend 翻成 false,同时把 lastLow 与 lastExtremumBar 重置为当前下行起点。 若原本就处在下行段,逻辑分两层:只要 lows[i] 比 lastLow 更低就继续刷新谷值;一旦 highs[i] 越过 lastLow 加上 ThresholdPoints*_Point,就视为反转雏形,此时先 ArraySize 取尺寸、ArrayResize 扩一位,再把 lastExtremumBar 对应的时间、价格作为新谷写入。 外汇与贵金属波动剧烈,ThresholdPoints 设太小会在毛刺里频繁误判拐点,建议拿 EURUSD 的 M15 历史数据跑一遍,观察 ThresholdPoints=20 与 =50 时 extrema 数组条目数差异,再定参数。

MQL5 / C++
extrema[size].price = lastHigh;                 class=class="str">"cmt">//--- Store price of the peak
   extrema[size].isPeak = true;                   class=class="str">"cmt">//--- Mark as a peak
   class=class="str">"cmt">//Print("Extrema added: Bar ", lastExtremumBar, ", Time ", TimeToString(extrema[size].time), ", Price ", DoubleToString(lastHigh, _Digits), ", IsPeak true"); //--- Log new peak
   isUpTrend = class="kw">false;                             class=class="str">"cmt">//--- Switch trend to downtrend
   lastLow = lows[i];                             class=class="str">"cmt">//--- Update last low price
   lastExtremumBar = i;                           class=class="str">"cmt">//--- Update last extremum bar index
   }
   } else {                                       class=class="str">"cmt">//--- If currently in a downtrend
    if (lows[i] < lastLow) {                      class=class="str">"cmt">//--- Check if current low is below last low
     lastLow = lows[i];                           class=class="str">"cmt">//--- Update last low price
     lastExtremumBar = i;                         class=class="str">"cmt">//--- Update last extremum bar index
    } else if (highs[i] > lastLow + ThresholdPoints * _Point) { class=class="str">"cmt">//--- Check if current high indicates a reversal(peak)
     class="type">int size = ArraySize(extrema);               class=class="str">"cmt">//--- Get current size of extrema array
     ArrayResize(extrema, size + class="num">1);              class=class="str">"cmt">//--- Resize array to add new extremum
     extrema[size].bar = lastExtremumBar;         class=class="str">"cmt">//--- Store bar index of the trough
     extrema[size].time = iTime(_Symbol, _Period, lastExtremumBar); class=class="str">"cmt">//--- Store timestamp of the trough
     extrema[size].price = lastLow;               class=class="str">"cmt">//--- Store price of the trough

谷底翻转后如何挂上头肩判定

上一节在扫描到更低低点时会把当前极值标记为谷底:extrema[size].isPeak 置为 false,同时把 isUpTrend 翻成 true,并用 lastHigh、lastExtremumBar 记录反转起点。这段逻辑跑完,极值数组里就混入了可供形态识别的转折点。 极值落库后,脚本声明 Extremum extrema[] 并在主流程调用 FindExtrema(extrema, LookbackBars),只回看最近 LookbackBars 根 K 线。若你改小 LookbackBars,旧头肩会被忽略,信号频率可能上升但假突破概率也偏高。 DetectHeadAndShoulders 是标准头肩的识别函数。它先取数组长度,不足 6 个极值直接返回 false——因为标准头肩至少需要左肩谷、左肩峰、头下谷、头部峰、头右谷、右肩峰这 6 点。 函数从 size-6 往前循环,要求序列严格为「谷、峰、谷、峰、谷、峰」:即 !isPeak、isPeak、!isPeak、isPeak、!isPeak、isPeak。命中后取 extrema[i+1].price 作为左肩高度,后续再比头部与右肩。外汇与贵金属波动跳空多,这种纯价格序列判定在重大数据夜可能漏掉变异头肩,实盘前请在 MT5 用历史品种跑一遍校验。

MQL5 / C++
extrema[size].isPeak = class="kw">false;                                                 class=class="str">"cmt">//--- Mark as a trough
class=class="str">"cmt">//Print("Extrema added: Bar ", lastExtremumBar, ", Time ", TimeToString(extrema[size].time), ", Price ", DoubleToString(lastLow, _Digits), ", IsPeak class="kw">false"); //--- Log new trough
isUpTrend = true;                                                                  class=class="str">"cmt">//--- Switch trend to uptrend
lastHigh = highs[i];                                                               class=class="str">"cmt">//--- Update last high price
lastExtremumBar = i;                                                               class=class="str">"cmt">//--- Update last extremum bar index
     }
   }
 }
}
Extremum extrema[];                                                                class=class="str">"cmt">//--- Array to store identified peaks and troughs
FindExtrema(extrema, LookbackBars);                                                class=class="str">"cmt">//--- Find extrema in the last LookbackBars bars
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Detect standard Head and Shoulders pattern                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool DetectHeadAndShoulders(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 standard 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 H&S pattern(start at size-class="num">6 to ensure enough points)
    if (!extrema[i].isPeak && extrema[i+class="num">1].isPeak && !extrema[i+class="num">2].isPeak &&       class=class="str">"cmt">//--- Check sequence: trough, peak(LS), trough
        extrema[i+class="num">3].isPeak && !extrema[i+class="num">4].isPeak && extrema[i+class="num">5].isPeak) {      class=class="str">"cmt">//--- Check sequence: peak(head), trough, peak(RS)
      class="type">class="kw">double leftShoulder = extrema[i+class="num">1].price;                                    class=class="str">"cmt">//--- Get price of left shoulder
交给小布盯盘标注形态
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到头肩结构的自动识别与颈线提示,把重复劳动交给小布,你专注决策。

常见问题

标准变体基于上升趋势的三个峰值与下破颈线做空,反向变体基于下降趋势的三个谷底与上破颈线做多,两者肩高容差与谷底容差参数需分别校准。
回看窗口过短可能切不掉早期波动,左肩或头部未被纳入检测,形态不完整时 EA 不会触发信号,错过转折概率上升。
前者确认反转幅度是否足够,后者放宽两肩高度接近的容许偏差;容差过窄易过滤真形态,过宽则引入对称失真噪音。
可以,小布的品种页已内置形态识别模块,逻辑与量化肩高容差一致,无需自己部署 MetaEditor 也能看盘口标注。
移动止损会改变盈亏分布曲线,关掉是裸形态收益,打开是锁利后的回撤控制,两者分开看才能知道策略韧性来自哪。