English Русский Español Deutsch 日本語
preview
创建一个基于布林带PIRANHA策略的MQL5 EA

创建一个基于布林带PIRANHA策略的MQL5 EA

MetaTrader 5交易系统 |
222 2
Allan Munene Mutiiria
Allan Munene Mutiiria

引言

在本文中,我们将探讨如何基于 PIRANHA 策略,在MQL5中创建一款EA,并重点整合布林带。随着交易者对高效自动化方案的渴求,PIRANHA 策略应运而生——它以系统化方式捕捉市场波动,因而深受外汇玩家的青睐。

首先,我们将概述 PIRANHA 策略的核心理念,为自动化交易打下坚实基础。接着,深入剖析布林带这一经典指标:它通过度量市场波动,帮助我们锁定潜在进出场点。

随后,我们将引导您完成在 MQL5 中的编码过程,重点介绍推动该策略的核心函数和逻辑。此外,还会讨论 EA 的性能测试、参数优化,以及实盘部署的最佳实践。本文将涵盖以下主题:

  1. PIRANHA 策略概览
  2. 布林带深度解析
  3. 策略的具体实现
  4. 在MQL5中的实现
  5. 测试
  6. 结论

读完本文,您将具备开发 MQL5 EA 的完整知识体系,能够高效运用 PIRANHA 策略与布林带,全面提升交易表现。让我们开始吧。


PIRANHA 策略概览

PIRANHA 策略是一套动态交易系统,专为捕捉外汇市场波动而设。它以“快、准、狠”著称,正如其名称来源——水中迅捷的食人鲳,出手即中。核心技能:该策略以波动率为锚,通过高度相对化的方式,帮助交易者精准定位市场进出场点。

在本策略中,布林带为核心组件。作为衡量波动的常用指标,布林带可直观呈现市场的起伏。我们选用 12 周期布林带,经验证其结构能在长期内有效揭示价格行为。标准差倍数设为 2,在捕捉主要行情的同时,过滤掉微小波动干扰。上下轨形成“天花板”与“地板”,分别对应潜在的超买与超卖区。价格跌破下轨 → 视为极佳买入机会;价格突破上轨 → 提示可考虑卖出。示意图如下:

策略概览

风险管理同样是 PIRANHA 策略的核心环节。它强调通过明确的止损与止盈水平来保护本金。具体设定如下:做多时,止损放在入场价下方 100 点,止盈设在入场价上方 50 点。这套纪律性流程既控制潜在亏损,又锁定利润,使交易方法更具可持续性。

总而言之,PIRANHA 策略将技术分析与波动率捕捉、风险管理融为一体。理解这些原则与参数后,交易者即可更从容地在汇市航行,做出契合自身目标的理性决策。接下来,我们将在MQL5中把这一策略落地,让 PIRANHA 在自动化交易系统中真正“活”起来。


布林带深度解析

交易者可借助布林带这一稳健的技术分析工具,在剧烈波动的市场中捕捉潜在价格走向。John Bollinger于 20 世纪 80 年代提出该指标。它由三个部分组成:中轨(移动平均线)、上轨与下轨。通过计算,交易者可以衡量当前价格相对均值偏离了多少。

首先,我们计算中轨(即移动平均线),通常采用 20 周期的简单移动平均线(SMA)。SMA 的公式为:

SMA公式

其中 𝑃𝑖 为第 i 周期的收盘价,n 为周期数(此处为 20)。举例:若最近 20 个周期的收盘价如下:

周期 收盘价(𝑃𝑖)
1 1.1050
2 1.1070
3 1.1030
4 1.1080
5 1.1040
6 1.1100
7 1.1120
8 1.1150
9 1.1090
10 1.1060
11 1.1085
12 1.1105
13 1.1130
14  1.1110
15  1.1075
16  1.1055
17  1.1080
18  1.1095
19  1.1115
20  1.1120

我们把这些价格累加并除以20:

计算SMA

接下来,我们计算 标准差,用于衡量收盘价相对于简单移动平均线(SMA)的波动幅度。其 (𝜎) 公式为:

STD DEV FORMULA

以我们已算出的 SMA = 1.1080 为基准,先求出每个收盘价与该均值的差值平方,再取平均并开平方根即可得到标准差。例如,前几个差值平方的结果如下:

FIRST SQUARED DIFFERENCES

在计算了所有20个方差后,我们得到:

ALL DIFFERENCES

