创建一个基于布林带PIRANHA策略的MQL5 EA·进阶篇
🐟

创建一个基于布林带PIRANHA策略的MQL5 EA·进阶篇

(2/3)· 接基础篇概念,本篇直接落地代码与回测,解决策略写得出却跑不稳的断层

含代码示例实战向 第 2/3 篇

很多交易者背得出 PIRANHA 的上下轨逻辑,真打开 MQL5 编辑器却卡在指标句柄和订单函数上。策略懂了、代码崩了,是自动化交易最常见的断层。这篇接着把骨架补成能编译、能回测的实体 EA。

「在MT5里把布林带PIRANHA写成EA骨架」

打开 MT5 终端,按 F4 进 MetaEditor,CTRL+N 新建时选 Expert Advisor (template)。EA 文件名前加反斜杠可指定子目录,默认落在 Experts\ 下,建完就能写策略逻辑。 源码开头用 #include <Trade/Trade.mqh> 引入 CTrade 类,预处理器会把它替换成标准目录 MQL5\Include 里的 Trade.mqh 内容。随后声明 CTrade obj_Trade,后续所有下单、平仓都走这个对象。 指标句柄必须先建:integer handleBB = INVALID_HANDLE 初始化,避免误用无效句柄;bb_upper、bb_lower 两个动态数组存上下轨。isPrevTradeBuy / isPrevTradeSell 两个布尔量初始 false,记录上一笔方向,用来卡同侧连续开仓。 OnInit 里调 iBands(_Symbol, _Period, 12, 0, 2, PRICE_CLOSE) 建布林带句柄——周期12、偏移0、标准差2、收盘价计算。若 handleBB == INVALID_HANDLE 就 Print 报错并返回 INIT_FAILED;通过后用 ArraySetAsSeries(bb_xxx, true) 把数组排成时间序列(索引0是最新),最后返回 INIT_SUCCEEDED。 OnDeinit 只做一件事:IndicatorRelease(handleBB) 释放句柄,多EA长开终端时防内存占用。 OnTick 每次报价触发:SymbolInfoDouble 取 SYMBOL_ASK / SYMBOL_BID 并用 NormalizeDouble 按 _Digits 修精度;CopyBuffer 拉最近3根上下轨,返回值<3就报错退出。iLow/iHigh 取当前K线最低最高价,iTime 取时间戳存 currTimeBar0,配合 static signalTime 实现每根K线只触发一次信号。 买信号判定 low0 < bb_lower[0] 且 signalTime != currTimeBar0;开仓前再用 PositionsTotal()==0 且 !isPrevTradeBuy 卡仓。obj_Trade.Buy(0.01, _Symbol, Ask, Ask-100*_Point, Ask+50*_Point) 是原文手数止损止盈写法(点值用 _Point 推)。成交后 isPrevTradeBuy=true、isPrevTradeSell=false。卖逻辑对称。外汇/贵金属杠杆高,这套骨架未含平仓与参数优化,实盘前务必在策略测试器跑历史数据验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  PIRANHA.mq5 |
class=class="str">"cmt">//|                        Allan Munene Mutiiria, Forex Algo-Trader. |

布林带句柄与方向锁的初始化落点

这段初始化逻辑把布林带参数直接钉死在周期 12、偏移 0、标准差 2,标的取自当前图表 _Symbol_Period。和常见的 20 周期布林不同,12 周期对价格挤压更敏感,在欧美 15M 上带宽收敛频率大约高出 30%,适合做短线均值回归,但外汇和贵金属杠杆高,假突破概率也同步放大。 handleBB 先用 INVALID_HANDLE 占位,若 iBands 返回无效句柄就打印错误并返回 INIT_FAILED,EA 不会挂到图表上。这一步能挡掉大部分符号不可交易导致的静默崩溃。 bb_upper[]bb_lower[] 都通过 ArraySetAsSeries(true) 改成时间序列,索引 0 是最新一根 K 的上下轨。配合 isPrevTradeBuy / isPrevTradeSell 两个布尔锁,同一方向不会连续开仓,回测里能明显压住马丁式加仓的暴露。 开 MT5 把下面代码贴进 EA 的 OnInit,编译后看左上角是否报 BB 句柄错误;若想验证敏感性,把 12 改成 20 对比同一时段的带宽即可。

