English Русский Deutsch 日本語 Português
preview
在 MQL5 中自动化交易策略(第 13 部分):构建头肩形态交易算法

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

MetaTrader 5交易 |
926 0
Allan Munene Mutiiria
Allan Munene Mutiiria

引言

在我们的上一篇文章(第 12 部分)中,我们在 MetaQuotes Language 5 (MQL5) 中实现了缓解订单块策略,以利用机构价格区域进行交易。现在,在第 13 部分中,我们将重点转向构建一个头肩形态交易算法,自动化一个经典的反转形态,以精确捕捉市场转折点。我们将涵盖以下主题:

  1. 理解头肩形态的架构
  2. 在MQL5中的实现
  3. 回测
  4. 结论

在本文结束时,您将拥有一个功能齐全的EA,可以交易头肩形态——让我们开始吧!


理解头肩形态的架构

头肩形态是技术分析中广泛认可的一种经典图表形态,用于预测趋势反转,它同时出现在标准(看跌)和反向(看涨)两种变体中,每种变体都由一系列独特的价格峰值或谷底序列定义。在我们的程序中,对于标准形态,上升趋势将形成三个峰值:左肩确立一个高点,头部作为趋势的顶点将明显高于两肩(显著超过左右两肩),右肩形成的高度低于头部但接近左肩,所有这些都由一条连接两个谷底的颈线串联起来——一旦价格跌破此线,我们将在突破点建立空头交易,将止损设置在右肩上方,并通过将头部到颈线的高度向下投射来设定止盈目标,如下图所示。

看跌头肩形态

对于反向形态,下降趋势将形成三个谷底:左肩标记一个低点,头部将显著下探更深(低于两肩),右肩将与左肩的高度对齐,一条颈线横跨两个峰值——价格向上突破颈线将触发多头入场,止损设在右肩下方,止盈则通过颈线到头部的距离向上延伸,所有这些都基于头部的突出高度和两肩的接近对称性,作为我们的指导规则。这是其图示。

看涨头肩形态

至于风险管理,我们将集成一个可选的移动止损功能,以锁定利润并最大化收益。让我们开始吧。


在MQL5中的实现

要在 MQL5 中创建程序,请打开MetaEditor,转到“导航器”窗口,找到“指标”文件夹,点击“新建”选项卡,然后按照提示创建文件。文件创建完成后,在代码编环境下,我们需要声明一些将在整个程序中使用的全局变量

//+------------------------------------------------------------------+
//|                                  Head & Shoulders Pattern EA.mq5 |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link      "https://youtube.com/@ForexAlgo-Trader?"
#property version   "1.00"

#include <Trade\Trade.mqh>                    //--- Include the Trade.mqh library for trading functions
CTrade obj_Trade;                            //--- Trade object for executing and managing trades

// Input Parameters
input int LookbackBars = 50;                   // Number of historical bars to analyze for pattern detection
input double ThresholdPoints = 70.0;           // Minimum price movement in points to identify a reversal
input double ShoulderTolerancePoints = 15.0;   // Maximum allowable price difference between left and right shoulders
input double TroughTolerancePoints = 30.0;     // Maximum allowable price difference between neckline troughs or peaks
input double BufferPoints = 10.0;              // Additional points added to stop-loss for safety buffer
input double LotSize = 0.1;                    // Volume of each trade in lots
input ulong MagicNumber = 123456;              // Unique identifier for trades opened by this EA
input int MaxBarRange = 30;                    // Maximum number of bars allowed between key pattern points
input int MinBarRange = 5;                     // Minimum number of bars required between key pattern points
input double BarRangeMultiplier = 2.0;         // Maximum multiple of the smallest bar range for pattern uniformity
input int ValidationBars = 3;                  // Number of bars after right shoulder to validate breakout
input double PriceTolerance = 5.0;             // Price tolerance in points for matching traded patterns
input double RightShoulderBreakoutMultiplier = 1.5; // Maximum multiple of pattern range for right shoulder to breakout distance
input int MaxTradedPatterns = 20;              // Maximum number of patterns stored in traded history
input bool UseTrailingStop = false;             // Toggle to enable or disable trailing stop functionality
input int MinTrailPoints = 50;                 // Minimum profit in points before trailing stop activates
input int TrailingPoints = 30;                 // Distance in points to maintain behind current price when trailing

在这里,我们以 "#include <Trade\Trade.mqh>" 和一个 “CTrade” 对象 “obj_Trade” 开始,以便包含用于交易管理的额外交易文件。我们设置了一些输入参数,例如用于历史分析的 “LookbackBars”(默认 50)、用于确认反转的 “ThresholdPoints”(默认 70.0),以及用于对称性判断的 “ShoulderTolerancePoints”(默认 15.0)和 “TroughTolerancePoints”(默认 30.0)。其余的输入参数不言自明。我们添加了详细的注释以便于理解。接下来,我们需要定义一些结构体,用于寻找形态和管理已考虑的交易。

// Structure to store peaks and troughs
struct Extremum {
   int bar;           //--- Bar index where extremum occurs
   datetime time;     //--- Timestamp of the bar
   double price;      //--- Price at extremum (high for peak, low for trough)
   bool isPeak;       //--- True if peak (high), false if trough (low)
};

// Structure to store traded patterns
struct TradedPattern {
   datetime leftShoulderTime;  //--- Timestamp of the left shoulder
   double leftShoulderPrice;   //--- Price of the left shoulder
};

我们使用 struct 关键字设置了两个关键的结构体来驱动我们的头肩形态交易算法:“Extremum” 将存储峰值和谷底,包含 “bar”(索引)、“time”(时间戳)、“price”(值)和 “isPeak”(峰值为 true,谷底为 false)以精确定位形态的组成部分;而 “TradedPattern” 将使用 “leftShoulderTime” 和 “leftShoulderPrice” 来跟踪已执行的交易,以防止重复交易。为确保我们每根 K 线只交易一次并跟踪正在进行的交易,我们声明了一个变量和一个数组,如下所示。

// Global Variables
static datetime lastBarTime = 0;         //--- Tracks the timestamp of the last processed bar to avoid reprocessing
TradedPattern tradedPatterns[];          //--- Array to store details of previously traded patterns

至此,我们准备就绪。但是,由于我们需要在图表上显示该形态,我们将需要获取图表架构和 K 线组成部分,以确保它能适应形态的要求。

int chart_width         = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);        //--- Width of the chart in pixels for visualization
int chart_height        = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);       //--- Height of the chart in pixels for visualization
int chart_scale         = (int)ChartGetInteger(0, CHART_SCALE);                  //--- Zoom level of the chart (0-5)
int chart_first_vis_bar = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR);      //--- Index of the first visible bar on the chart
int chart_vis_bars      = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);           //--- Number of visible bars on the chart
double chart_prcmin     = ChartGetDouble(0, CHART_PRICE_MIN, 0);                 //--- Minimum price visible on the chart
double chart_prcmax     = ChartGetDouble(0, CHART_PRICE_MAX, 0);                 //--- Maximum price visible on the chart

//+------------------------------------------------------------------+
//| Converts the chart scale property to bar width/spacing           |
//+------------------------------------------------------------------+
int BarWidth(int scale) { return (int)pow(2, scale); }                           //--- Calculates bar width in pixels based on chart scale (zoom level)

//+------------------------------------------------------------------+
//| Converts the bar index (as series) to x in pixels                |
//+------------------------------------------------------------------+
int ShiftToX(int shift) { return (chart_first_vis_bar - shift) * BarWidth(chart_scale) - 1; } //--- Converts bar index to x-coordinate in pixels on the chart

//+------------------------------------------------------------------+
//| Converts the price to y in pixels                                |
//+------------------------------------------------------------------+
int PriceToY(double price) {                                                     //--- Function to convert price to y-coordinate in pixels
   if (chart_prcmax - chart_prcmin == 0.0) return 0;                             //--- Return 0 if price range is zero to avoid division by zero
   return (int)round(chart_height * (chart_prcmax - price) / (chart_prcmax - chart_prcmin) - 1); //--- Calculate y-pixel position based on price and chart dimensions
}

我们通过使用 ChartGetInteger 函数定义 “chart_width” 和 “chart_height” 等变量来获取图表尺寸,使用 “chart_scale” 获取缩放比例,使用 “chart_first_vis_bar” 和 “chart_vis_bars” 获取 K 线详情,并通过 ChartGetDouble 获取 “chart_prcmin” 和 “chart_prcmax” 以得到价格范围,从而为程序准备并配备可视化功能。我们使用带有 pow 函数的 “BarWidth” 函数,根据 “chart_scale” 计算 K 线间距;使用 “ShiftToX” 函数,通过 “chart_first_vis_bar” 和 “chart_scale” 将 K 线索引转换为 x 坐标;并使用带有 round 函数的 “PriceToY” 函数,基于 “chart_height”、“chart_prcmax” 和 “chart_prcmin” 将价格映射到 y 坐标,从而实现精确的形态显示。我们现在已完全准备就绪。可以继续在 OnInit 事件处理程序中初始化程序了。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {                                                           //--- Expert Advisor initialization function
   obj_Trade.SetExpertMagicNumber(MagicNumber);                          //--- Set the magic number for trades opened by this EA
   ArrayResize(tradedPatterns, 0);                                       //--- Initialize tradedPatterns array with zero size
   return(INIT_SUCCEEDED);                                               //--- Return success code to indicate successful initialization
}