在已得出中轨(SMA)与标准差(σ)的基础上,我们现在可以计算布林带的上下轨:这些公式如下:

  • 上轨 = SMA + (k × σ)
  • 下轨 = SMA − (k × σ)

这里,我们一般设置k=2(代表两个标准差)。代入,得:

  1. 上轨 = 1.1080 + (2 × 0.0030) = 1.1140
  2. 下轨 = 1.1080 − (2 × 0.0030) = 1.1020

布林带计算结果如下:

  • 中轨(SMA):1.1080
  • 上轨:1.1140
  • 下轨:1.1020

这三个轨道如下所示:

3 BANDS

两条轨道之间的空间会随着市况而动态变化。 当轨道扩张,意味着波动率上升——行情往往开始剧烈。当轨道收缩,则提示市场正进入盘整。交易者常关注价格与轨道的触碰或突破,以此生成信号。价格触及上轨 → 视为超买;价格触及下轨 → 视为超卖。

综上所述,布林带的计算围绕三个核心:SMA 中轨、标准差、上轨与下轨。掌握这些公式不仅是量化阅读的训练,更为交易者提供了做出明智决策的“弹药”——尤其在运用 PIRANHA 策略时尤为关键。


策略的具体实现

上轨:卖出条件

当价格向上穿越并收于布林带上轨之外时,即发出“市场可能超买”的信号。这表明涨势过度,后续很可能出现向下修正。因此,我们把该情形视为卖出信号。因此,若当前 K 线收盘价仍位于上轨之上,即开空仓。以期捕捉潜在反转或回撤。

上轨做空

下轨:买入条件

反之,当价格向下穿越并收于布林带下轨之外时,则暗示市场可能超卖。这代表跌幅已深,随时有望反弹。于是,我们把该情形视为买入信号。因此,若当前 K 线收盘价位于下轨之下,即开多仓,博弈向上反转。

下轨做多

这些策略的图形化表达,对我们在MQL5中实现这些交易条件时非常有帮助,可作为编写精确入场和出场规则的代码的参考。


在MQL5中的实现

在掌握了 PIRANHA 交易策略的全部理论之后,接下来我们把这套策略在MetaTrader 5 中自动化,打造一款EA。

要在MetaTrader 5终端中创建EA,请点击“工具”选项卡并选择“MetaQuotes语言编辑器”,或者简单地在键盘上按F4键。另外,您还可以点击工具栏上的IDE(集成开发环境)图标。这将打开MetaQuotes语言编辑器,允许您编写EA、技术指标、脚本和函数库。

打开编辑器

一旦MetaEditor被打开,在工具栏上,导航到“文件”选项卡并选择“新建文件”,或者简单地按CTRL + N,来创建一个新文档。另外,您也可以点击工具栏上的“新建”图标。这将弹出一个MQL向导(MQL Wizard)窗口。

创建一个新EA

在弹出的向导中,选择“Expert Advisor (template) ”并点击“下一步”。

MQL 向导

在EA的一般属性中,在名称部分,输入您的EA文件的名称。请注意,如果要指定或创建一个不存在的文件夹,您需要在EA名称前使用反斜杠。例如,这里我们默认有“Experts\”。这意味着我们的EA将被创建在Experts文件夹中,我们可以在那里找到它。其他部分都很容易理解,您也可以按照向导底部的链接去详细了解这一过程。

新EA的名称

在提供了您想要的EA文件名后,点击“下一步”,再点击“下一步”,然后点击“完成”。 完成这些步骤后,我们现在就可以开始编写和规划我们的策略了。

首先我们从定义EA的基础数据开始。这包括EA的名称、版本信息和MetaQuotes的网站链接。我们还将指定EA的版本号,设置为“1.00”。

//+------------------------------------------------------------------+
//|                                                      PIRANHA.mq5 |
//|                        Allan Munene Mutiiria, Forex Algo-Trader. |
//|                                     https://forexalgo-trader.com |
//+------------------------------------------------------------------+

//--- Properties to define metadata about the Expert Advisor (EA)
#property copyright "Allan Munene Mutiiria, Forex Algo-Trader."   //--- Copyright information
#property link      "https://forexalgo-trader.com"               //--- Link to the creator's website
#property version   "1.00"                                       //--- Version number of the EA

在加载程序时,会呈现出与下图类似的信息。

METADATA

首先,在源代码的开头使用#include包含一个交易实例。这样我们就能够访问CTrade类,并使用它来创建一个交易对象。这非常关键,因为我们需要用它来执行交易。