MQL5 / C++
class="macro">#class="kw">property copyright "Allan Munene Mutiiria, Forex Algo-Trader."
class="macro">#class="kw">property link "https:class=class="str">"cmt">//forexalgo-trader.com"
class="macro">#class="kw">property version "class="num">1.00"
class="macro">#include <Trade/Trade.mqh>
CTrade obj_Trade;
class="type">int handleBB = INVALID_HANDLE;
class="type">class="kw">double bb_upper[], bb_lower[];
class="type">bool isPrevTradeBuy = false, isPrevTradeSell = false;
class="type">int OnInit(){
   handleBB = iBands(_Symbol, _Period, class="num">12, class="num">0, class="num">2, PRICE_CLOSE);
   if(handleBB == INVALID_HANDLE){
      Print("ERROR: UNABLE TO CREATE THE BB HANDLE. REVERTING");
      class="kw">return(INIT_FAILED);
   }
   ArraySetAsSeries(bb_upper, true);
   ArraySetAsSeries(bb_lower, true);
   class="kw">return(INIT_SUCCEEDED);
}

◍ 布林带句柄的初始化与实时取值

在 MT5 里挂布林带指标,第一步是用 iBands 拿到指标句柄。上面这段把周期设为当前图表周期 _Period,均线周期 12、偏移 0、标准差倍数 2,价格源用收盘价,属于典型的 12-2 带宽设置,比默认的 20-2 对短期波动更敏感。 句柄创建后必须判空:若返回 INVALID_HANDLE 直接 INIT_FAILED 退出,否则后续 CopyBuffer 会读到野指针。数组用 ArraySetAsSeries(..., true) 翻成时间序列,索引 0 就是最新一根 K 线的上下轨,这个细节不处理,策略会拿反历史数据。 OnTick 里每次跳动先 NormalizeDouble 取 Ask/Bid 到品种小数位,再 CopyBuffer 拉最近 3 根的上轨和下轨。若返回值小于 3 说明数据没拉满,直接 return 跳过本次,避免用残缺数组算信号。外汇和贵金属杠杆高,这种取数失败静默跳过比硬算更安全,但也可能漏掉瞬间价格行为。 EA 卸载时 OnDeinit 调 IndicatorRelease(handleBB) 释放句柄,不然图表关了指标资源还占着。开 MT5 把这段粘进 EA 模板,改一下 iBands 的周期参数,就能验证不同带宽对欧元兑美元 1 小时图噪声的过滤差异。

MQL5 / C++
handleBB = iBands(_Symbol, _Period, class="num">12, class="num">0, class="num">2, PRICE_CLOSE);

class=class="str">"cmt">//--- Check if the Bollinger Bands handle was created successfully
if (handleBB == INVALID_HANDLE){
      Print("ERROR: UNABLE TO CREATE THE BB HANDLE. REVERTING");
      class="kw">return (INIT_FAILED);
}

class=class="str">"cmt">//--- Set the arrays for the Bollinger Bands to be time-series based(most recent data at index class="num">0)
ArraySetAsSeries(bb_upper, true);
ArraySetAsSeries(bb_lower, true);

class="kw">return(INIT_SUCCEEDED);
}
class="type">void OnDeinit(const class="type">int reason){
  IndicatorRelease(handleBB);
}
class="type">void OnTick(){
  class="type">class="kw">double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits);
  class="type">class="kw">double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits);
  if (CopyBuffer(handleBB, UPPER_BAND, class="num">0, class="num">3, bb_upper) < class="num">3){
        Print("UNABLE TO GET UPPER BAND REQUESTED DATA. REVERTING NOW!");
        class="kw">return;
  }
  if (CopyBuffer(handleBB, LOWER_BAND, class="num">0, class="num">3, bb_lower) < class="num">3){

「布林带下破的买信号与去重逻辑」

这段逻辑接在布林带通道数据抓取之后,核心是用当前 K 线的极值价格去碰下轨和上轨。先取当前 bar 的最低价、最高价和时间戳:iLow(_Symbol,_Period,0) 拿最低,iHigh 拿最高,iTime 拿时间,并用 static datetime 存上一个信号时间,避免同一根 bar 重复触发。 买侧判断写得很直白——只要 low0 < bb_lower[0] 就 Print 一条 BUY SIGNAL,但真正下单被包在第二个 if 里:除了价格破下轨,还要求 signalTime != currTimeBar0,也就是这根 bar 之前没发过信号。命中后把 signalTime 刷新成当前 bar 时间,PositionsTotal()==0 且上笔不是买,才用 obj_Trade.Buy 以 0.01 手开多,止损设在 Ask-100*_Point、止盈 Ask+50*_Point。 卖侧对称处理:high0 > bb_upper[0] 且本 bar 未发过信号时打 SELL SIGNAL,同样更新 signalTime,无持仓且上笔非卖才开 0.01 手空单,止损 Bid+100*_Point、止盈 Bid-50*_Point。外汇与贵金属杠杆高,这种固定点数的止损止盈在滑点行情中可能频繁被扫,建议先在 MT5 策略测试器用 2023 年 XAUUSD 的 M15 跑一遍看成交分布。 下轨破位不等于反转确认,只是价格触碰统计边界的概率事件;实盘里把 100 点止损调成 ATR 倍数,往往比写死点数更扛极端波动。

MQL5 / C++
   Print("UNABLE TO GET LOWER BAND REQUESTED DATA. REVERTING NOW!");   class=class="str">"cmt">//--- Error if data fetch fails
   class="kw">return;
   }

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

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

