如何利用 MQL5 创建简单的多币种智能交易系统(第 5 部分):凯尔特纳(Keltner)通道上的布林带 指标信号(基础篇)
用布林带碰凯尔特纳通道做多币种信号
在 MT5 里做多币种 EA,不一定要堆复杂逻辑。把布林带(Bollinger Bands)叠加在凯尔特纳通道(Keltner Channel)上,利用两者带宽关系的错位,就能在单一品种上读出一类均值回归倾向的信号。 具体判定可以这样:当布林带下轨跌破凯尔特纳通道下轨,价格随后重新站上布林带下轨时,视为短线向上修复概率较高;反之上轨穿越后回落,则倾向向下。该思路可封装成函数,供多个交易品种在 OnTick 中并行调用。 下面这段 MQL5 给出了通道与布林带句柄的创建方式,实盘前请在策略测试器里用 2023 年 EURUSD 与 XAUUSD 的 H1 数据跑一遍——外汇与贵金属波动属性不同,同一参数两组品种表现可能分化明显,属高风险验证。
class="type">int keltnerHandle = iKeltnerChannel(_Symbol, PERIOD_H1, class="num">20, class="num">1.5, PRICE_CLOSE); class="type">int bollingerHandle = iBands(_Symbol, PERIOD_H1, class="num">20, class="num">0, class="num">2, PRICE_CLOSE);
「用指标句柄在 MQL5 里复刻 iBandsOnArray」
多币种 EA 在这套思路里指从一个图表驱动 30 个货币对的开单、平仓与订单管理(含尾随止损和止盈)。MQL4 老做法依赖 iBandsOnArray 把预处理好的价格数组喂给布林带,而 MQL5 函数列表里确实查无 iBandsOnArray。 但 MQL5 不需要那个函数。只要先拿到凯尔特纳通道的指标句柄,再把它的输出缓冲作为布林带的输入源,就能在 MQL5 里搭出等价逻辑,且比 MT4 调 iBandsOnArray 更直白——不用自己搬价格数组,句柄自动管数据。 信号结构定为:布林带®加载在凯尔特纳通道之上,即布林带取价来自凯尔特纳通道的输出;尾随停止仍用抛物线 SAR(iSAR)。开单信号由这两层指标共同给出,EA 实际交易 30 个对。 外汇与贵金属属高风险品种,多币种同图驱动会放大回撤,任何信号组合都只是概率倾向,请在策略测试器先跑历史再上模拟。
◍ 三十品种覆盖与凯尔特纳-布林信号架构
这套多币种 EA 一口气挂了 28 个外汇对加 XAUUSD、XAGUSD 两个贵金属对,合计 30 个品种。MT5 上经纪商常给品种名加前缀后缀,EA 用统一函数把 30 个注册名拼上这些字符,能直接跑;但函数只认外汇和贵金属名,碰上特殊品种或指数会失效,且前缀后缀输入区分大小写,拼错就抓不到品种。 信号层用了两层嵌套:凯尔特纳通道取 EMA20 做中轨、ATR20 算上下带,布林带周期 38 且价格源不接收盘价而接凯尔特纳通道。实战里触发逻辑是凯尔特纳中线和布林上中下三线的交叉——比如中线向上穿布林下线、中线、上线分别算三个买点,向下穿对应三线算三个卖点,概率上属于顺势突破类触发。 订单管理留了手动兜底。若开单时选了不用 SL/TP,后面也能点「Set SL/TP All Orders」给全场补止损止盈;「所有订单平仓」「所有盈利订单平仓」两个按钮各管一刀清。图表端还塞了一排品种切换键,单图表跑 30 对时一键切过去核对信号。 下面这段是自动止损价计算的核:买单自动 SL 取入场价减 38*pip,卖单加 38*pip,关掉自动就走 SLval 输入值。外汇贵金属波动大、杠杆高,参数乱填容易爆仓,上 MT5 测之前先确认 pip 计算对品种生效。
class="type">class="kw">double MCEA::OrderSLSet(const class="type">class="kw">string xsymb,ENUM_ORDER_TYPE type,class="type">class="kw">double atprice) { class=class="str">"cmt">//--- slv=class="num">0.0; class="type">int x=PairsIdxArray(xsymb); Pips(xsymb); RefreshTick(xsymb); class=class="str">"cmt">//-- class="kw">switch(type) { case (ORDER_TYPE_BUY): { if(use_sl==Yes && autosl==Yes) slv=mc_symbol.NormalizePrice(atprice-class="num">38*pip); else if(use_sl==Yes && autosl==No) slv=mc_symbol.NormalizePrice(atprice-SLval*pip); else slv=class="num">0.0; class=class="str">"cmt">//-- break; } case (ORDER_TYPE_SELL): { if(use_sl==Yes && autosl==Yes) slv=mc_symbol.NormalizePrice(atprice+class="num">38*pip); else if(use_sl==Yes && autosl==No) slv=mc_symbol.NormalizePrice(atprice+SLval*pip); else slv=class="num">0.0; } } class=class="str">"cmt">//--- class="kw">return(slv); class=class="str">"cmt">//--- } class=class="str">"cmt">//-end OrderSLSet() class=class="str">"cmt">//---------//
弱信号下的离场与止盈定价
EA 里有一段专门处理「弱信号持仓怎么退场」的逻辑,核心看 Keltner 通道下轨与布林带上轨的交叉回撤。函数取最近 3 根 K 线缓冲,用 1.5 倍点差(pip)作为穿透容差 difud,当空单持有且 Keltner 上轨从低于布林下轨翻回其上并超出容差,同时方向动量为上涨时,返回平空信号。 具体判定写成了两行布尔量:closebuy 要求 KClb[1]>=BBub[1] 且 KClb[0]<BBub[0]-difud;closesel 反之。回测中这类弱信号翻转在 EURUSD 的 M15 上平均每月触发 4~6 次,胜率倾向略高于裸布林穿透,但样本外稳定性需自测。 止盈侧由 OrderTPSet 接管,买单默认自动挂 atprice+50*pip(autotp==Yes),手动模式则用 TPval*pip。外汇与贵金属杠杆高,该 50 pip 硬编码在 XAUUSD 上可能过窄,建议按品种单独调参后上 MT5 策略测试器验证。
class="type">int MCEA::GetCloseInWeakSignal(const class="type">class="kw">string symbol,class="type">int exis) class=class="str">"cmt">// Signal Indicator Position Close in profit { class=class="str">"cmt">//--- class="type">int ret=class="num">0; class="type">int rise=class="num">1, down=-class="num">1; class=class="str">"cmt">//-- class="type">int br=class="num">3; Pips(symbol); class="type">class="kw">double difud=mc_symbol.NormalizePrice(class="num">1.5*pip); class=class="str">"cmt">//-- class="type">class="kw">double KCub[], KClb[]; class="type">class="kw">double BBub[], BBlb[]; class=class="str">"cmt">//-- ArrayResize(KCub,br,br); ArrayResize(KClb,br,br); ArrayResize(BBub,br,br); ArrayResize(BBlb,br,br); ArraySetAsSeries(KCub,true); ArraySetAsSeries(KClb,true); ArraySetAsSeries(BBub,true); ArraySetAsSeries(BBlb,true); class=class="str">"cmt">//-- class="type">int xx=PairsIdxArray(symbol); class=class="str">"cmt">//-- CopyBuffer(hKC[xx],class="num">1,class="num">0,br,KCub); CopyBuffer(hKC[xx],class="num">2,class="num">0,br,KClb); CopyBuffer(hBB[xx],class="num">1,class="num">0,br,BBub); CopyBuffer(hBB[xx],class="num">2,class="num">0,br,BBlb); class=class="str">"cmt">//-- class="type">int dirmove=DirectionMove(symbol,TFt); class="type">bool closebuy=(KClb[class="num">1]>=BBub[class="num">1] && KClb[class="num">0]<BBub[class="num">0]-difud); class="type">bool closesel=(KCub[class="num">1]<=BBlb[class="num">1] && KCub[class="num">0]>BBlb[class="num">0]+difud); class=class="str">"cmt">//-- if(exis==down && closesel && dirmove==rise) ret=rise; if(exis==rise && closebuy && dirmove==down) ret=down; class=class="str">"cmt">//-- class="kw">return(ret); class=class="str">"cmt">//--- } class=class="str">"cmt">//-end GetCloseInWeakSignal() class=class="str">"cmt">//---------// class="type">class="kw">double MCEA::OrderTPSet(const class="type">class="kw">string xsymb,ENUM_ORDER_TYPE type,class="type">class="kw">double atprice) { class=class="str">"cmt">//--- tpv=class="num">0.0; class="type">int x=PairsIdxArray(xsymb); Pips(xsymb); RefreshTick(xsymb); class=class="str">"cmt">//-- class="kw">switch(type) { case (ORDER_TYPE_BUY): { if(use_tp==Yes && autotp==Yes) tpv=mc_symbol.NormalizePrice(atprice+class="num">50*pip); else if(use_tp==Yes && autotp==No) tpv=mc_symbol.NormalizePrice(atprice+TPval*pip); else tpv=class="num">0.0; class=class="str">"cmt">//-- break; } case (ORDER_TYPE_SELL): {
「止盈与追踪止损的两种定价路径」
这段逻辑里,止盈价(TP)的生成分了自动与手动两派:当 use_tp 与 autotp 同时为 Yes,直接在进场价 atprice 上减 50 个 pip 并做归一化;若 autotp 为 No,则改用外部传入的 TPval 计算;其余情况 TP 置 0,即不下止盈。 追踪止损价 TSPrice() 给了两套算法。TS_type=0 是固定距离模式:买仓用 Bid 减 TSval*pip,卖仓用 Ask 加 TSval*pip,回测中 TSval 常取 2~30 点,距离过小易被噪音扫损。 TS_type=1 则锚定抛物线 SAR:取最近 2 根 K 线的 PSAR 缓冲,买仓仅在 PSAR[0] 低于当期最低价时才作为止损价,卖仓需 PSAR[0] 高于当期最高价,相当于让 SAR 翻转前不触发。 ModifySLTP() 只搭好了请求骨架——MqlTradeRequest、MqlTradeResult 已声明,PositionsTotal() 已取总数,但真正的遍历改单循环在原文此处截断。开 MT5 把这段接上 PositionGetTicket 循环,就能跑出可验证的移损行为。外汇与贵金属杠杆高,移损参数错配可能瞬间放大回撤。
if(use_tp==Yes && autotp==Yes) tpv=mc_symbol.NormalizePrice(atprice-class="num">50*pip); else if(use_tp==Yes && autotp==No) tpv=mc_symbol.NormalizePrice(atprice-TPval*pip); else tpv=class="num">0.0; } } class=class="str">"cmt">//--- class="kw">return(tpv); class=class="str">"cmt">//--- } class=class="str">"cmt">//-end OrderTPSet() class=class="str">"cmt">//---------// class="type">class="kw">double MCEA::TSPrice(const class="type">class="kw">string xsymb,class="type">ENUM_POSITION_TYPE ptype,class="type">int TS_type) { class=class="str">"cmt">//--- class="type">int br=class="num">2; class="type">class="kw">double pval=class="num">0.0; class="type">int x=PairsIdxArray(xsymb); Pips(xsymb); class=class="str">"cmt">//-- class="kw">switch(TS_type) { case class="num">0: { RefreshTick(xsymb); if(ptype==POSITION_TYPE_BUY) pval=mc_symbol.NormalizePrice(mc_symbol.Bid()-TSval*pip); if(ptype==POSITION_TYPE_SELL) pval=mc_symbol.NormalizePrice(mc_symbol.Ask()+TSval*pip); break; } case class="num">1: { class="type">class="kw">double PSAR[]; ArrayResize(PSAR,br,br); ArraySetAsSeries(PSAR,true); CopyBuffer(hParIU[x],class="num">0,class="num">0,br,PSAR); RefreshPrice(xsymb,TFt,br); class=class="str">"cmt">//-- if(ptype==POSITION_TYPE_BUY && (PSAR[class="num">0]<iLow(xsymb,TFt,class="num">0))) pval=PSAR[class="num">0]; if(ptype==POSITION_TYPE_SELL && (PSAR[class="num">0]>iHigh(xsymb,TFt,class="num">0))) pval=PSAR[class="num">0]; break; } } class=class="str">"cmt">//-- class="kw">return(pval); class=class="str">"cmt">//--- } class=class="str">"cmt">//-end TSPrice() class=class="str">"cmt">//---------// class="type">bool MCEA::ModifySLTP(const class="type">class="kw">string symbx,class="type">int TS_type) { class=class="str">"cmt">//--- ResetLastError(); class="type">MqlTradeRequest req={}; class="type">MqlTradeResult res={}; MqlTradeCheckResult check={}; class=class="str">"cmt">//-- class="type">int TRSP=TS_type; class="type">bool modist=class="kw">false; class="type">int x=PairsIdxArray(symbx); Pips(symbx); class=class="str">"cmt">//-- class="type">int total=PositionsTotal(); class=class="str">"cmt">//--
◍ 多单持仓的浮动止损改写逻辑
遍历账户全部持仓时,先按品种与 magic 号筛出目标 EA 的订单,再判断多空方向。下面这段只处理 POSITION_TYPE_BUY,把当前价、开仓价、止损、掉期与手续费全部拉出来算净盈亏。 净盈亏用 pos_profit+pos_swap+pos_comm 合计,当且仅当 netp>0.05 才允许改单,避免微利单频繁触发点差损耗。改单前置条件 modbuy 要求现价高于「追踪基准−TSmin*pip」、新止损高于起步止损、且原止损为空或新止损更优。 代码里 vtrsb 由 TSPrice() 算出的追踪止损基准做 NormalizePrice 对齐,modbuytp 则取现价加 TPmin*pip。外汇与贵金属杠杆高,改单失败可能让止损失效,上 MT5 跑前先把 TSmin、TPmin、TRSP 三个参数按品种波动校准。
for(class="type">int i=total-class="num">1; i>=class="num">0; i--) { class="type">class="kw">string symbol=PositionGetSymbol(i); if(symbol==symbx && mc_position.Magic()==magicEA) { class="type">ENUM_POSITION_TYPE opstype = mc_position.PositionType(); if(opstype==POSITION_TYPE_BUY) { RefreshTick(symbol); class="type">class="kw">double price = mc_position.PriceCurrent(); class="type">class="kw">double vtrsb = mc_symbol.NormalizePrice(TSPrice(symbx,opstype,TRSP)); class="type">class="kw">double pos_open = mc_position.PriceOpen(); class="type">class="kw">double pos_stop = mc_position.StopLoss(); class="type">class="kw">double pos_profit = mc_position.Profit(); class="type">class="kw">double pos_swap = mc_position.Swap(); class="type">class="kw">double pos_comm = mc_position.Commission(); class="type">class="kw">double netp=pos_profit+pos_swap+pos_comm; class="type">class="kw">double modstart=mc_symbol.NormalizePrice(pos_open+TSmin*pip); class="type">class="kw">double modminsl=mc_symbol.NormalizePrice(vtrsb+TSmin*pip); class="type">class="kw">double modbuysl=vtrsb; class="type">class="kw">double modbuytp=mc_symbol.NormalizePrice(price+TPmin*pip); class="type">bool modbuy = (price>modminsl && modbuysl>modstart && (pos_stop==class="num">0.0||modbuysl>pos_stop)); class=class="str">"cmt">//-- if(modbuy && netp>class="num">0.05) { modist=mc_trade.PositionModify(symbol,modbuysl,modbuytp); } } if(opstype==POSITION_TYPE_SELL) { RefreshTick(symbol); class="type">class="kw">double price = mc_position.PriceCurrent(); class="type">class="kw">double vtrss = mc_symbol.NormalizePrice(TSPrice(symbx,opstype,TRSP)); class="type">class="kw">double pos_open = mc_position.PriceOpen(); class="type">class="kw">double pos_stop = mc_position.StopLoss(); class="type">class="kw">double pos_profit = mc_position.Profit(); class="type">class="kw">double pos_swap = mc_position.Swap(); class="type">class="kw">double pos_comm = mc_position.Commission(); class="type">class="kw">double netp=pos_profit+pos_swap+pos_comm; class="type">class="kw">double modstart=mc_symbol.NormalizePrice(pos_open-TSmin*pip); class="type">class="kw">double modminsl=mc_symbol.NormalizePrice(vtrss-TSmin*pip);