//--- Including the MQL5 trading library
#include <Trade/Trade.mqh>      //--- Import trading functionalities
CTrade obj_Trade;               //--- Creating an object of the CTrade class to handle trading operations

预处理器会用文件Trade.mqh的内容替换#include <Trade/Trade.mqh>这一行。尖括号表示Trade.mqh文件将从标准目录(通常是terminal_installation_directory\MQL5\Include)中获取。当前目录不会包含在搜索路径中。这行代码可以放在程序的任何位置,但通常,为了代码结构更好和引用更方便,所有的包含指令都放在源代码的开头。声明CTrade类的obj_Trade对象将使我们能够轻松访问该类中包含的方法,这得益于MQL5开发者的设计。

CTrade 类

我们需要先创建指标句柄(handle),以便在策略中调用所需的指标。

//--- Defining variables for Bollinger Bands indicator and price arrays
int handleBB = INVALID_HANDLE;  //--- Store Bollinger Bands handle; initialized as invalid
double bb_upper[], bb_lower[];  //--- Arrays to store upper and lower Bollinger Bands values

在这里,我们声明并初始化一个integer变量 handleBB,它将作为布林带指标在 EA 中的唯一句柄。在 MQL5 中,句柄 是系统分配给某个指标的唯一标识符,方便在整段代码中随时引用该指标。先把 handleBB 初始化为INVALID_HANDLE,可确保在真正创建指标前不会误用无效句柄,从而避免意外错误。除了句柄,我们还定义两个动态数组:bb_upper 和 bb_lower,分别用于存储布林带的上轨与下轨数值。借助这两个数组,我们可以实时捕获并分析指标状态,为基于布林带条件执行交易策略提供可靠依据。再次强调:我们将确保同一时刻仅保留一个方向的单一持仓。

//--- Flags to track if the last trade was a buy or sell
bool isPrevTradeBuy = false, isPrevTradeSell = false;  //--- Prevent consecutive trades in the same direction

在此,我们声明并初始化两个Boolean标志变量 isPrevTradeBuy 与 isPrevTradeSell,用于记录上一笔已执行交易的方向。两者均初始化为 false,表示尚未发生任何交易。这两个标志将在交易逻辑中发挥关键作用,保证EA不会在同一个方向上连续开仓。比如,若上一笔为买单,isPrevTradeBuy 置为 true,在新卖单出现前禁止再次开多;若上一笔为卖单,isPrevTradeSell 置为 true,在新买单出现前禁止再次开空。由此避免同方向冗余开仓,保持策略平衡。

接下来,我们需要实现OnInit事件处理函数。该函数会在 EA 挂载到图表时由系统自动调用,用于完成所有初始化工作。该函数负责初始化EA,包括创建指标句柄,初始化变量,预分配资源。简言之,OnInit 是 EA 开始处理市场数据前的“启动脚本”。它具体如下:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){
   // OnInit is called when the EA is initialized on the chart
//...
}

在 OnInit 事件处理器中,我们需要先初始化指标句柄,以便为其分配数据值。

   //--- Create Bollinger Bands indicator handle with a period of 12, no shift, and a deviation of 2
   handleBB = iBands(_Symbol, _Period, 12, 0, 2, PRICE_CLOSE);  

这里,我们通过调用iBands函数来创建布林带指标的句柄,并按给定参数生成该指标。传入的参数包括:_Symbol当前图表的交易品种;_Period所用时间周期(可以是分钟、小时、日线等)。布林带的参数包括:周期设为 12,表示计算指标所用的 K 线根数;偏移设为 0,表示不对轨道进行前后平移;标准差设为 2,这决定轨道与移动平均值间的距离。PRICE_CLOSE表示以收盘价作为计算依据。若创建成功,句柄变量 handleBB 将保存一个有效的标识符,供后续获取数据和进行分析。因此,在继续之前必须先检查句柄是否创建成功。

   //--- Check if the Bollinger Bands handle was created successfully
   if (handleBB == INVALID_HANDLE){
      Print("ERROR: UNABLE TO CREATE THE BB HANDLE. REVERTING");  //--- Print error if handle creation fails
      return (INIT_FAILED);  //--- Return initialization failed
   }

