创建一个基于布林带PIRANHA策略的MQL5 EA·进阶篇
(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。卖逻辑对称。外汇/贵金属杠杆高,这套骨架未含平仓与参数优化,实盘前务必在策略测试器跑历史数据验证。
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 对比同一时段的带宽即可。
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 小时图噪声的过滤差异。
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 倍数,往往比写死点数更扛极端波动。
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,策略灵活性明显提升,能更好接住市场波动。外汇和贵金属杠杆高、滑点跳空频繁,回测顺不等于实盘稳,优化出的参数也只在历史样本上概率占优。
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