在 MQL5 中自动化交易策略(第 13 部分):构建头肩形态交易算法·进阶篇
(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 对照屏幕。
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) 拿到的是设备像素,后续画水平线或形态框都得靠它算右侧留白。
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 看峰值数量变化,能直观感受噪声与信号的边界。
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 跑一遍历史,看极点密度是否符合你的肉眼。
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 数组条目数差异,再定参数。
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 用历史品种跑一遍校验。
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