在此,我们通过判断布林带指标的句柄是否等于INVALID_HANDLE来验证其创建是否成功。若句柄无效,我们即打印错误信息:“ERROR: UNABLE TO CREATE THE BB HANDLE. REVERTING”,以便在初始化阶段快速定位问题。随后返回INIT_FAILED,表明EA未能正常初始化。若检查通过,则继续将数据数组设为时间序列格式。

   //--- Set the arrays for the Bollinger Bands to be time-series based (most recent data at index 0)
   ArraySetAsSeries(bb_upper, true);  //--- Set upper band array as series
   ArraySetAsSeries(bb_lower, true);  //--- Set lower band array as series
   
   return(INIT_SUCCEEDED);  //--- Initialization successful

在此处,我们调用ArraySetAsSeries函数,并把第二个参数设为 true,从而将布林带数组 bb_upper 与 bb_lower 配置为时间序列格式。这样一来,最新数据被存放在索引 0 的位置,方便在分析行情时快速获取最新值。通过这种数组排布,我们的数据结构契合了交易算法中的常见做法——最新信息往往最为关键。最后,我们返回INIT_SUCCEEDED,表明初始化流程已全部成功完成,EA可以继续执行后续操作。

至此,初始化部分的每一步都运行无误。负责程序初始化的完整源代码如下:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Create Bollinger Bands indicator handle with a period of 12, no shift, and a deviation of 2
   handleBB = iBands(_Symbol, _Period, 12, 0, 2, PRICE_CLOSE);  
   
   //--- Check if the Bollinger Bands handle was created successfully
   if (handleBB == INVALID_HANDLE){
      Print("ERROR: UNABLE TO CREATE THE BB HANDLE. REVERTING");  //--- Print error if handle creation fails
      return (INIT_FAILED);  //--- Return initialization failed
   }
   
   //--- Set the arrays for the Bollinger Bands to be time-series based (most recent data at index 0)
   ArraySetAsSeries(bb_upper, true);  //--- Set upper band array as series
   ArraySetAsSeries(bb_lower, true);  //--- Set lower band array as series
   
   return(INIT_SUCCEEDED);  //--- Initialization successful
  }

接下来进入OnDeinit事件处理器——当程序被反初始化时自动调用的函数。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason){
   // OnDeinit is called when the EA is removed from the chart or terminated
//...
}

OnDeinit会在 EA 从图表移除或终端关闭时触发。我们必须利用此事件处理器,确保资源得到正确清理与管理。当 EA 终止时,需要释放在初始化阶段创建的所有指标句柄。若不释放,就可能留下已占用的内存地址,造成资源浪费;我们绝不想留下任何无用资源。正因如此,OnDeinit 至关重要,在任何编程环境中清理步骤都不可忽视。

IndicatorRelease(handleBB); //--- Release the indicator handle

此处,我们仅用一行代码调用 IndicatorRelease 并传入 handleBB,即可释放先前创建的布林带指标句柄。及时清理对于维持平台性能尤为关键,尤其当您同时运行多个EA或长时间开启终端时。因此,完整的资源释放源代码如下:

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   //--- Function to handle cleanup when the EA is removed from the chart
   IndicatorRelease(handleBB); //--- Release the indicator handle
  }

接下来,每当行情更新时,我们都需要侦测交易机会。这通过 OnTick 事件处理器实现。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){
   // OnTick is called whenever there is a new market tick (price update)

//...

}

事件处理函数 OnTick 会在每次出现新 tick 或市况变动时执行,处理最新价格信息。它是 EA 运行的核心:在这里执行交易逻辑,只要逻辑设计得当,就能带来盈利。每当行情变化,我们随即评估市场状态,决定是否开仓或平仓。该函数随市况实时触发,确保策略紧跟最新价格与指标数值。

为及时掌握最新市况,我们先获取当前报价。

   //--- Get current Ask and Bid prices
   double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits);  //--- Normalize Ask price to correct digits
   double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits);  //--- Normalize Bid price to correct digits

此处使用Ask和Bid来取得交易品种的最新买卖价。要获取这些价格,我们使用SymbolInfoDouble函数。用SYMBOL_ASKSYMBOL_BID分别指定买价与卖价。随后用NormalizeDouble_Digits保留正确小数位。这步很关键,避免浮点误差导致下单价格偏差。如果我们不这么做,可能会导致价格计算错误。接着,我们复制指标值以供分析与下单。

   //--- Retrieve the most recent Bollinger Bands values (3 data points)
   if (CopyBuffer(handleBB, UPPER_BAND, 0, 3, bb_upper) < 3){
      Print("UNABLE TO GET UPPER BAND REQUESTED DATA. REVERTING NOW!");  //--- Error if data fetch fails
      return;
   }
   if (CopyBuffer(handleBB, LOWER_BAND, 0, 3, bb_lower) < 3){
      Print("UNABLE TO GET LOWER BAND REQUESTED DATA. REVERTING NOW!");  //--- Error if data fetch fails
      return;
   }