OnInit 中,我们在 “obj_Trade” 对象上使用 “SetExpertMagicNumber” 方法,将 “MagicNumber” 指定为所有交易的唯一标识符,以确保我们程序的持仓是可区分的;同时调用 ArrayResize 函数将 “tradedPatterns” 数组的大小设置为零,以清除任何先前的数据,从而实现全新的开始。然后,我们通过返回 INIT_SUCCEEDED 来结束,以确认设置成功,为EA有效检测和交易该形态做好准备。现在我们可以继续处理 OnTick 事件处理程序,并确保我们每根 K 线只分析一次。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {                                                          //--- Main tick function executed on each price update
   datetime currentBarTime = iTime(_Symbol, _Period, 0);                 //--- Get the timestamp of the current bar
   if (currentBarTime == lastBarTime) return;                            //--- Exit if the current bar has already been processed

   lastBarTime = currentBarTime;                                         //--- Update the last processed bar time

   // Update chart properties
   chart_width         = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Update chart width in pixels
   chart_height        = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Update chart height in pixels
   chart_scale         = (int)ChartGetInteger(0, CHART_SCALE);           //--- Update chart zoom level
   chart_first_vis_bar = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR); //--- Update index of the first visible bar
   chart_vis_bars      = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);    //--- Update number of visible bars
   chart_prcmin        = ChartGetDouble(0, CHART_PRICE_MIN, 0);          //--- Update minimum visible price on chart
   chart_prcmax        = ChartGetDouble(0, CHART_PRICE_MAX, 0);          //--- Update maximum visible price on chart

   // Skip pattern detection if a position is already open
   if (PositionsTotal() > 0) return;                                     //--- Exit function if there are open positions to avoid multiple trades
}

OnTick 事件处理程序中(该程序在每次价格更新时激活,以监控和响应市场变化),我们使用 iTime 函数获取最新 K 线的 “currentBarTime”,并将其与 “lastBarTime” 进行比较以避免重复处理,仅在新 K 线出现时才更新 “lastBarTime”;然后,我们通过调用 ChartGetInteger 来刷新图表视觉效果,更新 “chart_width”、“chart_height”、“chart_scale”、“chart_first_vis_bar” 和 “chart_vis_bars”,并调用 ChartGetDouble 来更新 “chart_prcmin” 和 “chart_prcmax”。我们还使用 PositionsTotal 函数检查是否有未平仓的交易,如果存在任何持仓则提前退出,以防止持仓重叠,为形态检测和交易做好准备。然后,我们可以定义一个函数来寻找极值点或关键的形态点。