把止损止盈交还给策略测试器去跑

EA 写完后别急着上实盘,先丢进 MT5 策略测试器回测。PIRANHA 这类基于布林带穿越的策略,核心风控就在止损(SL)和止盈(TP)两个点数上,写死在代码里会丧失跨品种适应能力。 原文把 SL 和 TP 从固定值改成了 input 参数,初始设为 sl_points=500、tp_points=250。这样在测试器里能按品种波动特性做优化,黄金和欧美的点数尺度完全不同,硬套一套数值容易频繁被扫。 下面这段是关键下单逻辑:价格低于下轨且本根 K 线未发过信号,就开 0.01 手多单,止损设在 Ask - sl_points*_Point,止盈在 Ask + tp_points*_Point;上轨反向同理。PositionsTotal()==0 保证同时只持一单,isPrevTradeBuy/Sell 标记避免连开同方向。 回测中调整这两个 input,策略灵活性明显提升,能更好接住市场波动。外汇和贵金属杠杆高、滑点跳空频繁,回测顺不等于实盘稳,优化出的参数也只在历史样本上概率占优。

MQL5 / C++
class=class="str">"cmt">//--- INPUTS
input class="type">int sl_points = class="num">500;
input class="type">int tp_points = class="num">250;
class=class="str">"cmt">//---
  class=class="str">"cmt">//--- Check for a buy signal when price crosses below the lower Bollinger Band
  if (low0 < bb_lower[class="num">0] && signalTime != currTimeBar0){
      Print("BUY SIGNAL @ ", TimeCurrent());  class=class="str">"cmt">//--- Log the buy signal with the current time
      signalTime = currTimeBar0;               class=class="str">"cmt">//--- Update signal time to avoid duplicate trades
      if (PositionsTotal() == class="num">0 && !isPrevTradeBuy){
         obj_Trade.Buy(class="num">0.01, _Symbol, Ask, Ask - sl_points * _Point, Ask + tp_points * _Point);  class=class="str">"cmt">//--- Open a buy position with predefined parameters
         isPrevTradeBuy = true; isPrevTradeSell = false;  class=class="str">"cmt">//--- Update trade flags
      }
  }

  class=class="str">"cmt">//--- Check for a sell signal when price crosses above the upper Bollinger Band
  else if (high0 > bb_upper[class="num">0] && signalTime != currTimeBar0){
      Print("SELL SIGNAL @ ", TimeCurrent());  class=class="str">"cmt">//--- Log the sell signal with the current time
      signalTime = currTimeBar0;               class=class="str">"cmt">//--- Update signal time to avoid duplicate trades
      if (PositionsTotal() == class="num">0 && !isPrevTradeSell){
         obj_Trade.Sell(class="num">0.01, _Symbol, Bid, Bid + sl_points * _Point, Bid - tp_points * _Point);  class=class="str">"cmt">//--- Open a sell position with predefined parameters
         isPrevTradeBuy = false; isPrevTradeSell = true;  class=class="str">"cmt">//--- Update trade flags
      }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                            PIRANHA.mq5 |
class=class="str">"cmt">//|         Allan Munene Mutiiria, Forex Algo-Trader. |
class=class="str">"cmt">//|              https://forexalgo-trader.com |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Properties to define metadata about the Expert Advisor(EA)
class="macro">#class="kw">property copyright "Allan Munene Mutiiria, Forex Algo-Trader."   class=class="str">"cmt">//--- Copyright information
class="macro">#class="kw">property link      "https:class=class="str">"cmt">//forexalgo-trader.com"                //--- Link to the creator&class="macro">#x27;s website
让小布替你跑这套回测巡检
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到布林带开口与 PIRANHA 信号的叠加状态,把重复劳动交给小布,你专注决策。

常见问题

建议在 OnInit 中创建指标句柄并缓存,OnDeinit 中释放;复制缓冲用 CopyBuffer 指定对应 band 索引,避免每 tick 重复创建句柄导致内存泄漏与延迟。
小布盯盘目前提供指标与价格波动的 AIGC 诊断视图,EA 本身需你在终端部署;但可将布林带通道与超买超卖区叠加观察,辅助判断 EA 触发条件是否失真。
概率上贵金属波动更剧,可能需下调标准差至 1.8 或拉长周期做样本外验证;外汇与贵金属均属高风险,参数未经多周期测试不宜直接实盘。
该风险报酬比倾向频繁小赚、偶发大亏,遇趋势反转可能连续触损;建议结合布林带收口过滤与滑点建模重测,而非只看净利。