CopyBuffer用于获取最近 3 个周期的布林带上下轨值。首次调用 CopyBuffer 时,我们从上轨(upper band)获取数据,起始索引为 0,并将结果存入 bb_upper 数组。如果该函数返回值小于 3,说明数据拉取失败,于是输出错误信息: “UNABLE TO GET UPPER BAND REQUESTED DATA. REVERTING NOW!”随后立即退出函数,避免后续逻辑继续执行。对于下轨也采用同样的处理流程,以确保在获取下轨数据时同样能捕获并处理任何错误。需要指出的是,在引用缓冲区索引时,我们使用布林带指标允许的“指标线标识符”,而非直接使用缓冲区编号。这样做最简单,也能避免混淆,核心逻辑保持不变。下方是缓冲区编号的可视化示意图。

缓冲区可视化

为了将指标数值与价格进行比对,我们需要获取当前 K 线的关键价格——即最高价与最低价。

   //--- Get the low and high prices of the current bar
   double low0 = iLow(_Symbol, _Period, 0);    //--- Lowest price of the current bar
   double high0 = iHigh(_Symbol, _Period, 0);  //--- Highest price of the current bar

这里,我们通过调用iLowiHigh两个函数来获取当前 K 线的最低价与最高价。iLow 会返回指定品种(_Symbol)和周期(_Period)下,当前 K 线(索引 0)的最低价,并把该值存入变量 low0。iHigh 同理,返回当前 K 线的最高价,并存入变量 high0。接下来,我们还需确保在同一根 K 线内只触发一次信号。为此采用了以下逻辑代码。

   //--- Get the timestamp of the current bar
   datetime currTimeBar0 = iTime(_Symbol, _Period, 0);  //--- Time of the current bar
   static datetime signalTime = currTimeBar0;           //--- Static variable to store the signal time

在此,我们使用函数iTime获取当前 K 线的时间戳,该函数返回指定品种 (_Symbol) 和周期 (_Period) 下、索引为 0 的 K 线时间。该时间戳存入变量 currTimeBar0。随后,我们声明一个静态变量 signalTime,并用 currTimeBar0 的值为其初始化。由于 signalTime 被设为static,其值在函数调用之间保持不变,从而让我们能追踪最近一次产生交易信号的时间。这对策略至关重要,可防止在同一根 K 线内多次触发信号,确保每根 K 线仅执行一次信号操作。完成上述准备工作后,便可开始检测信号。首先检查买入信号。

   //--- Check for a buy signal when price crosses below the lower Bollinger Band
   if (low0 < bb_lower[0]){
      Print("BUY SIGNAL @ ", TimeCurrent());  //--- Log the buy signal with the current time

   }

在此,我们通过判断当前 K 线的最低价(变量 low0)是否低于最新下轨值(数组 bb_lower[0])来检测潜在买入信号。若 low0 < bb_lower[0],即价格已跌破下轨,暗示可能出现超卖,构成买入机会。一旦条件成立,程序便调用Print函数输出 “BUY SIGNAL @” 并附带TimeCurrent取得的当前时间,方便追踪 EA 的决策时点。这条提示让我们能够追踪何时捕捉到买入信号,为 EA 的决策过程提供透明度和可追溯性。运行后,我们得到如下输出。

买入信号1

从已有的输出可以看到,每当多头条件满足时,我们都会在每个 tick 打印一次信号。而我们希望在每根 K 线只打印一次信号,即使该 K 线内条件多次成立。为此,我们引入以下逻辑。

   //--- Check for a buy signal when price crosses below the lower Bollinger Band
   if (low0 < bb_lower[0] && signalTime != currTimeBar0){
      Print("BUY SIGNAL @ ", TimeCurrent());  //--- Log the buy signal with the current time
      signalTime = currTimeBar0;              //--- Update signal time to avoid duplicate trades
   }

这里,我们在原有条件上增加了一个额外检查,以避免在同一根 K 线内产生重复交易。最初,我们只验证了当前 K 线最低价 low0 是否低于最新下轨 bb_lower[0]。现在,我们加入第二个条件 signalTime != currTimeBar0,确保当前 K 线时间戳 currTimeBar0 与上一次记录的信号时间 signalTime 不同。随后,将 signalTime 更新为 currTimeBar0,从而确认每根 K 线仅处理一次买入信号,即使价格在该 K 线内多次跌破下轨。运行更新后的代码,我们得到如下输出。