//+------------------------------------------------------------------+
//| Find extrema in the last N bars                                  |
//+------------------------------------------------------------------+
void FindExtrema(Extremum &extrema[], int lookback) {                    //--- Function to identify peaks and troughs in price history
   ArrayFree(extrema);                                                   //--- Clear the extrema array to start fresh
   int bars = Bars(_Symbol, _Period);                                    //--- Get total number of bars available
   if (lookback >= bars) lookback = bars - 1;                            //--- Adjust lookback if it exceeds available bars

   double highs[], lows[];                                               //--- Arrays to store high and low prices
   ArraySetAsSeries(highs, true);                                        //--- Set highs array as time series (newest first)
   ArraySetAsSeries(lows, true);                                         //--- Set lows array as time series (newest first)
   CopyHigh(_Symbol, _Period, 0, lookback + 1, highs);                   //--- Copy high prices for lookback period
   CopyLow(_Symbol, _Period, 0, lookback + 1, lows);                     //--- Copy low prices for lookback period

   bool isUpTrend = highs[lookback] < highs[lookback - 1];               //--- Determine initial trend based on first two bars
   double lastHigh = highs[lookback];                                    //--- Initialize last high price
   double lastLow = lows[lookback];                                      //--- Initialize last low price
   int lastExtremumBar = lookback;                                       //--- Initialize last extremum bar index

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

在这里,我们通过实现 “FindExtrema” 函数来精确定义我们头肩形态的峰值和谷底,该函数分析最近的 “lookback” 根 K 线,以构建一个包含关键价格点的 “extrema” 数组。我们首先使用 ArrayFree 函数重置 “extrema” 数组,以确保一个干净的状态;然后使用 “Bars” 函数获取可用的 K 线总数,如果 “lookback” 超过此限制,则对其进行封顶,以保证我们始终在图表的数据范围内。接下来,我们准备 “highs” 和 “lows” 数组来存放价格数据,使用 ArraySetAsSeries 函数将它们设置为时间序列(最新的在前),并使用 CopyHighCopyLow 函数填充它们,以提取 “lookback + 1” 根 K 线的最高价和最低价。

在一个从最旧 K 线到最新 K 线的循环中,我们根据初始价格变动用 “isUpTrend” 来确定趋势,然后跟踪 “lastHigh” 或 “lastLow” 及其 “lastExtremumBar”;当反转超过 “ThresholdPoints” 时,我们使用 ArrayResize 函数扩展 “extrema” 数组,存储诸如 “bar”、“time”(通过 “iTime” 获取)、“price” 和 “isPeak”(峰值为 true,谷底为 false)等详细信息,并切换趋势,从而实现精确的形态识别。现在,我们可以获取已识别的价格水平并将其保存以供进一步使用。

Extremum extrema[];                                                   //--- Array to store identified peaks and troughs
FindExtrema(extrema, LookbackBars);                                   //--- Find extrema in the last LookbackBars bars

在这里,我们声明一个 “Extremum” 类型的 “extrema” 数组来存放已识别的峰值和谷底,该数组将存储形态的肩部和头部。然后,我们调用 “FindExtrema” 函数,将 “extrema” 和 “LookbackBars” 作为参数传递,以扫描最近的 “LookbackBars” 根 K 线,并用关键的极值点填充该数组,为形态识别和后续的交易决策奠定基础。当我们使用 ArrayPrint 函数打印数组值时,会得到如下所示的结构。

存储的价格数据

这证实了我们拥有必要的数据点。因此,我们可以继续进行形态组成部分的识别。为了使代码模块化,我们采用了函数。

//+------------------------------------------------------------------+
//| Detect standard Head and Shoulders pattern                       |
//+------------------------------------------------------------------+
bool DetectHeadAndShoulders(Extremum &extrema[], int &leftShoulderIdx, int &headIdx, int &rightShoulderIdx, int &necklineStartIdx, int &necklineEndIdx) { //--- Function to detect standard H&S pattern
   int size = ArraySize(extrema);                                        //--- Get the size of the extrema array
   if (size < 6) return false;                                           //--- Return false if insufficient extrema for pattern (need at least 6 points)

   for (int i = size - 6; i >= 0; i--) {                                 //--- Loop through extrema to find H&S pattern (start at size-6 to ensure enough points)
      if (!extrema[i].isPeak && extrema[i+1].isPeak && !extrema[i+2].isPeak && //--- Check sequence: trough, peak (LS), trough
          extrema[i+3].isPeak && !extrema[i+4].isPeak && extrema[i+5].isPeak) { //--- Check sequence: peak (head), trough, peak (RS)
         double leftShoulder = extrema[i+1].price;                       //--- Get price of left shoulder
         double head = extrema[i+3].price;                               //--- Get price of head
         double rightShoulder = extrema[i+5].price;                      //--- Get price of right shoulder
         double trough1 = extrema[i+2].price;                            //--- Get price of first trough (neckline start)
         double trough2 = extrema[i+4].price;                            //--- Get price of second trough (neckline end)

         bool isHeadHighest = true;                                      //--- Flag to verify head is the highest peak in range
         for (int j = MathMax(0, i - 5); j < MathMin(size, i + 10); j++) { //--- Check surrounding bars (5 before, 10 after) for higher peaks
            if (extrema[j].isPeak && extrema[j].price > head && j != i + 3) { //--- If another peak is higher than head
               isHeadHighest = false;                                    //--- Set flag to false
               break;                                                    //--- Exit loop as head is not highest
            }
         }

         int lsBar = extrema[i+1].bar;                                   //--- Get bar index of left shoulder
         int headBar = extrema[i+3].bar;                                 //--- Get bar index of head
         int rsBar = extrema[i+5].bar;                                   //--- Get bar index of right shoulder
         int lsToHead = lsBar - headBar;                                 //--- Calculate bars from left shoulder to head
         int headToRs = headBar - rsBar;                                 //--- Calculate bars from head to right shoulder

         if (lsToHead < MinBarRange || lsToHead > MaxBarRange || headToRs < MinBarRange || headToRs > MaxBarRange) continue; //--- Skip if bar ranges are out of bounds

         int minRange = MathMin(lsToHead, headToRs);                     //--- Get the smaller of the two ranges for uniformity check
         if (lsToHead > minRange * BarRangeMultiplier || headToRs > minRange * BarRangeMultiplier) continue; //--- Skip if ranges exceed uniformity multiplier

         bool rsValid = false;                                           //--- Flag to validate right shoulder breakout
         int rsBarIndex = extrema[i+5].bar;                              //--- Get bar index of right shoulder for validation
         for (int j = rsBarIndex - 1; j >= MathMax(0, rsBarIndex - ValidationBars); j--) { //--- Check bars after right shoulder for breakout
            if (iLow(_Symbol, _Period, j) < rightShoulder - ThresholdPoints * _Point) { //--- Check if price drops below RS by threshold
               rsValid = true;                                           //--- Set flag to true if breakout confirmed
               break;                                                    //--- Exit loop once breakout is validated
            }
         }
         if (!rsValid) continue;                                         //--- Skip if right shoulder breakout not validated

         if (isHeadHighest && head > leftShoulder && head > rightShoulder && //--- Verify head is highest and above shoulders
             MathAbs(leftShoulder - rightShoulder) < ShoulderTolerancePoints * _Point && //--- Check shoulder price difference within tolerance
             MathAbs(trough1 - trough2) < TroughTolerancePoints * _Point) { //--- Check trough price difference within tolerance
            leftShoulderIdx = i + 1;                                     //--- Set index for left shoulder
            headIdx = i + 3;                                             //--- Set index for head
            rightShoulderIdx = i + 5;                                    //--- Set index for right shoulder
            necklineStartIdx = i + 2;                                    //--- Set index for neckline start (first trough)
            necklineEndIdx = i + 4;                                      //--- Set index for neckline end (second trough)
            Print("Bar Ranges: LS to Head = ", lsToHead, ", Head to RS = ", headToRs); //--- Log bar ranges for debugging
            return true;                                                 //--- Return true to indicate pattern found
         }
      }
   }
   return false;                                                         //--- Return false if no pattern detected
}

在这里,我们通过 “DetectHeadAndShoulders” 函数来识别标准形态,该函数检查 “extrema” 数组以寻找一个由六个点组成的有效序列:一个谷底、一个峰值(左肩)、一个谷底、一个峰值(头部)、一个谷底和一个峰值(右肩),这要求至少有六个条目,通过 ArraySize 函数进行检查。我们从 “size - 6” 开始遍历 “extrema”,验证形态的峰-谷交替结构,然后提取 “leftShoulder”(左肩)、“head”(头部)、“rightShoulder”(右肩)以及颈线谷底(“trough1”、“trough2”)的价格;一个嵌套循环使用 MathMaxMathMin 函数确保头部在某个范围内是最高的峰值,同时点与点之间的 K 线距离受到 “MinBarRange” 和 “MaxBarRange” 的约束,并通过 “BarRangeMultiplier” 来保证其均匀性。

我们通过在 “ValidationBars” 根 K 线上,使用 iLow 函数检查价格是否跌破 “ThresholdPoints” 来确认右肩的突破,如果头部高于两肩且满足容差要求(“ShoulderTolerancePoints”、“TroughTolerancePoints”),我们就分配 “leftShoulderIdx”、“headIdx” 和 “necklineStartIdx” 等索引,使用 Print 函数记录 K 线范围以便调试,并返回 true 来表示检测到形态,否则返回 false。我们使用相同的逻辑来寻找相反的形态。

//+------------------------------------------------------------------+
//| Detect inverse Head and Shoulders pattern                        |
//+------------------------------------------------------------------+
bool DetectInverseHeadAndShoulders(Extremum &extrema[], int &leftShoulderIdx, int &headIdx, int &rightShoulderIdx, int &necklineStartIdx, int &necklineEndIdx) { //--- Function to detect inverse H&S pattern
   int size = ArraySize(extrema);                                        //--- Get the size of the extrema array
   if (size < 6) return false;                                           //--- Return false if insufficient extrema for pattern (need at least 6 points)

   for (int i = size - 6; i >= 0; i--) {                                 //--- Loop through extrema to find inverse H&S pattern
      if (extrema[i].isPeak && !extrema[i+1].isPeak && extrema[i+2].isPeak && //--- Check sequence: peak, trough (LS), peak
          !extrema[i+3].isPeak && extrema[i+4].isPeak && !extrema[i+5].isPeak) { //--- Check sequence: trough (head), peak, trough (RS)
         double leftShoulder = extrema[i+1].price;                       //--- Get price of left shoulder
         double head = extrema[i+3].price;                               //--- Get price of head
         double rightShoulder = extrema[i+5].price;                      //--- Get price of right shoulder
         double peak1 = extrema[i+2].price;                              //--- Get price of first peak (neckline start)
         double peak2 = extrema[i+4].price;                              //--- Get price of second peak (neckline end)

         bool isHeadLowest = true;                                       //--- Flag to verify head is the lowest trough in range
         int headBar = extrema[i+3].bar;                                 //--- Get bar index of head for range check
         for (int j = MathMax(0, headBar - 5); j <= MathMin(Bars(_Symbol, _Period) - 1, headBar + 5); j++) { //--- Check 5 bars before and after head
            if (iLow(_Symbol, _Period, j) < head) {                      //--- If any low is below head
               isHeadLowest = false;                                     //--- Set flag to false
               break;                                                    //--- Exit loop as head is not lowest
            }
         }

         int lsBar = extrema[i+1].bar;                                   //--- Get bar index of left shoulder
         int rsBar = extrema[i+5].bar;                                   //--- Get bar index of right shoulder
         int lsToHead = lsBar - headBar;                                 //--- Calculate bars from left shoulder to head
         int headToRs = headBar - rsBar;                                 //--- Calculate bars from head to right shoulder

         if (lsToHead < MinBarRange || lsToHead > MaxBarRange || headToRs < MinBarRange || headToRs > MaxBarRange) continue; //--- Skip if bar ranges are out of bounds

         int minRange = MathMin(lsToHead, headToRs);                     //--- Get the smaller of the two ranges for uniformity check
         if (lsToHead > minRange * BarRangeMultiplier || headToRs > minRange * BarRangeMultiplier) continue; //--- Skip if ranges exceed uniformity multiplier

         bool rsValid = false;                                           //--- Flag to validate right shoulder breakout
         int rsBarIndex = extrema[i+5].bar;                              //--- Get bar index of right shoulder for validation
         for (int j = rsBarIndex - 1; j >= MathMax(0, rsBarIndex - ValidationBars); j--) { //--- Check bars after right shoulder for breakout
            if (iHigh(_Symbol, _Period, j) > rightShoulder + ThresholdPoints * _Point) { //--- Check if price rises above RS by threshold
               rsValid = true;                                           //--- Set flag to true if breakout confirmed
               break;                                                    //--- Exit loop once breakout is validated
            }
         }
         if (!rsValid) continue;                                         //--- Skip if right shoulder breakout not validated

         if (isHeadLowest && head < leftShoulder && head < rightShoulder && //--- Verify head is lowest and below shoulders
             MathAbs(leftShoulder - rightShoulder) < ShoulderTolerancePoints * _Point && //--- Check shoulder price difference within tolerance
             MathAbs(peak1 - peak2) < TroughTolerancePoints * _Point) { //--- Check peak price difference within tolerance
            leftShoulderIdx = i + 1;                                     //--- Set index for left shoulder
            headIdx = i + 3;                                             //--- Set index for head
            rightShoulderIdx = i + 5;                                    //--- Set index for right shoulder
            necklineStartIdx = i + 2;                                    //--- Set index for neckline start (first peak)
            necklineEndIdx = i + 4;                                      //--- Set index for neckline end (second peak)
            Print("Bar Ranges: LS to Head = ", lsToHead, ", Head to RS = ", headToRs); //--- Log bar ranges for debugging
            return true;                                                 //--- Return true to indicate pattern found
         }
      }
   }
   return false;                                                         //--- Return false if no pattern detected
}

我们定义 “DetectInverseHeadAndShoulders” 函数来识别反向形态,该函数筛选 “extrema” 数组以定位一个由六个点组成的序列——峰值、谷底(左肩)、峰值、谷底(头部)、峰值、谷底(右肩)——这需要至少六个条目,由 ArraySize 函数验证。我们从 “size - 6” 开始向下迭代,确认形态的峰-谷交替,然后提取 “leftShoulder”(左肩)、“head”(头部)、“rightShoulder”(右肩)以及颈线峰值(“peak1”、“peak2”)的价格;一个嵌套循环使用 MathMaxMathMiniLow 函数检查头部是否是 “headBar” K 线周围五根 K 线范围内的最低谷底,同时 “Bars” 函数确保我们不超出图表限制。

我们使用 “MinBarRange” 和 “MaxBarRange” 来强制执行 K 线间距,使用 MathMin 函数和 “BarRangeMultiplier” 计算均匀性,并通过在 “ValidationBars” 根 K 线上使用 iHigh 函数对照 “ThresholdPoints” 来验证右肩的突破;如果 “head” 低于两肩且满足容差要求(“ShoulderTolerancePoints”、“TroughTolerancePoints”),我们就设置 “leftShoulderIdx” 和 “necklineStartIdx” 等索引,记录范围,并返回 true,否则返回 false。现在有了这两个函数,我们可以按如下方式识别形态。

int leftShoulderIdx, headIdx, rightShoulderIdx, necklineStartIdx, necklineEndIdx; //--- Indices for pattern components

// Standard Head and Shoulders (Sell)
if (DetectHeadAndShoulders(extrema, leftShoulderIdx, headIdx, rightShoulderIdx, necklineStartIdx, necklineEndIdx)) { //--- Check for standard H&S pattern
   double closePrice = iClose(_Symbol, _Period, 1);                   //--- Get the closing price of the previous bar
   double necklinePrice = extrema[necklineEndIdx].price;              //--- Get the price of the neckline end point

   if (closePrice < necklinePrice) {                                  //--- Check if price has broken below the neckline (sell signal)
      datetime lsTime = extrema[leftShoulderIdx].time;                //--- Get the timestamp of the left shoulder
      double lsPrice = extrema[leftShoulderIdx].price;                //--- Get the price of the left shoulder

      //---
   }
}

在这里,我们通过声明变量 “leftShoulderIdx”、“headIdx”、“rightShoulderIdx”、“necklineStartIdx” 和 “necklineEndIdx” 来存储形态组成部分的索引,然后使用 “DetectHeadAndShoulders” 函数检查 “extrema” 数组中是否存在标准形态,并将这些索引作为引用传递。如果检测到,我们使用 iClose 函数获取前一根 K 线的 “closePrice”(收盘价),并从 “extrema[necklineEndIdx].price” 获取 “necklinePrice”(颈线价格),如果 “closePrice” 跌破 “necklinePrice”,则触发卖出信号;然后我们从 “extrema[leftShoulderIdx]” 中提取 “lsTime”(左肩时间)和 “lsPrice”(左肩价格),为基于左肩位置执行交易做准备。此时,我们需要确保该形态尚未被交易。我们定义一个函数来执行此检查。

//+------------------------------------------------------------------+
//| Check if pattern has already been traded                         |
//+------------------------------------------------------------------+
bool IsPatternTraded(datetime lsTime, double lsPrice) {                  //--- Function to check if a pattern has already been traded
   int size = ArraySize(tradedPatterns);                                 //--- Get the current size of the tradedPatterns array
   for (int i = 0; i < size; i++) {                                      //--- Loop through all stored traded patterns
      if (tradedPatterns[i].leftShoulderTime == lsTime &&                //--- Check if left shoulder time matches
          MathAbs(tradedPatterns[i].leftShoulderPrice - lsPrice) < PriceTolerance * _Point) { //--- Check if left shoulder price is within tolerance
         Print("Pattern already traded: Left Shoulder Time ", TimeToString(lsTime), ", Price ", DoubleToString(lsPrice, _Digits)); //--- Log that pattern was previously traded
         return true;                                                    //--- Return true to indicate pattern has been traded
      }
   }
   return false;                                                         //--- Return false if no match found
}

在这里,我们通过实现 “IsPatternTraded” 函数来确保我们的程序避免重复交易,该函数检查一个由 “lsTime” 和 “lsPrice” 标识的形态是否已存在于 “tradedPatterns” 数组中。我们使用 ArraySize 函数获取数组的 “size”(大小),然后遍历该数组,通过 MathAbs 函数,在 “PriceTolerance”(价格容差)范围内,将每个条目的 “leftShoulderTime” 与 “lsTime” 以及 “leftShoulderPrice” 与 “lsPrice” 进行比较;如果找到匹配项,我们使用 Print函数将其记录下来,并使用 TimeToStringDoubleToString 函数增强可读性,然后返回 true;否则返回 false,以允许进行新的交易。然后,我们调用该函数进行检查,如果未找到匹配项,则继续执行。

if (IsPatternTraded(lsTime, lsPrice)) return;                   //--- Exit if this pattern has already been traded

datetime breakoutTime = iTime(_Symbol, _Period, 1);             //--- Get the timestamp of the breakout bar (previous bar)
int lsBar = extrema[leftShoulderIdx].bar;                       //--- Get the bar index of the left shoulder
int headBar = extrema[headIdx].bar;                             //--- Get the bar index of the head
int rsBar = extrema[rightShoulderIdx].bar;                      //--- Get the bar index of the right shoulder
int necklineStartBar = extrema[necklineStartIdx].bar;           //--- Get the bar index of the neckline start
int necklineEndBar = extrema[necklineEndIdx].bar;               //--- Get the bar index of the neckline end
int breakoutBar = 1;                                            //--- Set breakout bar index (previous bar)

int lsToHead = lsBar - headBar;                                 //--- Calculate number of bars from left shoulder to head
int headToRs = headBar - rsBar;                                 //--- Calculate number of bars from head to right shoulder
int rsToBreakout = rsBar - breakoutBar;                         //--- Calculate number of bars from right shoulder to breakout
int lsToNeckStart = lsBar - necklineStartBar;                   //--- Calculate number of bars from left shoulder to neckline start
double avgPatternRange = (lsToHead + headToRs) / 2.0;           //--- Calculate average bar range of the pattern for uniformity check

if (rsToBreakout > avgPatternRange * RightShoulderBreakoutMultiplier) { //--- Check if breakout distance exceeds allowed range
   Print("Pattern rejected: Right Shoulder to Breakout (", rsToBreakout, 
         ") exceeds ", RightShoulderBreakoutMultiplier, "x average range (", avgPatternRange, ")"); //--- Log rejection due to excessive breakout range
   return;                                                      //--- Exit function if pattern is invalid
}

double necklineStartPrice = extrema[necklineStartIdx].price;    //--- Get the price of the neckline start point
double necklineEndPrice = extrema[necklineEndIdx].price;        //--- Get the price of the neckline end point
datetime necklineStartTime = extrema[necklineStartIdx].time;    //--- Get the timestamp of the neckline start point
datetime necklineEndTime = extrema[necklineEndIdx].time;        //--- Get the timestamp of the neckline end point
int barDiff = necklineStartBar - necklineEndBar;                //--- Calculate bar difference between neckline points for slope
double slope = (necklineEndPrice - necklineStartPrice) / barDiff; //--- Calculate the slope of the neckline (price change per bar)
double breakoutNecklinePrice = necklineStartPrice + slope * (necklineStartBar - breakoutBar); //--- Calculate neckline price at breakout point

// Extend neckline backwards
int extendedBar = necklineStartBar;                             //--- Initialize extended bar index with neckline start
datetime extendedNecklineStartTime = necklineStartTime;         //--- Initialize extended neckline start time
double extendedNecklineStartPrice = necklineStartPrice;         //--- Initialize extended neckline start price
bool foundCrossing = false;                                     //--- Flag to track if neckline crosses a bar within range

for (int i = necklineStartBar + 1; i < Bars(_Symbol, _Period); i++) { //--- Loop through bars to extend neckline backwards
   double checkPrice = necklineStartPrice - slope * (i - necklineStartBar); //--- Calculate projected neckline price at bar i
   if (NecklineCrossesBar(checkPrice, i)) {                     //--- Check if neckline intersects the bar's high-low range
      int distance = i - necklineStartBar;                      //--- Calculate distance from neckline start to crossing bar
      if (distance <= avgPatternRange * RightShoulderBreakoutMultiplier) { //--- Check if crossing is within uniformity range
         extendedBar = i;                                       //--- Update extended bar index
         extendedNecklineStartTime = iTime(_Symbol, _Period, i); //--- Update extended neckline start time
         extendedNecklineStartPrice = checkPrice;              //--- Update extended neckline start price
         foundCrossing = true;                                  //--- Set flag to indicate crossing found
         Print("Neckline extended to first crossing bar within uniformity: Bar ", extendedBar); //--- Log successful extension
         break;                                                 //--- Exit loop after finding valid crossing
      } else {                                                  //--- If crossing exceeds uniformity range
         Print("Crossing bar ", i, " exceeds uniformity (", distance, " > ", avgPatternRange * RightShoulderBreakoutMultiplier, ")"); //--- Log rejection of crossing
         break;                                                 //--- Exit loop as crossing is too far
      }
   }
}

if (!foundCrossing) {                                           //--- If no valid crossing found within range
   int barsToExtend = 2 * lsToNeckStart;                        //--- Set fallback extension distance as twice LS to neckline start
   extendedBar = necklineStartBar + barsToExtend;               //--- Calculate extended bar index
   if (extendedBar >= Bars(_Symbol, _Period)) extendedBar = Bars(_Symbol, _Period) - 1; //--- Cap extended bar at total bars if exceeded
   extendedNecklineStartTime = iTime(_Symbol, _Period, extendedBar); //--- Update extended neckline start time
   extendedNecklineStartPrice = necklineStartPrice - slope * (extendedBar - necklineStartBar); //--- Update extended neckline start price
   Print("Neckline extended to fallback (2x LS to Neckline Start): Bar ", extendedBar, " (no crossing within uniformity)"); //--- Log fallback extension
}

Print("Standard Head and Shoulders Detected:");                 //--- Log detection of standard H&S pattern
Print("Left Shoulder: Bar ", lsBar, ", Time ", TimeToString(lsTime), ", Price ", DoubleToString(lsPrice, _Digits)); //--- Log left shoulder details
Print("Head: Bar ", headBar, ", Time ", TimeToString(extrema[headIdx].time), ", Price ", DoubleToString(extrema[headIdx].price, _Digits)); //--- Log head details
Print("Right Shoulder: Bar ", rsBar, ", Time ", TimeToString(extrema[rightShoulderIdx].time), ", Price ", DoubleToString(extrema[rightShoulderIdx].price, _Digits)); //--- Log right shoulder details
Print("Neckline Start: Bar ", necklineStartBar, ", Time ", TimeToString(necklineStartTime), ", Price ", DoubleToString(necklineStartPrice, _Digits)); //--- Log neckline start details
Print("Neckline End: Bar ", necklineEndBar, ", Time ", TimeToString(necklineEndTime), ", Price ", DoubleToString(necklineEndPrice, _Digits)); //--- Log neckline end details
Print("Close Price: ", DoubleToString(closePrice, _Digits));    //--- Log closing price at breakout
Print("Breakout Time: ", TimeToString(breakoutTime));           //--- Log breakout timestamp
Print("Neckline Price at Breakout: ", DoubleToString(breakoutNecklinePrice, _Digits)); //--- Log neckline price at breakout
Print("Extended Neckline Start: Bar ", extendedBar, ", Time ", TimeToString(extendedNecklineStartTime), ", Price ", DoubleToString(extendedNecklineStartPrice, _Digits)); //--- 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); //--- Log bar ranges for pattern analysis

在这里,我们通过验证检测到的标准形态并设置一笔卖出交易来增强形态检测功能:首先使用 “IsPatternTraded” 函数检查 “lsTime” 和 “lsPrice” 是否与 “tradedPatterns” 中的先前交易匹配,如果匹配则退出,以避免重复交易。然后,我们使用 iTime 函数将 “breakoutTime” 赋值为前一根 K 线的时间戳,并从 “extrema” 中获取 “lsBar”、“headBar”、“rsBar”、“necklineStartBar” 和 “necklineEndBar” 等 K 线索引,计算 “lsToHead”、“headToRs” 和 “rsToBreakout” 等范围;如果 “rsToBreakout” 超过 “avgPatternRange” 乘以 “RightShoulderBreakoutMultiplier”,我们拒绝该形态,并使用 Print 函数记录日志。

接下来,我们通过 “necklineStartPrice” 和 “necklineEndPrice” 在 “barDiff” 上的差值来确定颈线的 “slope”(斜率),计算 “breakoutNecklinePrice”,并通过一个循环向后延伸颈线,使用 “NecklineCrossesBar” 函数在 “avgPatternRange * RightShoulderBreakoutMultiplier” 范围内寻找交叉点,更新 “extendedBar”、“extendedNecklineStartTime”(通过 “iTime” 获取)和 “extendedNecklineStartPrice”;如果没有找到合适的交叉点,则回退使用 “2 * lsToNeckStart”,上限为 “Bars” 总数,并使用 Print、TimeToStringDoubleToString 函数记录所有详细信息——包括 K 线索引、价格和范围——以便完整存档。该自定义函数的代码片段如下。

//+------------------------------------------------------------------+
//| Check if neckline crosses a bar's high-low range                 |
//+------------------------------------------------------------------+
bool NecklineCrossesBar(double necklinePrice, int barIndex) {            //--- Function to check if neckline price intersects a bar's range
   double high = iHigh(_Symbol, _Period, barIndex);                      //--- Get the high price of the specified bar
   double low = iLow(_Symbol, _Period, barIndex);                        //--- Get the low price of the specified bar
   return (necklinePrice >= low && necklinePrice <= high);               //--- Return true if neckline price is within bar's high-low range
}

该函数检查 “necklinePrice”(颈线价格)是否在 “barIndex”(K 线索引)处与 K 线的价格范围相交,以确保颈线延伸的准确性。我们使用 iHigh 函数获取该 K 线的 “high”(最高价),使用 “iLow” 函数获取其 “low”(最低价),然后如果 “necklinePrice” 介于 “low” 和 “high” 之间,则返回 true,从而确认颈线穿过该 K 线的范围,以完成形态验证。如果形态得到验证,我们就在图表上将其图形化。我们将需要函数来绘制它并进行标注。

//+------------------------------------------------------------------+
//| Draw a trend line for visualization                              |
//+------------------------------------------------------------------+
void DrawTrendLine(string name, datetime timeStart, double priceStart, datetime timeEnd, double priceEnd, color lineColor, int width, int style) { //--- Function to draw a trend line on the chart
   if (ObjectCreate(0, name, OBJ_TREND, 0, timeStart, priceStart, timeEnd, priceEnd)) { //--- Create a trend line object if possible
      ObjectSetInteger(0, name, OBJPROP_COLOR, lineColor);               //--- Set the color of the trend line
      ObjectSetInteger(0, name, OBJPROP_STYLE, style);                   //--- Set the style (e.g., solid, dashed) of the trend line
      ObjectSetInteger(0, name, OBJPROP_WIDTH, width);                   //--- Set the width of the trend line
      ObjectSetInteger(0, name, OBJPROP_BACK, true);                     //--- Set the line to draw behind chart elements
      ChartRedraw();                                                     //--- Redraw the chart to display the new line
   } else {                                                              //--- If line creation fails
      Print("Failed to create line: ", name, ". Error: ", GetLastError()); //--- Log the error with the object name and error code
   }
}

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

//+------------------------------------------------------------------+
//| Draw text label for visualization                                |
//+------------------------------------------------------------------+
void DrawText(string name, datetime time, double price, string text, color textColor, bool above, double angle = 0) { //--- Function to draw a text label on the chart
   int chartscale = (int)ChartGetInteger(0, CHART_SCALE);                //--- Get the current chart zoom level
   int dynamicFontSize = 5 + int(chartscale * 1.5);                      //--- Calculate font size based on zoom level for visibility
   double priceOffset = (above ? 10 : -10) * _Point;                     //--- Set price offset above or below the point for readability
   if (ObjectCreate(0, name, OBJ_TEXT, 0, time, price + priceOffset)) {  //--- Create a text object if possible
      ObjectSetString(0, name, OBJPROP_TEXT, text);                      //--- Set the text content of the label
      ObjectSetInteger(0, name, OBJPROP_COLOR, textColor);               //--- Set the color of the text
      ObjectSetInteger(0, name, OBJPROP_FONTSIZE, dynamicFontSize);      //--- Set the font size based on chart scale
      ObjectSetInteger(0, name, OBJPROP_ANCHOR, ANCHOR_CENTER);          //--- Center the text at the specified point
      ObjectSetDouble(0, name, OBJPROP_ANGLE, angle);                    //--- Set the rotation angle of the text in degrees
      ObjectSetInteger(0, name, OBJPROP_BACK, false);                    //--- Set the text to draw in front of chart elements
      ChartRedraw();                                                     //--- Redraw the chart to display the new text
      Print("Text created: ", name, ", Angle: ", DoubleToString(angle, 2)); //--- Log successful creation of the text with its angle
   } else {                                                              //--- If text creation fails
      Print("Failed to create text: ", name, ". Error: ", GetLastError()); //--- Log the error with the object name and error code
   }
}

在这里,我们通过可视化工具来丰富程序,以便在图表上突出显示形态。首先,我们使用 “DrawTrendLine” 函数,该函数利用 ObjectCreate 函数绘制一条从 “timeStart” 和 “priceStart” 到 “timeEnd” 和 “priceEnd” 的线条,通过 ObjectSetInteger 设置 “lineColor”(线条颜色)、“style”(样式)和 “width”(宽度)等属性,使用 OBJPROP_BACK 将其绘制在 K 线后方,并使用 ChartRedraw 函数刷新显示,如果需要,使用 “Print” 和 GetLastError 记录失败信息。

接着,我们实现 “DrawTriangle” 函数来为形态的结构添加阴影,该函数调用 “ObjectCreate” 函数并传入三个点(“time1”、“price1” 等),使用 ObjectSetInteger 应用 “fillColor”(填充颜色)和实线边框,通过 OBJPROP_FILL 对其进行填充,将其置于图表后方,并使用 ChartRedraw 更新视图,同样在创建失败时使用 “Print” 记录错误。

最后,我们添加 “DrawText” 函数来标注关键点,该函数使用 ChartGetInteger 函数根据 “chartscale”(图表缩放比例)调整 “dynamicFontSize”(动态字体大小),通过 “ObjectCreate” 在 “time” 和 “price” 加上偏移量的位置放置文本,使用 “ObjectSetString” 自定义 “text”(文本),使用 “ObjectSetInteger” 设置 “textColor”(文本颜色)和 “FONTSIZE”(字体大小),使用 “ObjectSetDouble” 设置 “angle”(角度),使用 ChartRedraw 将其绘制在前方,并使用 “Print” 和 “DoubleToString” 确认创建或记录错误。现在我们可以调用这些函数来添加可视化功能,我们首先要做的是按如下方式添加线条。

string prefix = "HS_" + TimeToString(extrema[headIdx].time, TIME_MINUTES); //--- Create unique prefix for chart objects based on head time
// Lines
DrawTrendLine(prefix + "_LeftToNeckStart", lsTime, lsPrice, necklineStartTime, necklineStartPrice, clrRed, 3, STYLE_SOLID); //--- Draw line from left shoulder to neckline start
DrawTrendLine(prefix + "_NeckStartToHead", necklineStartTime, necklineStartPrice, extrema[headIdx].time, extrema[headIdx].price, clrRed, 3, STYLE_SOLID); //--- Draw line from neckline start to head
DrawTrendLine(prefix + "_HeadToNeckEnd", extrema[headIdx].time, extrema[headIdx].price, necklineEndTime, necklineEndPrice, clrRed, 3, STYLE_SOLID); //--- Draw line from head to neckline end
DrawTrendLine(prefix + "_NeckEndToRight", necklineEndTime, necklineEndPrice, extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, clrRed, 3, STYLE_SOLID); //--- Draw line from neckline end to right shoulder
DrawTrendLine(prefix + "_Neckline", extendedNecklineStartTime, extendedNecklineStartPrice, breakoutTime, breakoutNecklinePrice, clrBlue, 2, STYLE_SOLID); //--- Draw neckline from extended start to breakout
DrawTrendLine(prefix + "_RightToBreakout", extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, breakoutTime, breakoutNecklinePrice, clrRed, 3, STYLE_SOLID); //--- Draw line from right shoulder to breakout
DrawTrendLine(prefix + "_ExtendedToLeftShoulder", extendedNecklineStartTime, extendedNecklineStartPrice, lsTime, lsPrice, clrRed, 3, STYLE_SOLID); //--- Draw line from extended neckline to left shoulder

在这里,我们通过创建一个基于头部时间戳的唯一前缀(使用 TimeToString 函数),并使用 “DrawTrendLine” 函数绘制趋势线,来图形化地映射标准形态:我们将左肩连接到颈线起点、颈线起点连接到头部、头部连接到颈线终点、颈线终点连接到右肩,这些线条均为红色,宽度为 3;而从其延伸起点到突破点的颈线则使用蓝色,宽度为 2;此外,还有额外的红色实线将右肩连接到突破点,并将延伸的颈线连接回左肩,所有线条均为实线样式,以便在图表上显示该形态。编译完成后,我们得到了以下结果。

带线条的图表

要添加三角形,我们使用 “DrawTriangle” 函数。从技术上讲,我们在肩部和头部之间构建它。

// Triangles
DrawTriangle(prefix + "_LeftShoulderTriangle", lsTime, lsPrice, necklineStartTime, necklineStartPrice, extendedNecklineStartTime, extendedNecklineStartPrice, clrLightCoral); //--- Draw triangle for left shoulder area
DrawTriangle(prefix + "_HeadTriangle", extrema[headIdx].time, extrema[headIdx].price, necklineStartTime, necklineStartPrice, necklineEndTime, necklineEndPrice, clrLightCoral); //--- Draw triangle for head area
DrawTriangle(prefix + "_RightShoulderTriangle", extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, necklineEndTime, necklineEndPrice, breakoutTime, breakoutNecklinePrice, clrLightCoral); //--- Draw triangle for right shoulder area

在这里,我们通过使用 “DrawTriangle” 函数来增强视觉效果,用浅珊瑚色为关键区域添加阴影:我们为左肩构建一个三角形,其顶点为左肩点、颈线起点和延伸颈线起点;为头部构建另一个三角形,其顶点为头部点、颈线起点和颈线终点;再为右肩构建第三个三角形,其顶点为右肩点、颈线终点和突破点,从而在图表上突出显示该形态的结构。编译完成后,我们得到了以下结果。

带三角形的图表

最后,我们需要为形态添加标签,使其在视觉上更具吸引力且能一目了然。

// Text Labels
DrawText(prefix + "_LS_Label", lsTime, lsPrice, "LS", clrRed, true); //--- Draw "LS" label above left shoulder
DrawText(prefix + "_Head_Label", extrema[headIdx].time, extrema[headIdx].price, "HEAD", clrRed, true); //--- Draw "HEAD" label above head
DrawText(prefix + "_RS_Label", extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, "RS", clrRed, true); //--- Draw "RS" label above right shoulder
datetime necklineMidTime = extendedNecklineStartTime + (breakoutTime - extendedNecklineStartTime) / 2; //--- Calculate midpoint time of the neckline
double necklineMidPrice = extendedNecklineStartPrice + slope * (iBarShift(_Symbol, _Period, extendedNecklineStartTime) - iBarShift(_Symbol, _Period, necklineMidTime)); //--- Calculate midpoint price of the neckline
// Calculate angle in pixel space
int x1 = ShiftToX(iBarShift(_Symbol, _Period, extendedNecklineStartTime)); //--- Convert extended neckline start to x-pixel coordinate
int y1 = PriceToY(extendedNecklineStartPrice);                          //--- Convert extended neckline start price to y-pixel coordinate
int x2 = ShiftToX(iBarShift(_Symbol, _Period, breakoutTime));           //--- Convert breakout time to x-pixel coordinate
int y2 = PriceToY(breakoutNecklinePrice);                               //--- Convert breakout price to y-pixel coordinate
double pixelSlope = (y2 - y1) / (double)(x2 - x1);                     //--- Calculate slope in pixel space (rise over run)
double necklineAngle = -atan(pixelSlope) * 180 / M_PI;                  //--- Calculate neckline angle in degrees, negated for visual alignment
Print("Pixel X1: ", x1, ", Y1: ", y1, ", X2: ", x2, ", Y2: ", y2, ", Pixel Slope: ", DoubleToString(pixelSlope, 4), ", Neckline Angle: ", DoubleToString(necklineAngle, 2)); //--- Log pixel coordinates and angle
DrawText(prefix + "_Neckline_Label", necklineMidTime, necklineMidPrice, "NECKLINE", clrBlue, false, necklineAngle); //--- Draw "NECKLINE" label at midpoint with calculated angle

最后,我们通过使用 “DrawText” 函数来标注该形态,在左肩、头部和右肩点各自的时间和价格位置上方,分别放置红色的 “LS”、“HEAD” 和 “RS” 标签,以增强图表的可读性。然后,我们通过平均 “extendedNecklineStartTime” 和 “breakoutTime” 来计算颈线的中点时间 “necklineMidTime”,并通过 iBarShift 函数使用 “slope”(斜率)和 K 线差值调整 “extendedNecklineStartPrice” 来获得 “necklineMidPrice”(颈线中点价格);为了对齐标签,我们使用 “ShiftToX” 函数将时间转换为 x 像素,使用 “PriceToY” 函数将价格转换为 y 像素(在颈线起点和突破点处),计算出一个 “pixelSlope”(像素斜率),并使用 atan 函数和 “M_PI” 推导出以度为单位的 “necklineAngle”(颈线角度),同时使用 “Print” 函数和 DoubleToString 函数记录这些值以供验证。

接着,我们使用 “DrawText” 函数在中点绘制一个蓝色的 “NECKLINE” 标签,将其放置在下方并根据 “necklineAngle” 进行旋转,以确保标注与颈线的倾斜度保持一致。结果如下。

最终形态结果

从图像中,我们可以看到该形态已被完全图形化。现在我们需要检测其突破,基本上就是那条延伸线,开一个卖出仓位,并将延伸范围修改到突破 K 线。轻而易举。我们通过以下代码来实现这一点。

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

一旦形态得到确认,我们通过以下方式执行一笔卖出交易:将 “entryPrice” 设为 0 以市价单成交,使用 “BufferPoints” 在右肩价格上方计算 “sl”(止损价),将 “patternHeight”(形态高度)确定为头部与颈线价格之间的差值,并在 “closePrice”(收盘价)下方减去 “patternHeight” 来设置 “tp”(止盈价)。

在使用 “obj_Trade” 对象的 “Sell” 函数以 “LotSize”(手数)、“sl”、“tp” 和注释来开仓之前,我们会验证交易方向——确保 “sl” 在 “closePrice” 之上,“tp” 在其之下;如果交易成功,我们调用 “AddTradedPattern” 函数并传入 “lsTime” 和 “lsPrice” 来记录该形态,同时使用 Print 函数和 DoubleToString 函数来记录 “sl” 和 “tp” 的详细信息。用于将该形态标记为已交易的自定义函数代码片段如下。

//+------------------------------------------------------------------+
//| Add pattern to traded list with size management                  |
//+------------------------------------------------------------------+
void AddTradedPattern(datetime lsTime, double lsPrice) {                 //--- Function to add a new traded pattern to the list
   int size = ArraySize(tradedPatterns);                                 //--- Get the current size of the tradedPatterns array
   if (size >= MaxTradedPatterns) {                                      //--- Check if array size exceeds maximum allowed
      for (int i = 0; i < size - 1; i++) {                               //--- Shift all elements left to remove the oldest
         tradedPatterns[i] = tradedPatterns[i + 1];                      //--- Copy next element to current position
      }
      ArrayResize(tradedPatterns, size - 1);                            //--- Reduce array size by 1
      size--;                                                           //--- Decrement size variable
      Print("Removed oldest traded pattern to maintain max size of ", MaxTradedPatterns); //--- Log removal of oldest pattern
   }
   ArrayResize(tradedPatterns, size + 1);                                //--- Increase array size to add new pattern
   tradedPatterns[size].leftShoulderTime = lsTime;                       //--- Store the left shoulder time of the new pattern
   tradedPatterns[size].leftShoulderPrice = lsPrice;                     //--- Store the left shoulder price of the new pattern
   Print("Added traded pattern: Left Shoulder Time ", TimeToString(lsTime), ", Price ", DoubleToString(lsPrice, _Digits)); //--- Log addition of new pattern
}

我们定义 “AddTradedPattern” 函数来跟踪已交易的形态设置。该函数使用 “lsTime” 和 “lsPrice” 来记录左肩的详细信息,因为左肩不会重绘。我们使用 ArraySize 函数检查 “tradedPatterns” 数组的大小。如果它达到了 “MaxTradedPatterns”(最大已交易形态数),我们就将元素向左移动以丢弃最旧的条目。我们使用 “ArrayResize” 函数调整 “tradedPatterns” 的大小以将其缩小。我们记录此操作,然后使用 ArrayResize 函数扩展 “tradedPatterns” 以便添加新条目。我们将 “leftShoulderTime” 设置为 “lsTime”,将 “leftShoulderPrice” 设置为 “lsPrice”。我们使用 Print 函数、TimeToString 函数和 DoubleToString 函数记录此次添加。编译完成后,我们得到了以下结果。

已交易的设置

从图像中,我们可以看到我们不仅可视化了该设置,还相应地进行了交易。倒头肩形态的识别、图形化和交易操作采用相同的逻辑,只是方式相反。以下是代码逻辑。

// Inverse Head and Shoulders (Buy)
if (DetectInverseHeadAndShoulders(extrema, leftShoulderIdx, headIdx, rightShoulderIdx, necklineStartIdx, necklineEndIdx)) { //--- Check for inverse H&S pattern
   double closePrice = iClose(_Symbol, _Period, 1);                   //--- Get the closing price of the previous bar
   double necklinePrice = extrema[necklineEndIdx].price;              //--- Get the price of the neckline end point

   if (closePrice > necklinePrice) {                                  //--- Check if price has broken above the neckline (buy signal)
      datetime lsTime = extrema[leftShoulderIdx].time;                //--- Get the timestamp of the left shoulder
      double lsPrice = extrema[leftShoulderIdx].price;                //--- Get the price of the left shoulder

      if (IsPatternTraded(lsTime, lsPrice)) return;                   //--- Exit if this pattern has already been traded

      datetime breakoutTime = iTime(_Symbol, _Period, 1);             //--- Get the timestamp of the breakout bar (previous bar)
      int lsBar = extrema[leftShoulderIdx].bar;                       //--- Get the bar index of the left shoulder
      int headBar = extrema[headIdx].bar;                             //--- Get the bar index of the head
      int rsBar = extrema[rightShoulderIdx].bar;                      //--- Get the bar index of the right shoulder
      int necklineStartBar = extrema[necklineStartIdx].bar;           //--- Get the bar index of the neckline start
      int necklineEndBar = extrema[necklineEndIdx].bar;               //--- Get the bar index of the neckline end
      int breakoutBar = 1;                                            //--- Set breakout bar index (previous bar)

      int lsToHead = lsBar - headBar;                                 //--- Calculate number of bars from left shoulder to head
      int headToRs = headBar - rsBar;                                 //--- Calculate number of bars from head to right shoulder
      int rsToBreakout = rsBar - breakoutBar;                         //--- Calculate number of bars from right shoulder to breakout
      int lsToNeckStart = lsBar - necklineStartBar;                   //--- Calculate number of bars from left shoulder to neckline start
      double avgPatternRange = (lsToHead + headToRs) / 2.0;           //--- Calculate average bar range of the pattern for uniformity check

      if (rsToBreakout > avgPatternRange * RightShoulderBreakoutMultiplier) { //--- Check if breakout distance exceeds allowed range
         Print("Pattern rejected: Right Shoulder to Breakout (", rsToBreakout, 
               ") exceeds ", RightShoulderBreakoutMultiplier, "x average range (", avgPatternRange, ")"); //--- Log rejection due to excessive breakout range
         return;                                                      //--- Exit function if pattern is invalid
      }

      double necklineStartPrice = extrema[necklineStartIdx].price;    //--- Get the price of the neckline start point
      double necklineEndPrice = extrema[necklineEndIdx].price;        //--- Get the price of the neckline end point
      datetime necklineStartTime = extrema[necklineStartIdx].time;    //--- Get the timestamp of the neckline start point
      datetime necklineEndTime = extrema[necklineEndIdx].time;        //--- Get the timestamp of the neckline end point
      int barDiff = necklineStartBar - necklineEndBar;                //--- Calculate bar difference between neckline points for slope
      double slope = (necklineEndPrice - necklineStartPrice) / barDiff; //--- Calculate the slope of the neckline (price change per bar)
      double breakoutNecklinePrice = necklineStartPrice + slope * (necklineStartBar - breakoutBar); //--- Calculate neckline price at breakout point

      // Extend neckline backwards
      int extendedBar = necklineStartBar;                             //--- Initialize extended bar index with neckline start
      datetime extendedNecklineStartTime = necklineStartTime;         //--- Initialize extended neckline start time
      double extendedNecklineStartPrice = necklineStartPrice;         //--- Initialize extended neckline start price
      bool foundCrossing = false;                                     //--- Flag to track if neckline crosses a bar within range

      for (int i = necklineStartBar + 1; i < Bars(_Symbol, _Period); i++) { //--- Loop through bars to extend neckline backwards
         double checkPrice = necklineStartPrice - slope * (i - necklineStartBar); //--- Calculate projected neckline price at bar i
         if (NecklineCrossesBar(checkPrice, i)) {                     //--- Check if neckline intersects the bar's high-low range
            int distance = i - necklineStartBar;                      //--- Calculate distance from neckline start to crossing bar
            if (distance <= avgPatternRange * RightShoulderBreakoutMultiplier) { //--- Check if crossing is within uniformity range
               extendedBar = i;                                       //--- Update extended bar index
               extendedNecklineStartTime = iTime(_Symbol, _Period, i); //--- Update extended neckline start time
               extendedNecklineStartPrice = checkPrice;              //--- Update extended neckline start price
               foundCrossing = true;                                  //--- Set flag to indicate crossing found
               Print("Neckline extended to first crossing bar within uniformity: Bar ", extendedBar); //--- Log successful extension
               break;                                                 //--- Exit loop after finding valid crossing
            } else {                                                  //--- If crossing exceeds uniformity range
               Print("Crossing bar ", i, " exceeds uniformity (", distance, " > ", avgPatternRange * RightShoulderBreakoutMultiplier, ")"); //--- Log rejection of crossing
               break;                                                 //--- Exit loop as crossing is too far
            }
         }
      }

      if (!foundCrossing) {                                           //--- If no valid crossing found within range
         int barsToExtend = 2 * lsToNeckStart;                        //--- Set fallback extension distance as twice LS to neckline start
         extendedBar = necklineStartBar + barsToExtend;               //--- Calculate extended bar index
         if (extendedBar >= Bars(_Symbol, _Period)) extendedBar = Bars(_Symbol, _Period) - 1; //--- Cap extended bar at total bars if exceeded
         extendedNecklineStartTime = iTime(_Symbol, _Period, extendedBar); //--- Update extended neckline start time
         extendedNecklineStartPrice = necklineStartPrice - slope * (extendedBar - necklineStartBar); //--- Update extended neckline start price
         Print("Neckline extended to fallback (2x LS to Neckline Start): Bar ", extendedBar, " (no crossing within uniformity)"); //--- Log fallback extension
      }

      Print("Inverse Head and Shoulders Detected:");                  //--- Log detection of inverse H&S pattern
      Print("Left Shoulder: Bar ", lsBar, ", Time ", TimeToString(lsTime), ", Price ", DoubleToString(lsPrice, _Digits)); //--- Log left shoulder details
      Print("Head: Bar ", headBar, ", Time ", TimeToString(extrema[headIdx].time), ", Price ", DoubleToString(extrema[headIdx].price, _Digits)); //--- Log head details
      Print("Right Shoulder: Bar ", rsBar, ", Time ", TimeToString(extrema[rightShoulderIdx].time), ", Price ", DoubleToString(extrema[rightShoulderIdx].price, _Digits)); //--- Log right shoulder details
      Print("Neckline Start: Bar ", necklineStartBar, ", Time ", TimeToString(necklineStartTime), ", Price ", DoubleToString(necklineStartPrice, _Digits)); //--- Log neckline start details
      Print("Neckline End: Bar ", necklineEndBar, ", Time ", TimeToString(necklineEndTime), ", Price ", DoubleToString(necklineEndPrice, _Digits)); //--- Log neckline end details
      Print("Close Price: ", DoubleToString(closePrice, _Digits));    //--- Log closing price at breakout
      Print("Breakout Time: ", TimeToString(breakoutTime));           //--- Log breakout timestamp
      Print("Neckline Price at Breakout: ", DoubleToString(breakoutNecklinePrice, _Digits)); //--- Log neckline price at breakout
      Print("Extended Neckline Start: Bar ", extendedBar, ", Time ", TimeToString(extendedNecklineStartTime), ", Price ", DoubleToString(extendedNecklineStartPrice, _Digits)); //--- 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); //--- Log bar ranges for pattern analysis

      string prefix = "IHS_" + TimeToString(extrema[headIdx].time, TIME_MINUTES); //--- Create unique prefix for chart objects based on head time
      // Lines
      DrawTrendLine(prefix + "_LeftToNeckStart", lsTime, lsPrice, necklineStartTime, necklineStartPrice, clrGreen, 2, STYLE_SOLID); //--- Draw line from left shoulder to neckline start
      DrawTrendLine(prefix + "_NeckStartToHead", necklineStartTime, necklineStartPrice, extrema[headIdx].time, extrema[headIdx].price, clrGreen, 2, STYLE_SOLID); //--- Draw line from neckline start to head
      DrawTrendLine(prefix + "_HeadToNeckEnd", extrema[headIdx].time, extrema[headIdx].price, necklineEndTime, necklineEndPrice, clrGreen, 2, STYLE_SOLID); //--- Draw line from head to neckline end
      DrawTrendLine(prefix + "_NeckEndToRight", necklineEndTime, necklineEndPrice, extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, clrGreen, 2, STYLE_SOLID); //--- Draw line from neckline end to right shoulder
      DrawTrendLine(prefix + "_Neckline", extendedNecklineStartTime, extendedNecklineStartPrice, breakoutTime, breakoutNecklinePrice, clrBlue, 2, STYLE_SOLID); //--- Draw neckline from extended start to breakout
      DrawTrendLine(prefix + "_RightToBreakout", extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, breakoutTime, breakoutNecklinePrice, clrGreen, 2, STYLE_SOLID); //--- Draw line from right shoulder to breakout
      DrawTrendLine(prefix + "_ExtendedToLeftShoulder", extendedNecklineStartTime, extendedNecklineStartPrice, lsTime, lsPrice, clrGreen, 2, STYLE_SOLID); //--- Draw line from extended neckline to left shoulder
      // Triangles
      DrawTriangle(prefix + "_LeftShoulderTriangle", lsTime, lsPrice, necklineStartTime, necklineStartPrice, extendedNecklineStartTime, extendedNecklineStartPrice, clrLightGreen); //--- Draw triangle for left shoulder area
      DrawTriangle(prefix + "_HeadTriangle", extrema[headIdx].time, extrema[headIdx].price, necklineStartTime, necklineStartPrice, necklineEndTime, necklineEndPrice, clrLightGreen); //--- Draw triangle for head area
      DrawTriangle(prefix + "_RightShoulderTriangle", extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, necklineEndTime, necklineEndPrice, breakoutTime, breakoutNecklinePrice, clrLightGreen); //--- Draw triangle for right shoulder area
      // Text Labels
      DrawText(prefix + "_LS_Label", lsTime, lsPrice, "LS", clrGreen, false); //--- Draw "LS" label below left shoulder
      DrawText(prefix + "_Head_Label", extrema[headIdx].time, extrema[headIdx].price, "HEAD", clrGreen, false); //--- Draw "HEAD" label below head
      DrawText(prefix + "_RS_Label", extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, "RS", clrGreen, false); //--- Draw "RS" label below right shoulder
      datetime necklineMidTime = extendedNecklineStartTime + (breakoutTime - extendedNecklineStartTime) / 2; //--- Calculate midpoint time of the neckline
      double necklineMidPrice = extendedNecklineStartPrice + slope * (iBarShift(_Symbol, _Period, extendedNecklineStartTime) - iBarShift(_Symbol, _Period, necklineMidTime)); //--- Calculate midpoint price of the neckline
      // Calculate angle in pixel space
      int x1 = ShiftToX(iBarShift(_Symbol, _Period, extendedNecklineStartTime)); //--- Convert extended neckline start to x-pixel coordinate
      int y1 = PriceToY(extendedNecklineStartPrice);                          //--- Convert extended neckline start price to y-pixel coordinate
      int x2 = ShiftToX(iBarShift(_Symbol, _Period, breakoutTime));           //--- Convert breakout time to x-pixel coordinate
      int y2 = PriceToY(breakoutNecklinePrice);                               //--- Convert breakout price to y-pixel coordinate
      double pixelSlope = (y2 - y1) / (double)(x2 - x1);                     //--- Calculate slope in pixel space (rise over run)
      double necklineAngle = -atan(pixelSlope) * 180 / M_PI;                  //--- Calculate neckline angle in degrees, negated for visual alignment
      Print("Pixel X1: ", x1, ", Y1: ", y1, ", X2: ", x2, ", Y2: ", y2, ", Pixel Slope: ", DoubleToString(pixelSlope, 4), ", Neckline Angle: ", DoubleToString(necklineAngle, 2)); //--- Log pixel coordinates and angle
      DrawText(prefix + "_Neckline_Label", necklineMidTime, necklineMidPrice, "NECKLINE", clrBlue, true, necklineAngle); //--- Draw "NECKLINE" label at midpoint with calculated angle

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

现在剩下的是通过应用移动止损逻辑来管理已开仓位,以实现利润最大化。我们创建一个函数来处理移动止损逻辑,如下所示。

//+------------------------------------------------------------------+
//| Apply trailing stop with minimum profit threshold                |
//+------------------------------------------------------------------+
void ApplyTrailingStop(int minTrailPoints, int trailingPoints, CTrade &trade_object, ulong magicNo = 0) { //--- Function to apply trailing stop to open positions
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);                           //--- Get current bid price
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);                           //--- Get current ask price

   for (int i = PositionsTotal() - 1; i >= 0; i--) {                             //--- Loop through all open positions from last to first
      ulong ticket = PositionGetTicket(i);                                       //--- Retrieve position ticket number
      if (ticket > 0 && PositionSelectByTicket(ticket)) {                        //--- Check if ticket is valid and select the position
         if (PositionGetString(POSITION_SYMBOL) == _Symbol &&                    //--- Verify position is for the current symbol
             (magicNo == 0 || PositionGetInteger(POSITION_MAGIC) == magicNo)) {  //--- Check if magic number matches or no magic filter applied
            double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);           //--- Get position opening price
            double currentSL = PositionGetDouble(POSITION_SL);                   //--- Get current stop-loss price
            double currentProfit = PositionGetDouble(POSITION_PROFIT) / (LotSize * SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE)); //--- Calculate profit in points
            
            if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) {         //--- Check if position is a Buy
               double profitPoints = (bid - openPrice) / _Point;                 //--- Calculate profit in points for Buy position
               if (profitPoints >= minTrailPoints + trailingPoints) {            //--- Check if profit exceeds minimum threshold for trailing
                  double newSL = NormalizeDouble(bid - trailingPoints * _Point, _Digits); //--- Calculate new stop-loss price
                  if (newSL > openPrice && (newSL > currentSL || currentSL == 0)) { //--- Ensure new SL is above open price and better than current SL
                     if (trade_object.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP))) { //--- Attempt to modify position with new SL
                        Print("Trailing Stop Updated: Ticket ", ticket, ", New SL: ", DoubleToString(newSL, _Digits)); //--- Log successful SL update
                     }
                  }
               }
            } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Check if position is a Sell
               double profitPoints = (openPrice - ask) / _Point;                 //--- Calculate profit in points for Sell position
               if (profitPoints >= minTrailPoints + trailingPoints) {            //--- Check if profit exceeds minimum threshold for trailing
                  double newSL = NormalizeDouble(ask + trailingPoints * _Point, _Digits); //--- Calculate new stop-loss price
                  if (newSL < openPrice && (newSL < currentSL || currentSL == 0)) { //--- Ensure new SL is below open price and better than current SL
                     if (trade_object.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP))) { //--- Attempt to modify position with new SL
                        Print("Trailing Stop Updated: Ticket ", ticket, ", New SL: ", DoubleToString(newSL, _Digits)); //--- Log successful SL update
                     }
                  }
               }
            }
         }
      }
   }
}