BUY SINGLE BAR SIGNAL

成功了。现在我们看到每根K线只打印一次信号。接着我们就能根据信号进行开仓做多了。

      if (PositionsTotal() == 0 && !isPrevTradeBuy){
         obj_Trade.Buy(0.01, _Symbol, Ask, Ask - 100 * _Point, Ask + 50 * _Point);  //--- Open a buy position with predefined parameters
         isPrevTradeBuy = true; isPrevTradeSell = false;  //--- Update trade flags
      }

此处,我们进一步加入条件,确保仅在特定情况下才执行买入交易。先用函数PositionsTotal检查当前总持仓是否为 0,确保没有其它仓位存在。再验证上一笔交易不是买单,即 !isPrevTradeBuy。防止连续开多,并且确保我们的EA不会在已经存在多单的情况下再次下多单。

若两项条件同时满足,则调用 obj_Trade.Buy 开仓。我们将订单手数设为 0.01 手,交易品种为当前图表品种 (_Symbol),入场价格使用 Ask。止损设置在当前 Ask 下方 100 点,止盈则设在 Ask 上方 50 点,从而形成完整的风险管理规则。成功开多后,立即更新交易方向标志:把 isPrevTradeBuy 设为 true,isPrevTradeSell 设为 false;这表示上一笔是买单,在出现卖出信号前不会再开新多单。卖出逻辑采用完全对称的方式,具体如下。

   //--- Check for a sell signal when price crosses above the upper Bollinger Band
   else if (high0 > bb_upper[0] && signalTime != currTimeBar0){
      Print("SELL SIGNAL @ ", TimeCurrent());  //--- Log the sell signal with the current time
      signalTime = currTimeBar0;               //--- Update signal time to avoid duplicate trades
      if (PositionsTotal() == 0 && !isPrevTradeSell){
         obj_Trade.Sell(0.01, _Symbol, Bid, Bid + 100 * _Point, Bid - 50 * _Point);  //--- Open a sell position with predefined parameters
         isPrevTradeBuy = false; isPrevTradeSell = true;  //--- Update trade flags
      }
   }

编译并运行程序,我们得到如下输出。

买入头寸

可以看到我们成功执行了买入操作。随着实现完成,我们已将基于布林带的 PIRANHA 策略整合完毕,并配置程序在符合既定条件时响应买卖信号。在下一部分,我们将专注于测试程序,评估其表现,并微调参数以获得最佳结果。


测试

在完成实现之后,下一步关键步骤是对EA进行彻底测试,以评估其性能并优化其参数。有效的测试可以确保该策略在各种市场条件下表现如预期,最小化交易过程中出现意外问题的风险。在这里,我们将使用MetaTrader 5策略测试器进行回测和优化,以找到策略的最佳输入参数。

首先,我们将为“止损(SL)”和“止盈(TP)”设定初始输入参数,这两个数值对策略的风险管理至关重要。在最初的实现中,SL 和 TP 是以固定点数写入的。然而,为了给策略留出更大的回旋空间,并更有效地捕捉市场波动,我们将把这两个输入参数改为可灵活调整的形式,并在测试阶段进行优化。现在对代码做如下更新:

//--- INPUTS
input int sl_points = 500;
input int tp_points = 250;

//---

   //--- Check for a buy signal when price crosses below the lower Bollinger Band
   if (low0 < bb_lower[0] && signalTime != currTimeBar0){
      Print("BUY SIGNAL @ ", TimeCurrent());  //--- Log the buy signal with the current time
      signalTime = currTimeBar0;              //--- Update signal time to avoid duplicate trades
      if (PositionsTotal() == 0 && !isPrevTradeBuy){
         obj_Trade.Buy(0.01, _Symbol, Ask, Ask - sl_points * _Point, Ask + tp_points * _Point);  //--- Open a buy position with predefined parameters
         isPrevTradeBuy = true; isPrevTradeSell = false;  //--- Update trade flags
      }
   }
   
   //--- Check for a sell signal when price crosses above the upper Bollinger Band
   else if (high0 > bb_upper[0] && signalTime != currTimeBar0){
      Print("SELL SIGNAL @ ", TimeCurrent());  //--- Log the sell signal with the current time
      signalTime = currTimeBar0;               //--- Update signal time to avoid duplicate trades
      if (PositionsTotal() == 0 && !isPrevTradeSell){
         obj_Trade.Sell(0.01, _Symbol, Bid, Bid + sl_points * _Point, Bid - tp_points * _Point);  //--- Open a sell position with predefined parameters
         isPrevTradeBuy = false; isPrevTradeSell = true;  //--- Update trade flags
      }
   }

输入参数使我们能够在不同的品种和商品上进行动态优化。我们运行这个程序,就可得到如下输出。

最终测试

成功了!我们的程序按预期执行。实现 PIRANHA 策略的完整源码片段如下:

//+------------------------------------------------------------------+
//|                                                      PIRANHA.mq5 |
//|                        Allan Munene Mutiiria, Forex Algo-Trader. |
//|                                     https://forexalgo-trader.com |
//+------------------------------------------------------------------+

//--- Properties to define metadata about the Expert Advisor (EA)
#property copyright "Allan Munene Mutiiria, Forex Algo-Trader."   //--- Copyright information
#property link      "https://forexalgo-trader.com"               //--- Link to the creator's website
#property version   "1.00"                                       //--- Version number of the EA

//--- Including the MQL5 trading library
#include <Trade/Trade.mqh>      //--- Import trading functionalities
CTrade obj_Trade;               //--- Creating an object of the CTrade class to handle trading operations

input int sl_points = 500;
input int tp_points = 250;

//--- Defining variables for Bollinger Bands indicator and price arrays
int handleBB = INVALID_HANDLE;  //--- Store Bollinger Bands handle; initialized as invalid
double bb_upper[], bb_lower[];  //--- Arrays to store upper and lower Bollinger Bands values

//--- Flags to track if the last trade was a buy or sell
bool isPrevTradeBuy = false, isPrevTradeSell = false;  //--- Prevent consecutive trades in the same direction

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Create Bollinger Bands indicator handle with a period of 12, no shift, and a deviation of 2
   handleBB = iBands(_Symbol, _Period, 12, 0, 2, PRICE_CLOSE);  
   
   //--- Check if the Bollinger Bands handle was created successfully
   if (handleBB == INVALID_HANDLE){
      Print("ERROR: UNABLE TO CREATE THE BB HANDLE. REVERTING");  //--- Print error if handle creation fails
      return (INIT_FAILED);  //--- Return initialization failed
   }
   
   //--- Set the arrays for the Bollinger Bands to be time-series based (most recent data at index 0)
   ArraySetAsSeries(bb_upper, true);  //--- Set upper band array as series
   ArraySetAsSeries(bb_lower, true);  //--- Set lower band array as series
   
   return(INIT_SUCCEEDED);  //--- Initialization successful
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   //--- Function to handle cleanup when the EA is removed from the chart
   IndicatorRelease(handleBB); //--- Release the indicator handle
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   //--- Retrieve the most recent Bollinger Bands values (3 data points)
   if (CopyBuffer(handleBB, UPPER_BAND, 0, 3, bb_upper) < 3){
      Print("UNABLE TO GET UPPER BAND REQUESTED DATA. REVERTING NOW!");  //--- Error if data fetch fails
      return;
   }
   if (CopyBuffer(handleBB, LOWER_BAND, 0, 3, bb_lower) < 3){
      Print("UNABLE TO GET LOWER BAND REQUESTED DATA. REVERTING NOW!");  //--- Error if data fetch fails
      return;
   }
   
   //--- Get current Ask and Bid prices
   double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits);  //--- Normalize Ask price to correct digits
   double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits);  //--- Normalize Bid price to correct digits

   //--- Get the low and high prices of the current bar
   double low0 = iLow(_Symbol, _Period, 0);    //--- Lowest price of the current bar
   double high0 = iHigh(_Symbol, _Period, 0);  //--- Highest price of the current bar
   
   //--- Get the timestamp of the current bar
   datetime currTimeBar0 = iTime(_Symbol, _Period, 0);  //--- Time of the current bar
   static datetime signalTime = currTimeBar0;           //--- Static variable to store the signal time

   //--- Check for a buy signal when price crosses below the lower Bollinger Band
   if (low0 < bb_lower[0] && signalTime != currTimeBar0){
      Print("BUY SIGNAL @ ", TimeCurrent());  //--- Log the buy signal with the current time
      signalTime = currTimeBar0;              //--- Update signal time to avoid duplicate trades
      if (PositionsTotal() == 0 && !isPrevTradeBuy){
         obj_Trade.Buy(0.01, _Symbol, Ask, Ask - sl_points * _Point, Ask + tp_points * _Point);  //--- Open a buy position with predefined parameters
         isPrevTradeBuy = true; isPrevTradeSell = false;  //--- Update trade flags
      }
   }
   
   //--- Check for a sell signal when price crosses above the upper Bollinger Band
   else if (high0 > bb_upper[0] && signalTime != currTimeBar0){
      Print("SELL SIGNAL @ ", TimeCurrent());  //--- Log the sell signal with the current time
      signalTime = currTimeBar0;               //--- Update signal time to avoid duplicate trades
      if (PositionsTotal() == 0 && !isPrevTradeSell){
         obj_Trade.Sell(0.01, _Symbol, Bid, Bid + sl_points * _Point, Bid - tp_points * _Point);  //--- Open a sell position with predefined parameters
         isPrevTradeBuy = false; isPrevTradeSell = true;  //--- Update trade flags
      }
   }
  }