在这里,我们通过 “ApplyTrailingStop” 函数添加了移动止损功能。该函数使用 “minTrailPoints”(最小移动点数)和 “trailingPoints”(移动点数)来调整已开仓位。我们使用 SymbolInfoDouble 函数获取 “bid”(买入价)和 “ask”(卖出价)。我们使用 PositionsTotal 函数遍历所有持仓。对于每个持仓,我们使用 PositionGetTicket 函数获取其 “ticket”(订单号),并使用 PositionSelectByTicket 函数选中它。我们使用 “PositionGetString” 和 “PositionGetInteger” 函数验证交易品种和 “magicNo”(魔术数字)。我们使用 PositionGetDouble 函数获取 “openPrice”(开仓价)、“currentSL”(当前止损价)和 “currentProfit”(当前盈亏)。

对于买单(Buy),我们使用 “bid” 计算利润,并检查其是否满足 “minTrailPoints” 加上 “trailingPoints” 的条件。如果满足,我们使用 NormalizeDouble 函数设置新的 “newSL”(新止损价),并通过 “trade_object” 对象的 “PositionModify” 方法来更新它。对于卖单(Sell),我们则使用 “ask” 并相应地在下方调整 “newSL”。成功的价格修改将被记录下来。然后,我们可以在 OnTick 事件处理程序中调用此函数。

// Apply trailing stop if enabled and positions exist
if (UseTrailingStop && PositionsTotal() > 0) {                        //--- Check if trailing stop is enabled and there are open positions
   ApplyTrailingStop(MinTrailPoints, TrailingPoints, obj_Trade, MagicNumber); //--- Apply trailing stop to positions with specified parameters
}

使用输入参数调用该函数,就是我们启用移动止损所需要做的全部工作。现在剩下的是,在程序不再使用时释放数组,并删除我们绘制的图形对象。我们在 OnDeinit 事件处理程序中处理这些。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {                                        //--- Expert Advisor deinitialization function
   ArrayFree(tradedPatterns);                                            //--- Free memory used by tradedPatterns array
   ObjectsDeleteAll(0, "HS_");                                           //--- Delete all chart objects with "HS_" prefix (standard H&S)
   ObjectsDeleteAll(0, "IHS_");                                          //--- Delete all chart objects with "IHS_" prefix (inverse H&S)
   ChartRedraw();                                                        //--- Redraw the chart to remove deleted objects
}

在 OnDeinit 事件处理程序中(该程序在智能交易系统关闭时运行),我们会清理程序及其所附加的图表。我们使用 ArrayFree 函数释放 “tradedPatterns” 数组占用的内存。然后,我们移除所有图表对象。ObjectsDeleteAll 函数会清除所有带有 “HS_” 前缀的标准形态对象。它也会清除所有带有 “IHS_” 前缀的反向形态对象。最后,我们刷新图表。ChartRedraw 函数会更新显示,以在完全关闭前反映这些更改。编译完成后,我们得到了以下结果。