//+------------------------------------------------------------------+

回测结果:

回测结果

回测结果图形:

回测结果图形

在测试阶段,我们对输入参数进行了优化,并使用策略测试器验证了该策略的表现。通过对止损(SL)和止盈(TP)数值的调整,PIRANHA 策略获得了更大的灵活性。从而能够更好地应对市场波动。我们已经确认,在回测和优化该策略时,它能按照预期运行并取得良好的结果。


结论

在本文中,我们完整展示了如何基于 PIRANHA 策略,使用布林带在 MetaQuotes Language 5(MQL5)中开发一款 EA。我们首先,深入理解 PIRANHA 策略的核心思想;接着,系统梳理布林带的作用——捕捉市场波动并设定进出场点。

随后,逐步演示编码实现:配置指标句柄、构建交易逻辑。最后,通过 MetaTrader 5 的策略测试器,调整关键参数并验证策略在不同市场环境下的有效性。

免责声明:本文所提供的信息仅用于教育目的。本文旨在为基于 PIRANHA 策略开发EA提供思路与范例,并可作为后续进一步优化与测试、构建更高级系统的基石。本文所讨论的策略和方法并不保证任何交易结果,使用本文内容的风险由您自行承担。在应用任何自动化交易解决方案之前,请务必进行彻底测试,并考虑潜在的市场状况。

总体而言,本文旨在为您自动化 PIRANHA 策略并量身定制交易风格提供指引。我们希望它能带来有价值的见解,并激励您在 MQL5 中继续探索、构建更精妙的交易系统。祝编程愉快,交易成功!

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

附加的文件 |
PIRANHA.mq5 (5.57 KB)
最近评论 | 前往讨论 (2)
Roman Shiredchenko
Roman Shiredchenko | 23 5月 2025 在 12:21

非常感谢 - 非常有趣的文章和详细的条件。我将以此为基础,在自定义角色 上编写和测试我的机器人。

Allan Munene Mutiiria
Allan Munene Mutiiria | 23 5月 2025 在 13:28
Roman Shiredchenko 自定义角色 上编写和测试我的机器人。

当然,非常欢迎。

名义变量的序数编码 名义变量的序数编码
在本文中,我们将讨论并演示如何使用Python和MQL5将名义预测变量转换为适合机器学习算法的数值格式。
人工喷淋算法(ASHA) 人工喷淋算法(ASHA)
本文介绍了人工喷淋算法(Artificial Showering Algorithm,ASHA),这是一种为解决一般优化问题而开发的新型元启发式方法。基于对水流和积聚过程的模拟,该算法构建了理想场的概念,其中要求每个资源单元(水)找到最优解。我们将了解 ASHA 如何调整流和累积原则来有效地分配搜索空间中的资源,并查看其实现和测试结果。
如何将“聪明钱”概念(OB)与斐波那契指标相结合,实现最优进场策略 如何将“聪明钱”概念(OB)与斐波那契指标相结合,实现最优进场策略
SMC(订单块)是机构交易者发起大规模买入或卖出的关键区域。当价格出现显著波动后,借助斐波那契数字可识别从近期波段高点至波段低点的潜在回撤,从而锁定最佳进场位。
交易中的神经网络:对比形态变换器 交易中的神经网络:对比形态变换器
对比变换器在设计上基于单根烛条水平和整个形态来分析行情。这有助于提升行情趋势建模的品质。甚至,运用对比学习来统调烛条和形态的表示、促进自我调节,并提升预测的准确性。