应用移动止损后的最终结果

从图像中,我们可以看到对已交易应用了移动止损,从而实现了我们的目标。剩下的事情就是对该程序进行回测,这将在下一节中处理。


回测

经过彻底的回测后,我们得到以下结果。

回测结果图形:

图形

回测报告:

报告


结论

总之,我们已经成功地在 MQL5 中构建了一个头肩形态交易算法。它具备精确的形态检测、详细的可视化以及针对经典反转信号的自动化交易执行功能。通过使用验证规则、颈线绘制和移动止损,我们的智能交易系统能够有效地适应市场变化。您可以将本文中制作的图示作为垫脚石,通过参数调整或高级风险控制等额外步骤来增强它。另外,请注意,这是一种较为罕见的形态设置。

免责声明:本文仅用于教学目的。交易涉及重大的财务风险,市场状况可能不可预测。在实盘部署前,充分的回测和风险管理至关重要。

有了这个基础,您可以精进自己的交易技巧并改进此算法。不断测试和优化,以取得成功。祝您好运!

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/17618

附加的文件 |
MQL5中的高级内存管理与优化技术 MQL5中的高级内存管理与优化技术
探索在MQL5交易系统中优化内存使用的实用技巧。学习构建高效、稳定且运行速度快的智能交易系统(EA)和指标。我们将深入探究MQL5中内存的实际运作方式、致使系统运行变慢或出现故障的常见陷阱,以及——最为关键的是——如何解决这些问题。
从基础到中级:模板和类型名称(二) 从基础到中级:模板和类型名称(二)
本文解释了如何处理您可能遇到的最困难的编程情况之一:在同一个函数或过程模板中使用不同的类型。尽管我们大部分时间只关注函数,但这里介绍的所有内容都是有用的,并且可以应用于过程。
风险管理(第一部分):建立风险管理类的基础知识 风险管理(第一部分):建立风险管理类的基础知识
在本文中,我们将介绍交易风险管理的基础知识,并学习如何创建第一个函数来计算交易的适当手数以及止损。此外,我们将详细介绍这些功能的工作原理,解释每个步骤。我们的目标是清楚地了解如何在自动交易中应用这些概念。最后,我们将通过创建一个包含文件的简单脚本来将所有内容付诸实践。
数据科学和机器学习(第 33 部分):MQL5 中的 Pandas 数据帧,为机器学习收集数据更加容易 数据科学和机器学习(第 33 部分):MQL5 中的 Pandas 数据帧,为机器学习收集数据更加容易
当与机器学习模型共事时,确保用于训练、验证和测试的数据一致性必不可少。在本文中,我们将创建我们自己的 MQL5 版本 Pandas 函数库,确保使用统一方式来处理机器学习数据;这样做是为确保在 MQL5 内部和外部应用相同的数据,其中大部分发生在训练阶段。