如何利用 MQL5 创建简单的多币种智能交易系统(第 5 部分):凯尔特纳(Keltner)通道上的布林带 指标信号·综合运用
(3/3)· 从指标嵌套到策略测试,把 28 外汇对加金银的 Signals 逻辑一次跑通
- 多币种扫描的数组骨架与输入参数
- 带前缀后缀的货币对名称拼接
- 把商品货币前缀拼好再统管30个品种
- 按币种切换多货币篮子
- 按币种分流与品种名清洗的实现细节
- EA 类里的私有变量布局
- 交易时段与风控变量的结构体声明
- EA 内部函数声明的骨架与分工
- 多币种篮子与指标句柄的成员变量布局
- EA 类里的日期字段与核心方法声明
- 类接口收尾与初始化钩子
- 多周期与指标句柄的初始化套路
- 多品种轮询里的开仓触发逻辑
- 反向反转与持仓上限的触发逻辑
- 弱信号下的利润保护逻辑
- 按星期与自定义时段卡交易开关
- 用时间窗和布林贴轨卡掉无效信号
- 肯特纳与布林带中轨的穿越判定
- 初始化里先把缓冲区和句柄绑死
- 用类方法封装多品种下单逻辑
- 卖单回执与图表按钮的事件分支
- 手动平仓面板的按钮事件处理
- 图表按钮事件与跨日时段定位的实现细节
- 把账户与时段状态写进图表注释
- 正文
- 空止损持仓的兜底挂单逻辑
- 按魔术码与浮盈批量平仓的实现细节
- 盈利微利即平的逻辑与图表切换残留
- 点 R 按钮让 EA 自我卸载
- 在 MT5 测试器里跑多币种通道策略
- 自定义参数比默认更经得起回测
「多币种扫描的数组骨架与输入参数」
做跨品种波动带监控,第一步是把品种池结构化。下面这段代码把 30 个常见外汇与贵金属对拆成全量池和按基准货币归类的子池,方便后续按板块统计强弱。 input ENUM_APPLIED_PRICE ma_price = PRICE_TYPICAL; // 均线计算所用价格,典型价(H+L+C)/3 input int atr_period = 20; // ATR 周期,常用 10 以上或 20 input double band_multi = 1.00; // 通道倍数,直接乘 ATR 宽度 这段代码三个外部输入决定了通道的基线:ATR 取 20 周期在欧美盘波动中属于平滑但不迟钝的设定,band_multi 设为 1.0 意味着通道就是 ±1 倍 ATR。 void MCEA::HandlingSymbolArrays(void) { //--- string All30[]={"EURUSD","GBPUSD","AUDUSD","NZDUSD","USDCAD","USDCHF","USDJPY","EURGBP", "EURAUD","EURNZD","EURCAD","EURCHF","EURJPY","GBPAUD","GBPNZD","GBPCAD", "GBPCHF","GBPJPY","AUDNZD","AUDCAD","AUDCHF","AUDJPY","NZDCAD","NZDCHF", "NZDJPY","CADCHF","CADJPY","CHFJPY","XAUUSD","XAGUSD"}; // 30 pairs string USDs[]={"USDCAD","USDCHF","USDJPY","AUDUSD","EURUSD","GBPUSD","NZDUSD","XAUUSD","XAGUSD"}; // USD pairs string EURs[]={"EURAUD","EURCAD","EURCHF","EURGBP","EURJPY","EURNZD","EURUSD"}; // EUR pairs string GBPs[]={"GBPAUD","GBPCAD","GBPCHF","EURGBP","GBPJPY","GBPNZD","GBPUSD"}; // GBP pairs string AUDs[]={"AUDCAD","AUDCHF","EURAUD","GBPAUD","AUDJPY","AUDNZD","AUDUSD"}; // AUD pairs string NZDs[]={"AUDNZD","NZDCAD","NZDCHF","EURNZD","GBPNZD","NZDJPY","NZDUSD"}; // NZD pairs string CADs[]={"AUDCAD","CADCHF","EURCAD","GBPCAD","CADJPY","NZDCAD","USDCAD"}; // CAD pairs string CHFs[]={"AUDCHF","CADCHF","EURCHF","GBPCHF","NZDCHF","CHFJPY","USDCHF"}; // CHF pairs string JPYs[]={"AUDJPY","CADJPY","CHFJPY","EURJPY","GBPJPY","NZDJPY","USDJPY"}; // JPY pairs //-- sall=ArraySize(All30); arusd=ArraySize(USDs); aretc=ArraySize(EURs); 逐行看:All30 数组装了 30 个品种,含 XAUUSD 与 XAGUSD 两个贵金属;USDs 到 JPYs 是 9 个按美元、欧元、英镑等基准货币切分的子池,每个 7~9 个品种。末尾用 ArraySize 把全量长度写进 sall(30)、美元组长度写进 arusd(9)、欧元组写进 aretc(7)。 开 MT5 把这段塞进类方法里,先 Print(sall, arusd, aretc) 确认输出是 30 / 9 / 7;外汇与贵金属杠杆高、滑点跳空频繁,数组里 XAUUSD 在亚盘流动性薄时 ATR 可能瞬时放大,参数需按实盘复盘再调。
class="kw">input ENUM_APPLIED_PRICE ma_price = PRICE_TYPICAL; class=class="str">"cmt">// Select MA Applied Price class="kw">input class="type">int atr_period = class="num">20; class=class="str">"cmt">// Input ATR Period(typically over class="num">10 or class="num">20) class="kw">input class="type">class="kw">double band_multi = class="num">1.00; class=class="str">"cmt">// Input the Band Multiplier ATR Desired class=class="str">"cmt">//-- class="type">void MCEA::HandlingSymbolArrays(class="type">void) { class=class="str">"cmt">//--- class="type">class="kw">string All30[]={"EURUSD","GBPUSD","AUDUSD","NZDUSD","USDCAD","USDCHF","USDJPY","EURGBP", "EURAUD","EURNZD","EURCAD","EURCHF","EURJPY","GBPAUD","GBPNZD","GBPCAD", "GBPCHF","GBPJPY","AUDNZD","AUDCAD","AUDCHF","AUDJPY","NZDCAD","NZDCHF", "NZDJPY","CADCHF","CADJPY","CHFJPY","XAUUSD","XAGUSD"}; class=class="str">"cmt">// class="num">30 pairs class="type">class="kw">string USDs[]={"USDCAD","USDCHF","USDJPY","AUDUSD","EURUSD","GBPUSD","NZDUSD","XAUUSD","XAGUSD"}; class=class="str">"cmt">// USD pairs class="type">class="kw">string EURs[]={"EURAUD","EURCAD","EURCHF","EURGBP","EURJPY","EURNZD","EURUSD"}; class=class="str">"cmt">// EUR pairs class="type">class="kw">string GBPs[]={"GBPAUD","GBPCAD","GBPCHF","EURGBP","GBPJPY","GBPNZD","GBPUSD"}; class=class="str">"cmt">// GBP pairs class="type">class="kw">string AUDs[]={"AUDCAD","AUDCHF","EURAUD","GBPAUD","AUDJPY","AUDNZD","AUDUSD"}; class=class="str">"cmt">// AUD pairs class="type">class="kw">string NZDs[]={"AUDNZD","NZDCAD","NZDCHF","EURNZD","GBPNZD","NZDJPY","NZDUSD"}; class=class="str">"cmt">// NZD pairs class="type">class="kw">string CADs[]={"AUDCAD","CADCHF","EURCAD","GBPCAD","CADJPY","NZDCAD","USDCAD"}; class=class="str">"cmt">// CAD pairs class="type">class="kw">string CHFs[]={"AUDCHF","CADCHF","EURCHF","GBPCHF","NZDCHF","CHFJPY","USDCHF"}; class=class="str">"cmt">// CHF pairs class="type">class="kw">string JPYs[]={"AUDJPY","CADJPY","CHFJPY","EURJPY","GBPJPY","NZDJPY","USDJPY"}; class=class="str">"cmt">// JPY pairs class=class="str">"cmt">//-- sall=ArraySize(All30); arusd=ArraySize(USDs); aretc=ArraySize(EURs);
◍ 带前缀后缀的货币对名称拼接
MT5 里不少券商给品种名加了前缀或后缀(比如 m. 或 .pro),直接拿裸名去查会返回 -1。代码里先用 SetSymbolNamePS() 探测 inpre / insuf,命中后再批量给数组元素补上 pre、suf。
下面这段是处理自选对与全量数组的核心逻辑:先对交易愿望字符串按逗号切分、转大写、剔除非法对,再把前缀后缀套到每一个元素上。
若 usepairs==TrdWi 且切出了 arspc>0 个合法对,就遍历 SPC 给每个对名加 pre 和 suf;同时 All30、USDs、EURs、GBPs 也按各自长度(sall / arusd / aretc)统一拼接。这样后续 SymbolSelect / iBars 调用才不会因名称不匹配而失效。外汇与贵金属品种命名规则由各平台自定义,实盘前务必在 MT5 符号表核对真名,杠杆与滑点风险较高。
ArrayResize(VSym,sall,sall); ArrayCopy(VSym,All30,class="num">0,class="num">0,WHOLE_ARRAY); class=class="str">"cmt">//-- if(usepairs==TrdWi && StringFind(traderwishes,"eg.",class="num">0)<class="num">0) { class="type">class="kw">string to_split=traderwishes; class=class="str">"cmt">// A class="type">class="kw">string to split into substrings pairs name class="type">class="kw">string sep=","; class=class="str">"cmt">// A separator as a character class="type">class="kw">ushort u_sep; class=class="str">"cmt">// The code of the separator character class=class="str">"cmt">//--- Get the separator code u_sep=StringGetCharacter(sep,class="num">0); class=class="str">"cmt">//--- Split the class="type">class="kw">string to substrings class="type">int p=StringSplit(to_split,u_sep,SPC); if(p>class="num">0) { for(class="type">int i=class="num">0; i<p; i++) StringToUpper(SPC[i]); class=class="str">"cmt">//-- for(class="type">int i=class="num">0; i<p; i++) { if(ValidatePairs(SPC[i])<class="num">0) ArrayRemove(SPC,i,class="num">1); } } arspc=ArraySize(SPC); } class=class="str">"cmt">//-- SetSymbolNamePS(); class=class="str">"cmt">// With this function we will detect whether the Symbol Name has a prefix and/or suffix class=class="str">"cmt">//-- if(inpre>class="num">0 || insuf>class="num">0) { if(usepairs==TrdWi && arspc>class="num">0) { for(class="type">int t=class="num">0; t<arspc; t++) { SPC[t]=pre+SPC[t]+suf; } } class=class="str">"cmt">//-- for(class="type">int t=class="num">0; t<sall; t++) { All30[t]=pre+All30[t]+suf; } for(class="type">int t=class="num">0; t<arusd; t++) { USDs[t]=pre+USDs[t]+suf; } for(class="type">int t=class="num">0; t<aretc; t++) { EURs[t]=pre+EURs[t]+suf; } for(class="type">int t=class="num">0; t<aretc; t++) { GBPs[t]=pre+GBPs[t]+suf; } }
把商品货币前缀拼好再统管30个品种
这段初始化逻辑先把 AUD、NZD、CAD、CHFs、JPYs 五个数组各自循环 aretc 次,给每个元素前后接上 pre 与 suf 字符串,等于把裸符号补全成带前缀后缀的标准品种名。 补全后通过 ArrayCopy 把 All30 整体拷进 VSym,再用 ArrayResize(AS30,sall,sall) 按 sall 长度定型,随后把 AS30 也填成同样的 30 个品种,并对每个 x 调 SymbolSelect(AS30[x],true) 确保市场报价窗口已订阅。 若 ValidatePairs(Symbol()) 返回值小于 0,symbfix 保持 false,脚本会连弹两条 Alert 说明当前图表品种不在交易范围内,并直接 ExpertRemove() 把 EA 从图表踢掉——外汇与贵金属杠杆高,跑在未被校验的品种上可能触发非预期空单。 usepairs 开关决定实际交易集:case 0 把 DIRI 扩到 sall 并整拷 All30,标记为 Multi Currency 30 Pairs;case 1 则按 arspc 长度只装交易者自选的 SPC 数组,标题会显示具体自选数量。
for(class="type">int t=class="num">0; t<aretc; t++) { AUDs[t]=pre+AUDs[t]+suf; } for(class="type">int t=class="num">0; t<aretc; t++) { NZDs[t]=pre+NZDs[t]+suf; } for(class="type">int t=class="num">0; t<aretc; t++) { CADs[t]=pre+CADs[t]+suf; } for(class="type">int t=class="num">0; t<aretc; t++) { CHFs[t]=pre+CHFs[t]+suf; } for(class="type">int t=class="num">0; t<aretc; t++) { JPYs[t]=pre+JPYs[t]+suf; } } class=class="str">"cmt">//-- ArrayCopy(VSym,All30,class="num">0,class="num">0,WHOLE_ARRAY); ArrayResize(AS30,sall,sall); ArrayCopy(AS30,All30,class="num">0,class="num">0,WHOLE_ARRAY); for(class="type">int x=class="num">0; x<sall; x++) {SymbolSelect(AS30[x],true);} if(ValidatePairs(Symbol())>=class="num">0) symbfix=true; if(!symbfix) { Alert("Expert Advisors will not trade on pairs "+Symbol()); Alert("-- "+expname+" -- ",Symbol()," -- Expert Advisor will be Remove from the chart."); ExpertRemove(); } class=class="str">"cmt">//-- class="kw">switch(usepairs) { case class="num">0: class=class="str">"cmt">// All Forex class="num">30 Pairs { ArrayResize(DIRI,sall,sall); arrsymbx=sall; ArraySymbolResize(); ArrayCopy(DIRI,All30,class="num">0,class="num">0,WHOLE_ARRAY); pairs="Multi Currency class="num">30 Pairs"; class=class="str">"cmt">//-- break; } case class="num">1: class=class="str">"cmt">// Trader wishes pairs { ArrayResize(DIRI,arspc,arspc); arrsymbx=arspc; ArraySymbolResize(); ArrayCopy(DIRI,SPC,class="num">0,class="num">0,WHOLE_ARRAY); pairs="("+class="type">class="kw">string(arspc)+") Trader Wishes Pairs"; class=class="str">"cmt">//--
「按币种切换多货币篮子」
这段 switch 分支负责把指标监控的货币对集合切到具体币种组。case 2 到 case 6 分别处理 USD、EUR、GBP、AUD、NZD 五大直盘篮子,每个分支先按对应计数变量重设 DIRI 数组尺寸,再调用 ArraySymbolResize() 同步符号容器,最后用 ArrayCopy 把预存的币种数组整组搬进 DIRI。 以 USD 组为例:ArrayResize(DIRI,arusd,arusd) 把 DIRI 拉到 arusd 个槽位,arusd 就是 USD 相关对的数量;pairs 字符串会拼出类似 "(7) Multi Currency USD Pairs" 的标签,方便面板直接读。EUR/GBP/AUD/NZD 四组共用 aretc 作为长度,说明这几类篮子默认对等数量。 外汇与贵金属属高杠杆品种,篮子切换逻辑若计数变量算错,可能让指标漏掉部分品种。开 MT5 把 arusd、aretc 打印出来,核对和你 broker 实际可交易品种数是否一致。
break; } case class="num">2: class=class="str">"cmt">// USD pairs { ArrayResize(DIRI,arusd,arusd); arrsymbx=arusd; ArraySymbolResize(); ArrayCopy(DIRI,USDs,class="num">0,class="num">0,WHOLE_ARRAY); pairs="("+class="type">class="kw">string(arusd)+") Multi Currency USD Pairs"; class=class="str">"cmt">//-- break; } case class="num">3: class=class="str">"cmt">// EUR pairs { ArrayResize(DIRI,aretc,aretc); arrsymbx=aretc; ArraySymbolResize(); ArrayCopy(DIRI,EURs,class="num">0,class="num">0,WHOLE_ARRAY); pairs="("+class="type">class="kw">string(aretc)+") Forex EUR Pairs"; class=class="str">"cmt">//-- break; } case class="num">4: class=class="str">"cmt">// GBP pairs { ArrayResize(DIRI,aretc,aretc); arrsymbx=aretc; ArraySymbolResize(); ArrayCopy(DIRI,GBPs,class="num">0,class="num">0,WHOLE_ARRAY); pairs="("+class="type">class="kw">string(aretc)+") Forex GBP Pairs"; class=class="str">"cmt">//-- break; } case class="num">5: class=class="str">"cmt">// AUD pairs { ArrayResize(DIRI,aretc,aretc); arrsymbx=aretc; ArraySymbolResize(); ArrayCopy(DIRI,AUDs,class="num">0,class="num">0,WHOLE_ARRAY); pairs="("+class="type">class="kw">string(aretc)+") Forex AUD Pairs"; class=class="str">"cmt">//-- break; } case class="num">6: class=class="str">"cmt">// NZD pairs { ArrayResize(DIRI,aretc,aretc); arrsymbx=aretc; ArraySymbolResize(); ArrayCopy(DIRI,NZDs,class="num">0,class="num">0,WHOLE_ARRAY);
◍ 按币种分流与品种名清洗的实现细节
在 HandlingSymbolArrays() 的 switch 分支里,case 7/8/9 分别对应加元、瑞郎、日元系外汇组。每组都先按 aretc 动态扩容 DIRI 数组,再把对应币种数组(CADs / CHFs / JPYs)整组拷入,pairs 字符串会拼出「(N) Forex XXX Pairs」便于面板标识,其中 N 即 aretc 表示的品种数量。 这段逻辑依赖 ArrayResize(DIRI,aretc,aretc) 与 ArrayCopy(DIRI,XXXs,0,0,WHOLE_ARRAY) 的配合:前者预留内存,后者从 0 偏移全量复制,arrsymbx 同步记录实际长度。开 MT5 把 aretc 打印出来,就能确认某次加载到底塞进了多少个 CAD 或 JPY 品种。 SetSymbolNamePS() 则负责剥离前缀后缀。它先取 Symbol() 原始名,量出 prefix、suffix 长度;若长度大于 0 就调用 StringTrimLeft / StringTrimRight 处理,最后把 sym_pre、sym_suf 赋给 pre、suf。注意原文里 trim 操作传的是 suffix 而非对应前缀变量,这处可能是笔误,复制代码时建议核对。 外汇与贵金属品种名常带 broker 前缀(如 m.、c.),这套清洗若漏掉会导致后续匹配失效。高杠杆下品种识别错误可能引发错单,实盘前务必在策略测试器里单步验证 pre/suf 取值。
pairs=class="type">class="kw">string(aretc)+" Forex NZD Pairs"; class=class="str">"cmt">//-- break; case class="num">7: class=class="str">"cmt">// CAD pairs { ArrayResize(DIRI,aretc,aretc); arrsymbx=aretc; ArraySymbolResize(); ArrayCopy(DIRI,CADs,class="num">0,class="num">0,WHOLE_ARRAY); pairs=class="type">class="kw">string(aretc)+" Forex CAD Pairs"; class=class="str">"cmt">//-- break; } case class="num">8: class=class="str">"cmt">// CHF pairs { ArrayResize(DIRI,aretc,aretc); arrsymbx=aretc; ArraySymbolResize(); ArrayCopy(DIRI,CHFs,class="num">0,class="num">0,WHOLE_ARRAY); pairs=class="type">class="kw">string(aretc)+" Forex CHF Pairs"; class=class="str">"cmt">//-- break; } case class="num">9: class=class="str">"cmt">// JPY pairs { ArrayResize(DIRI,aretc,aretc); arrsymbx=aretc; ArraySymbolResize(); ArrayCopy(DIRI,JPYs,class="num">0,class="num">0,WHOLE_ARRAY); pairs=class="type">class="kw">string(aretc)+" Forex JPY Pairs"; class=class="str">"cmt">//-- break; } } class=class="str">"cmt">//-- class="kw">return; class=class="str">"cmt">//--- } class=class="str">"cmt">//-end HandlingSymbolArrays() class=class="str">"cmt">//---------// class="type">void MCEA::SetSymbolNamePS(class="type">void) { class=class="str">"cmt">//--- symbfix=class="kw">false; class="type">int ptriml; class="type">int ptrimr; class="type">class="kw">string insymbol=Symbol(); class="type">int sym_Lenpre=StringLen(prefix); class="type">int sym_Lensuf=StringLen(suffix); if(sym_Lenpre>class="num">0) { ptriml=StringTrimLeft(suffix); ptriml=StringTrimRight(suffix); } if(sym_Lensuf>class="num">0) { ptrimr=StringTrimLeft(suffix); ptrimr=StringTrimRight(suffix); } class="type">class="kw">string sym_pre=prefix; class="type">class="kw">string sym_suf=suffix; class=class="str">"cmt">//-- pre=sym_pre; suf=sym_suf;
EA 类里的私有变量布局
这段 MQL5 代码露出了专家顾问核心类的私有段,主要干两件事:给带前缀后缀的品种名算位置,给交易时段记边界。 前缀长度用 StringLen(pre) 取,后缀同理,posCur1 直接等于 inpre,posCur2 则固定跳 3 个字符——这意味着代码默认品种名中段有 3 位固定标识(如 XAU 里的 'XAU' 后接数字)。 时间类变量铺得很细:x_year 到 x_sec 六个整型把当前时间拆到秒,SesCuOp / SesCuCl 到 Ses02Cl 这组 datetime 则准备存两个交易时段的开关点。做黄金或外汇 EA 时,这种拆分能让你在 OnTick 里直接比对 session 边界,避免跨时段误成交。 开 MT5 把这段粘进 class MCEA 私有区,编译后看左侧导航的变量树,能确认前缀解析逻辑是否和你 broker 的品种命名一致(比如 'EURUSD.pro' 的 suf 长度会是 4)。
inpre=StringLen(pre); insuf=StringLen(suf); posCur1=inpre; posCur2=posCur1+class="num">3; class=class="str">"cmt">//-- class="kw">return; class=class="str">"cmt">//--- } class=class="str">"cmt">//-end SetSymbolNamePS() class=class="str">"cmt">//--------- class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class for working Expert Advisor | class=class="str">"cmt">//+------------------------------------------------------------------+ class MCEA { class=class="str">"cmt">//--- class="kw">private: class=class="str">"cmt">//---- class="type">int x_year; class=class="str">"cmt">// Year class="type">int x_mon; class=class="str">"cmt">// Month class="type">int x_day; class=class="str">"cmt">// Day of the month class="type">int x_hour; class=class="str">"cmt">// Hour in a day class="type">int x_min; class=class="str">"cmt">// Minutes class="type">int x_sec; class=class="str">"cmt">// Seconds class=class="str">"cmt">//-- class="type">int oBm, oSm, ldig; class=class="str">"cmt">//--- Variables used in prefix and suffix symbols class="type">int posCur1, posCur2; class="type">int inpre, insuf; class="type">bool symbfix; class="type">class="kw">string pre,suf; class="type">class="kw">string prefix,suffix; class=class="str">"cmt">//--- Variables are used in Trading Time Zone class="type">int ishour, onhour; class="type">int tftrlst, tfcinws; class="type">class="kw">datetime rem, znop, zncl, zntm; class="type">class="kw">datetime SesCuOp, SesCuCl, Ses01Op, Ses01Cl, Ses02Op, Ses02Cl,
「交易时段与风控变量的结构体声明」
这段声明把多时段交易所需的开收盘价、时区字符串以及下单参数一次性铺开,方便后续按会话(Session)索引。Ses03Op 到 Ses05Cl 记录了第3至第5时段的开口与收口价,SesNoOp / SesNoCl 则留给「无时段」兜底,避免空指针式报错。 tz_ses、tz_opn、tz_cls 三个字符串用来承载时区名与开收盘标签;tmop01~tmop05 配 tmcl01~tmcl05 则是把对应时段的开关时间也存成文本,回测时可直接打印核对。 LotPS 每点手数、slv/tpv 止损止盈价、pip/xpip 点值,加 SARstep / SARmaxi 两组抛物线参数,构成仓位与出场骨架。floatprofit 与 fixclprofit 分离浮动与固定平仓收益,便于写日志。 最后 OPEN[] HIGH[] LOW[] CLOSE[] TIME[] 五个数组成对声明,是 MT5 里接 CopyBuffer 或 iBar 前的标准容器。开 MT5 新建 EA 时,照这套命名直接粘进全局区,编译能过就说明时段框架没漏变量。
Ses03Op,
Ses03Cl,
Ses04Op,
Ses04Cl,
Ses05Op,
Ses05Cl,
SesNoOp,
SesNoCl;
class=class="str">"cmt">//--
class="type">class="kw">string tz_ses,
tz_opn,
tz_cls;
class=class="str">"cmt">//--
class="type">class="kw">string tmopcu,
tmclcu,
tmop01,
tmcl01,
tmop02,
tmcl02,
tmop03,
tmcl03,
tmop04,
tmcl04,
tmop05,
tmcl05,
tmopno,
tmclno;
class=class="str">"cmt">//----------------------
class=class="str">"cmt">//--
class="type">class="kw">double LotPS;
class="type">class="kw">double slv,
tpv,
pip,
xpip;
class="type">class="kw">double SARstep,
SARmaxi;
class="type">class="kw">double floatprofit,
fixclprofit;
class=class="str">"cmt">//--
class="type">class="kw">string pairs,
hariini,
daytrade,
trade_mode;
class=class="str">"cmt">//--
class="type">class="kw">double OPEN[],
HIGH[],
LOW[],
CLOSE[];
class="type">class="kw">datetime TIME[];◍ EA 内部函数声明的骨架与分工
这段声明块定义了一个多品种 EA 的核心接口层,从时间处理到下单风控全在一处列清。 datetime closetime; 记录仓位平仓时间,配合后面的 Time_Zone、ThisTime、ReqTime 等函数,说明该 EA 把时区换算和本地时间请求拆成了独立方法,而不是在 OnTick 里硬算。 下方 DirectionMove、BBOnKeltnerChannel、PARSAR05 是三类信号判定函数,分别处理方向位移、布林带与肯特纳通道关系、以及改良 SAR 过滤;LotDig、MLots 管手数精度。 OrderSLSet / SetOrderSL 与 OrderTPSet / SetOrderTP 成对出现,区分挂单与持仓的止损止盈计算逻辑,TSPrice 则接管移动止损价。外汇与贵金属波动剧烈,这类函数若时区或手数算错,可能直接放大回撤。 开 MT5 把这段声明贴进头文件,先编译看 Symbol 相关函数是否报未定义,能快速验证你当前品种数组逻辑是否接得上。
class="type">class="kw">datetime closetime; class=class="str">"cmt">//-- class=class="str">"cmt">//------------ class=class="str">"cmt">//------------ class="type">void SetSymbolNamePS(class="type">void); class="type">void HandlingSymbolArrays(class="type">void); class="type">void Set_Time_Zone(class="type">void); class="type">void Time_Zone(class="type">void); class="type">bool Trade_session(class="type">void); class="type">class="kw">string PosTimeZone(class="type">void); class="type">int ThisTime(const class="type">int reqmode); class="type">int ReqTime(class="type">class="kw">datetime reqtime,const class="type">int reqmode); class=class="str">"cmt">//-- class="type">int DirectionMove(const class="type">class="kw">string symbol,const ENUM_TIMEFRAMES stf); class="type">int BBOnKeltnerChannel(const class="type">class="kw">string symbol); class="type">int PARSAR05(const class="type">class="kw">string symbol); class="type">int LotDig(const class="type">class="kw">string symbol); class=class="str">"cmt">//-- class="type">class="kw">double MLots(const class="type">class="kw">string symbx); class="type">class="kw">double NonZeroDiv(class="type">class="kw">double val1,class="type">class="kw">double val2); class="type">class="kw">double OrderSLSet(const class="type">class="kw">string xsymb,ENUM_ORDER_TYPE type,class="type">class="kw">double atprice); class="type">class="kw">double OrderTPSet(const class="type">class="kw">string xsymb,ENUM_ORDER_TYPE type,class="type">class="kw">double atprice); class="type">class="kw">double SetOrderSL(const class="type">class="kw">string xsymb,class="type">ENUM_POSITION_TYPE type,class="type">class="kw">double atprice); class="type">class="kw">double SetOrderTP(const class="type">class="kw">string xsymb,class="type">ENUM_POSITION_TYPE type,class="type">class="kw">double atprice); class="type">class="kw">double 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">class="kw">string ReqDate(class="type">int d,class="type">int h,class="type">int m); class="type">class="kw">string TF2Str(ENUM_TIMEFRAMES period); class="type">class="kw">string timehr(class="type">int hr,class="type">int mn); class="type">class="kw">string TradingDay(class="type">void); class="type">class="kw">string AccountMode();
多币种篮子与指标句柄的成员变量布局
这段类声明把布林带叠加肯特纳通道(BBOnKeltnerChannel)策略需要的全局状态一次性摊开,主要分三块:币种符号数组、指标句柄、运行参数。 币种维度用 DIRI[]、AS30[]、VSym[] 三个字符串数组承接方向表与别名映射,再单列 SPC[]、USD[]、EUR[]、GBP[]、AUD[]、NZD[]、CAD[]、CHF[]、JPY[] 共 9 个货币组数组成员,说明策略覆盖外汇全主要直盘与交叉盘,贵金属交易者也可类比把 XAU[]、XAG[] 塞进同类结构。 指标句柄部分用 hKC[]、hBB[] 存肯特纳与布林带句柄,hParIU[]、hPar05[] 存两组辅助参数句柄;ALO、dgts、arrsar、arrsymbx 等 int 成员控制数组长度与精度,slip 用 ulong 存允许滑点。 GetCommentForOrder 直接返回 expname,订单注释即策略名,方便在 MT5 终端历史里一眼区分 EA 实例。外汇与贵金属杠杆高、滑点跳空频繁,slip 设小了可能拒单,设大了回测与实盘偏差会拉大,建议先在策略测试器用 2023 年 EURUSD M15 跑一遍看成交率。
class="type">class="kw">string GetCommentForOrder(class="type">void) { class="kw">return(expname); } class=class="str">"cmt">//------------ class="kw">public: class=class="str">"cmt">//--- class=class="str">"cmt">//-- BBOnKeltnerChannel_MCEA Config -- class="type">class="kw">string DIRI[], AS30[], VSym[]; class="type">class="kw">string SPC[]; class="type">class="kw">string USD[]; class="type">class="kw">string EUR[]; class="type">class="kw">string GBP[]; class="type">class="kw">string AUD[]; class="type">class="kw">string NZD[]; class="type">class="kw">string CAD[]; class="type">class="kw">string CHF[]; class="type">class="kw">string JPY[]; class=class="str">"cmt">//-- class="type">class="kw">string expname; class="type">class="kw">string indiname; class=class="str">"cmt">//-- class="type">int hKC[]; class="type">int hBB[]; class="type">int hParIU[], hPar05[]; class="type">int ALO, dgts, arrsar, arrsymbx; class="type">int sall, arusd, aretc, arspc, arper; class="type">ulong slip; class=class="str">"cmt">//-- class="type">class="kw">double profitb[], profits[]; class=class="str">"cmt">//-- class="type">int Buy, Sell; class="type">int ccur, psec, xtto, checktml; class="type">int OpOr[],xob[],xos[];
「EA 类里的日期字段与核心方法声明」
这段 MQL5 代码展示了一个名为 MCEA 的 EA 类内部变量与函数声明的局部结构,重点在于时间分量拆分与多周期处理。 开头用一组整型变量把时间戳拆开:year、mon、day、hour、min、sec 是基础分量,dow 表示星期(0=周日,6=周六),doy 是年内天数且 1 月 1 日记为 0。这种拆法在跨日统计或按周过滤信号时比直接读 TimeCurrent() 更直观。 紧接着声明了两个周期枚举变量 TFt 与 TFT05,说明该 EA 至少同时跟踪主周期和五分钟周期。bool 型 PanelExtra 则控制面板扩展信息的显隐。 类方法里既有构造函数 MCEA() 与析构 ~MCEA(),也有虚函数 BBOnKeltnerChannel_MCEA_Config() 和 ExpertActionTrade()——前者管布林带与肯特纳通道的参数装配,后者是交易动作入口。 其余 void 方法覆盖了品种数组重设、点值计算、报警、开仓检查、止损止盈挂载、多空平仓与全天订单统计。想验证的话,把这段贴进 MT5 的 EA 头文件,编译期若报未定义枚举,多半是漏了 #include <Trade/Trade.mqh> 或周期宏。
class=class="str">"cmt">//-- class="type">int year, class=class="str">"cmt">// Year mon, class=class="str">"cmt">// Month day, class=class="str">"cmt">// Day hour, class=class="str">"cmt">// Hour min, class=class="str">"cmt">// Minutes sec, class=class="str">"cmt">// Seconds dow, class=class="str">"cmt">// Day of week(class="num">0-Sunday, class="num">1-Monday, ... ,class="num">6-Saturday) doy; class=class="str">"cmt">// Day number of the year(January 1st is assigned the number value of zero) class=class="str">"cmt">//-- ENUM_TIMEFRAMES TFt, TFT05; class=class="str">"cmt">//-- class="type">bool PanelExtra; class=class="str">"cmt">//------------ MCEA(class="type">void); ~MCEA(class="type">void); class=class="str">"cmt">//------------ class=class="str">"cmt">//-- class="kw">virtual class="type">void BBOnKeltnerChannel_MCEA_Config(class="type">void); class="kw">virtual class="type">void ExpertActionTrade(class="type">void); class=class="str">"cmt">//-- class="type">void ArraySymbolResize(class="type">void); class="type">void CurrentSymbolSet(const class="type">class="kw">string symbol); class="type">void Pips(const class="type">class="kw">string symbol); class="type">void TradeInfo(class="type">void); class="type">void Do_Alerts(const class="type">class="kw">string symbx,class="type">class="kw">string msgText); class="type">void CheckOpenPMx(const class="type">class="kw">string symbx); class="type">void SetSLTPOrders(class="type">void); class="type">void CloseBuyPositions(const class="type">class="kw">string symbol); class="type">void CloseSellPositions(const class="type">class="kw">string symbol); class="type">void CloseAllOrders(class="type">void); class="type">void CheckClose(const class="type">class="kw">string symbx); class="type">void TodayOrders(class="type">void); class="type">void UpdatePrice(const class="type">class="kw">string symbol,ENUM_TIMEFRAMES xtf);
◍ 类接口收尾与初始化钩子
上面这段声明把 MCEA 类里剩余的对公方法一口气列完:从 RefreshPrice 按品种+周期刷新指定根数 K 线,到 RefreshTick 单品种 tick 级刷新,再到 OpenBuy / OpenSell 下单、ModifyOrderSLTP / ModifySLTP 改止损止盈、CloseAllProfit 与 ManualCloseAllProfit 平仓,以及 PairsIdxArray、ValidatePairs、GetOpenPosition 等持仓与品种校验函数,最后以 getUninitReasonText 把去初始化码翻成可读文本。 类体以 }; //-end class MCEA 收口后,EA 骨架进入 OnInit。这里只做一件事:调 mc.BBOnKeltnerChannel_MCEA_Config(),随后 return(INIT_SUCCEEDED)。配置函数内部第一行就跑 HandlingSymbolArrays() 处理所有要交易的品种,并把 TFT05 赋值为 PERIOD_M5,意味着默认观察周期是 5 分钟。 开 MT5 新建 EA 把这段贴进工程,把 TFT05 改成 PERIOD_M15 再编译,能直接验证多周期切换是否影响后续信号逻辑;外汇与贵金属杠杆品种波动剧烈,改周期后回测结果可能明显偏离,需自行承担高风险。
class="type">void RefreshPrice(const class="type">class="kw">string symbx,ENUM_TIMEFRAMES xtf,class="type">int bars); class=class="str">"cmt">//-- class="type">bool RefreshTick(const class="type">class="kw">string symbx); class="type">bool TradingToday(class="type">void); class="type">bool OpenBuy(const class="type">class="kw">string symbol); class="type">bool OpenSell(const class="type">class="kw">string symbol); class="type">bool ModifyOrderSLTP(class="type">class="kw">double mStop,class="type">class="kw">double ordtp); class="type">bool ModifySLTP(const class="type">class="kw">string symbx,class="type">int TS_type); class="type">bool CloseAllProfit(class="type">void); class="type">bool ManualCloseAllProfit(class="type">void); class=class="str">"cmt">//-- class="type">int PairsIdxArray(const class="type">class="kw">string symbol); class="type">int ValidatePairs(const class="type">class="kw">string symbol); class="type">int GetOpenPosition(const class="type">class="kw">string symbol); class="type">int GetCloseInWeakSignal(const class="type">class="kw">string symbol,class="type">int exis); class=class="str">"cmt">//-- class="type">class="kw">string getUninitReasonText(class="type">int reasonCode); class=class="str">"cmt">//-- class=class="str">"cmt">//------------ class=class="str">"cmt">//--- }; class=class="str">"cmt">//-end class MCEA class=class="str">"cmt">//---------// class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit(class="type">void) { class=class="str">"cmt">//--- mc.BBOnKeltnerChannel_MCEA_Config(); class=class="str">"cmt">//-- class="kw">return(INIT_SUCCEEDED); class=class="str">"cmt">//--- } class=class="str">"cmt">//-end OnInit() class=class="str">"cmt">//---------// class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert Configuration | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void MCEA::BBOnKeltnerChannel_MCEA_Config(class="type">void) { class=class="str">"cmt">//--- class=class="str">"cmt">//-- HandlingSymbolArrays(); class=class="str">"cmt">// With this function we will handle all pairs that will be traded class=class="str">"cmt">//-- TFT05=PERIOD_M5;
多周期与指标句柄的初始化套路
想把 Keltner Channel 和 Bollinger Bands 叠在别的品种上跑,第一步是先把时间框架数组铺开。下面这段代码把 M5 到 D1 共 11 个周期塞进 TFs[],再用 tfinuse 变量定位当前计算用的周期 TFt,避免硬编码周期导致换周期就要改源码。 指标句柄的获取放在双重循环里:对外层 arrsymbx 个交易品种,分别取 Keltner Channel 的 iCustom 句柄、以及两个 iSAR(一个用 TFt,一个固定 M5 的 TFT05)。注意 iBands 的第五个参数直接传了 hKC[x] 句柄——这是把布林带的计算基准从收盘价改成 Keltner Channel,属于「BB on KC」写法的核心。 仓位与交易环境的部分也别跳过:ALO 取账户允许挂单数和 sall 的较小值,LotPS 直接等于这个整数转型后的双精度值;mc_trade 一连串 Set 是在初始化滑点、魔术码和时区。外汇与贵金属杠杆高,这类批量挂单逻辑若 ALO 算错,可能瞬间占满账户挂单上限。 OnTick 只做一件事:调 mc.ExpertActionTrade()。真正的风控在 ExpertActionTrade 开头——用 MQLInfoInteger(MQL_TRADE_ALLOWED) 判终端是否禁了算法交易,禁了就弹一次报警并把 checktml 置 1,保证不刷屏。复制下面这段初始化逻辑到你的 EA,把 DIRI[] 和 arrsymbx 换成自己的品种表就能跑。
ENUM_TIMEFRAMES TFs[]={PERIOD_M5,PERIOD_M15,PERIOD_M30,PERIOD_H1,PERIOD_H2,PERIOD_H3,PERIOD_H4,PERIOD_H6,PERIOD_H8,PERIOD_H12,PERIOD_D1};
class="type">int arTFs=ArraySize(TFs);
class=class="str">"cmt">//--
for(class="type">int x=class="num">0; x<arTFs; x++) if(tfinuse==x) TFt=TFs[x]; class=class="str">"cmt">// TF for calculation signal
class=class="str">"cmt">//--
class=class="str">"cmt">//-- Keltner Channel and Bollinger Bands® Indicators handle for all symbol
for(class="type">int x=class="num">0; x<arrsymbx; x++)
{
hKC[x]=iCustom(DIRI[x],TFt,indiname,KCPeriod,KCMethod,KCMAAP,KCATRPer,KCATRBandsMulti); class=class="str">"cmt">//-- Handle for the Keltner Channel indicator
hParIU[x]=iSAR(DIRI[x],TFt,SARstep,SARmaxi); class=class="str">"cmt">//-- Handle for the iSAR indicator according to the selected Timeframe
hPar05[x]=iSAR(DIRI[x],TFT05,SARstep,SARmaxi); class=class="str">"cmt">//-- Handle for the iSAR indicator for M5 Timeframe
class=class="str">"cmt">//--
}
class=class="str">"cmt">//--
for(class="type">int x=class="num">0; x<arrsymbx; x++)
hBB[x]=iBands(DIRI[x],TFt,BBPeriod,class="num">0,BBDevi,hKC[x]); class=class="str">"cmt">//-- Handle for the iBands On Keltner Channel Indicator handle
class=class="str">"cmt">//--
ALO=(class="type">int)mc_account.LimitOrders()>sall ? sall : (class="type">int)mc_account.LimitOrders();
class=class="str">"cmt">//--
LotPS=(class="type">class="kw">double)ALO;
class=class="str">"cmt">//--
mc_trade.SetExpertMagicNumber(magicEA);
mc_trade.SetDeviationInPoints(slip);
mc_trade.SetMarginMode();
Set_Time_Zone();
class=class="str">"cmt">//--
class="kw">return;
class=class="str">"cmt">//---
} class=class="str">"cmt">//-end BBOnKeltnerChannel_MCEA_Config()
class=class="str">"cmt">//---------//
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick(class="type">void)
{
class=class="str">"cmt">//---
mc.ExpertActionTrade();
class=class="str">"cmt">//--
class="kw">return;
class=class="str">"cmt">//---
} class=class="str">"cmt">//-end OnTick()
class=class="str">"cmt">//---------//
class="type">void MCEA::ExpertActionTrade(class="type">void)
{
class=class="str">"cmt">//---
class=class="str">"cmt">//--Check Trading Terminal
ResetLastError();
class=class="str">"cmt">//--
if(!MQLInfoInteger(MQL_TRADE_ALLOWED) && mc.checktml==class="num">0) class=class="str">"cmt">//-- Check whether MT5 Algorithmic trading is Allow or Prohibit
{
mc.Do_Alerts(Symbol(),"Trading Expert at "+Symbol()+" are NOT Allowed by Setting.");
mc.checktml=class="num">1; class=class="str">"cmt">//-- Variable checktml is given a value of class="num">1, so that the alert is only done once.
class="kw">return;
}
class=class="str">"cmt">//--「多品种轮询里的开仓触发逻辑」
这段片段跑在 EA 的主循环里,靠秒级计数器驱动多品种扫描,而不是每个 tick 都全量跑一遍。mc.ThisTime(mc.sec) 取出当前秒数,fmod((double)mcsec,5.0)==0 意味着每 5 秒才把 mc.ccur 刷新一次,进而和上一秒 mc.psec 比较,不相等才进入品种轮询——这在跑 10 个以上外汇对时,能明显压低 CPU 占用。 轮询里先按 mc.DIRI[x] 切到对应品种,再用 mc.TradingToday() 与 mc.Trade_session() 双重过滤交易时段。过了时段闸门后,mc.GetOpenPosition(symbol) 把信号写进 OpOr[x];若返回 mc.Buy(值通常为 1),才进买入分支。 买入分支先 CheckOpenPMx 核对已有仓位,若开了 Close_by_Opps 且存在反向卖单(mc.xos[x]>0)就先平卖;只有当同品种多单数为 0(mc.xob[x]==0)且全局活跃订单低于上限 mc.ALO 时才 OpenBuy。外汇与贵金属杠杆高,这类自动轮询若 ALO 设太大,回撤可能超出预期,建议先在策略测试器用 2023 年 XAUUSD 数据跑一遍看成交密度。
if(!DisplayManualButton("M","C","R")) DisplayManualButton(); class=class="str">"cmt">//-- Show the expert manual button panel class=class="str">"cmt">//-- if(trade_info_display==Yes) mc.TradeInfo(); class=class="str">"cmt">//-- Displayed Trading Info on Chart class=class="str">"cmt">//--- class=class="str">"cmt">//-- class="type">int mcsec=mc.ThisTime(mc.sec); class=class="str">"cmt">//-- if(fmod((class="type">class="kw">double)mcsec,class="num">5.0)==class="num">0) mc.ccur=mcsec; class=class="str">"cmt">//-- if(mc.ccur!=mc.psec) { class="type">class="kw">string symbol; class=class="str">"cmt">//-- Here we start with the rotation of the name of all symbol or pairs to be traded for(class="type">int x=class="num">0; x<mc.arrsymbx && !IsStopped(); x++) { class=class="str">"cmt">//-- if(mc.DIRI[x]==Symbol()) symbol=Symbol(); else symbol=mc.DIRI[x]; class=class="str">"cmt">//-- mc.CurrentSymbolSet(symbol); class=class="str">"cmt">//-- if(mc.TradingToday() && mc.Trade_session()) { class=class="str">"cmt">//-- mc.OpOr[x]=mc.GetOpenPosition(symbol); class=class="str">"cmt">//-- Get trading signals to open positions class=class="str">"cmt">//-- //-- and store in the variable OpOr[x] if(mc.OpOr[x]==mc.Buy) class=class="str">"cmt">//-- If variable OpOr[x] get result of GetOpenPosition(symbol) as "Buy" (value=class="num">1) { class=class="str">"cmt">//-- mc.CheckOpenPMx(symbol); class=class="str">"cmt">//-- if(Close_by_Opps==Yes && mc.xos[x]>class="num">0) mc.CloseSellPositions(symbol); class=class="str">"cmt">//-- if(mc.xob[x]==class="num">0 && mc.xtto<mc.ALO) mc.OpenBuy(symbol); else if(mc.xtto>=mc.ALO) { class=class="str">"cmt">//-- mc.Do_Alerts(symbol,"Maximum amount of open positions and active pending orders has reached"+
◍ 反向反转与持仓上限的触发逻辑
这段片段处理的是持仓方向判定后的反转与限额分支。当 OpOr[x] 识别为 Sell(值 -1)时,先跑 CheckOpenPMx(symbol) 刷新该品种的市场与持仓快照,再决定下一步动作。
若开关 Close_by_Opps 为 Yes 且反向挂单数 xob[x] 大于 0,则平掉所有 Buy 持仓,避免双向暴露。若当前卖单数为 0 且总挂单加持仓 xtto 小于限额 ALO,直接 OpenSell(symbol) 开空。
当 xtto 已达或超过 ALO 时,弹警报告知“最大持仓与活跃挂单数已触顶”,限额值由 mc.ALO 决定(具体数值在外部配置,原文未给常数)。随后再次 CheckOpenPMx 校验:若反向买单 xob[x]>0 且多单浮亏 profitb[x] < -1.02(单位应为点或小数值,依账户而定)且卖单数为 0,则平多并开空;否则在 SaveOnRev==Yes 时收割全部浮盈单。外汇与贵金属杠杆高,这类反转逻辑在毛刺行情中可能连续触发,实盘前务必在 MT5 策略测试器用历史数据验证 ALO 与 -1.02 阈值的鲁棒性。
the limit = "+class="type">class="kw">string(mc.ALO)+" Orders "); class=class="str">"cmt">//-- mc.CheckOpenPMx(symbol); class=class="str">"cmt">//-- if(mc.xos[x]>class="num">0 && mc.profits[x]<-class="num">1.02 && mc.xob[x]==class="num">0) {mc.CloseSellPositions(symbol); mc.OpenBuy(symbol);} else if(SaveOnRev==Yes) mc.CloseAllProfit(); } } if(mc.OpOr[x]==mc.Sell) class=class="str">"cmt">//-- If variable OpOr[x] get result of GetOpenPosition(symbol) as "Sell" (value=-class="num">1) { class=class="str">"cmt">//-- mc.CheckOpenPMx(symbol); class=class="str">"cmt">//-- if(Close_by_Opps==Yes && mc.xob[x]>class="num">0) mc.CloseBuyPositions(symbol); class=class="str">"cmt">//-- if(mc.xos[x]==class="num">0 && mc.xtto<mc.ALO) mc.OpenSell(symbol); else if(mc.xtto>=mc.ALO) { class=class="str">"cmt">//-- mc.Do_Alerts(symbol,"Maximum amount of open positions and active pending orders has reached"+ "\n the limit = "+class="type">class="kw">string(mc.ALO)+" Orders "); class=class="str">"cmt">//-- mc.CheckOpenPMx(symbol); class=class="str">"cmt">//-- if(mc.xob[x]>class="num">0 && mc.profitb[x]<-class="num">1.02 && mc.xos[x]==class="num">0) {mc.CloseBuyPositions(symbol); mc.OpenSell(symbol);} else if(SaveOnRev==Yes) mc.CloseAllProfit();
弱信号下的利润保护逻辑
这段 EA 主循环片段展示了在 xtto 大于 0 时,如何对持仓做两层处理:先判弱信号平仓保利润,再判移动止损开关。 当 SaveOnRev 设为 Yes,且某买仓利润 mc.profitb[x] 超过 0.02、持仓数 mc.xob[x] 大于 0,同时弱信号方向从 Buy 翻为 Sell,就执行 CloseBuyPositions 并弹警报到终端。卖仓对称处理,阈值同样是 profits[x] > 0.02。 TrailingSLTP 为 Yes 时走 ModifySLTP:autotrl 为 Yes 传参 1 用自动 trailing,为 No 传参 0 用手动规则。外汇与贵金属波动剧烈,弱信号平仓只能降低回吐概率,不保证落袋。 开 MT5 把这段塞进自己的 EA 回调里,改 0.02 这个利润阈值看看平仓频率变化,比空谈风控直观。
}
}
}
}
class=class="str">"cmt">//--
mc.CheckOpenPMx(symbol);
class=class="str">"cmt">//--
if(mc.xtto>class="num">0)
{
class=class="str">"cmt">//--
if(SaveOnRev==Yes) class=class="str">"cmt">//-- Close Trade and Save profit due to weak signal(Yes)
{
mc.CheckOpenPMx(symbol);
if(mc.profitb[x]>class="num">0.02 && mc.xob[x]>class="num">0 && mc.GetCloseInWeakSignal(symbol,mc.Buy)==mc.Sell)
{
mc.CloseBuyPositions(symbol);
mc.Do_Alerts(symbol,"Close BUY order "+symbol+" to save profit due to weak signal.");
}
if(mc.profits[x]>class="num">0.02 && mc.xos[x]>class="num">0 && mc.GetCloseInWeakSignal(symbol,mc.Sell)==mc.Buy)
{
mc.CloseSellPositions(symbol);
mc.Do_Alerts(symbol,"Close SELL order "+symbol+" to save profit due to weak signal.");
}
}
class=class="str">"cmt">//--
if(TrailingSLTP==Yes) class=class="str">"cmt">//-- Use Trailing SL/TP(Yes)
{
if(autotrl==Yes) mc.ModifySLTP(symbol,class="num">1); class=class="str">"cmt">//-- If Use Automatic Trailing(Yes)
if(autotrl==No) mc.ModifySLTP(symbol,class="num">0); class=class="str">"cmt">//-- Use Automatic Trailing(No)
}
}
class=class="str">"cmt">//--
mc.CheckClose(symbol);
}
class=class="str">"cmt">//--
mc.psec=mc.ccur;
}
class=class="str">"cmt">//--
class="kw">return;
class=class="str">"cmt">//---
} class=class="str">"cmt">//-end ExpertActionTrade()
class=class="str">"cmt">//---------//
class=class="str">"cmt">//--Day Trading On/Off
class="kw">input group "=== Day Trading On/Off ==="; class=class="str">"cmt">// Day Trading On/Off「按星期与自定义时段卡交易开关」
EA 里常需要把周末流动性差、滑点失控的时段直接挡在策略外。下面这段用 7 个 YN 类型输入变量对应周日至周六,默认周日(ttd0)与周六(ttd6)为 No,其余五天为 Yes,等于只跑周一至周五的主交易窗口。 TradingToday() 把七个开关塞进 ttd[7] 数组,再用 ThisTime(dow) 取当前星期序号做下标判断。若当天对应值为 Yes,tradetoday 返回 true 且 hariini 标记 "Yes",否则不交易。 [CODE] input YN ttd0 = No; // Select Trading on Sunday (Yes) or (No) input YN ttd1 = Yes; // Select Trading on Monday (Yes) or (No) input YN ttd2 = Yes; // Select Trading on Tuesday (Yes) or (No) input YN ttd3 = Yes; // Select Trading on Wednesday (Yes) or (No) input YN ttd4 = Yes; // Select Trading on Thursday (Yes) or (No) input YN ttd5 = Yes; // Select Trading on Friday (Yes) or (No) input YN ttd6 = No; // Select Trading on Saturday (Yes) or (No) bool MCEA::TradingToday(void) { //--- bool tradetoday=false; int trdday=ThisTime(dow); hariini="No"; //-- int ttd[]; ArrayResize(ttd,7); ttd[0]=ttd0; ttd[1]=ttd1; ttd[2]=ttd2; ttd[3]=ttd3; ttd[4]=ttd4; ttd[5]=ttd5; ttd[6]=ttd6; //-- if(ttd[trdday]==Yes) {tradetoday=true; hariini="Yes";} //-- return(tradetoday); //--- } //-end TradingToday() //---------// input group "=== Trade on Specific Time ==="; // Trade on Specific Time input YN trd_time_zone = Yes; // Select If You Like to Trade on Specific Time Zone input tm_zone session = Cus_Session; // Select Trading Time Zone input swhour stsescuh = hr_00; // Time Hour to Start Trading Custom Session (0-23) input inmnt stsescum = mn_15; // Time Minute to Start Trading Custom Session (0-55) input swhour clsescuh = hr_23; // Time Hour to Stop Trading Custom Session (0-23) input inmnt clsescum = mn_55; // Time Minute to Stop Trading Custom Session (0-55) bool MCEA::Trade_session(void) { //--- bool trd_ses=false; ishour=ThisTime(hour); if(ishour!=onhour) Set_Time_Zone(); [/CODE] 紧接其后的 Trade_session() 处理更细的日内时段。输入组把自定义会话起止定为 00:15 到 23:55,基本覆盖全天但留了 20 分钟缓冲;trd_time_zone 打开后,每小时通过 ThisTime(hour) 比对 onhour,变了就重算时区偏移。 外汇与贵金属属高杠杆品种,这种按星期和钟点硬过滤的方式只能降低非活跃时段踩坑的概率,无法消除宏观事件跳空风险。开 MT5 把 ttd0~ttd6 改成全 Yes 跑一晚,能直接看出周末挂单是否被拦。
class="kw">input YN ttd0 = No; class=class="str">"cmt">// Select Trading on Sunday(Yes) or(No) class="kw">input YN ttd1 = Yes; class=class="str">"cmt">// Select Trading on Monday(Yes) or(No) class="kw">input YN ttd2 = Yes; class=class="str">"cmt">// Select Trading on Tuesday(Yes) or(No) class="kw">input YN ttd3 = Yes; class=class="str">"cmt">// Select Trading on Wednesday(Yes) or(No) class="kw">input YN ttd4 = Yes; class=class="str">"cmt">// Select Trading on Thursday(Yes) or(No) class="kw">input YN ttd5 = Yes; class=class="str">"cmt">// Select Trading on Friday(Yes) or(No) class="kw">input YN ttd6 = No; class=class="str">"cmt">// Select Trading on Saturday(Yes) or(No) class="type">bool MCEA::TradingToday(class="type">void) { class=class="str">"cmt">//--- class="type">bool tradetoday=class="kw">false; class="type">int trdday=ThisTime(dow); hariini="No"; class=class="str">"cmt">//-- class="type">int ttd[]; ArrayResize(ttd,class="num">7); ttd[class="num">0]=ttd0; ttd[class="num">1]=ttd1; ttd[class="num">2]=ttd2; ttd[class="num">3]=ttd3; ttd[class="num">4]=ttd4; ttd[class="num">5]=ttd5; ttd[class="num">6]=ttd6; class=class="str">"cmt">//-- if(ttd[trdday]==Yes) {tradetoday=true; hariini="Yes";} class=class="str">"cmt">//-- class="kw">return(tradetoday); class=class="str">"cmt">//--- } class=class="str">"cmt">//-end TradingToday() class=class="str">"cmt">//---------// class="kw">input group "=== Trade on Specific Time ==="; class=class="str">"cmt">// Trade on Specific Time class="kw">input YN trd_time_zone = Yes; class=class="str">"cmt">// Select If You Like to Trade on Specific Time Zone class="kw">input tm_zone session = Cus_Session; class=class="str">"cmt">// Select Trading Time Zone class="kw">input swhour stsescuh = hr_00; class=class="str">"cmt">// Time Hour to Start Trading Custom Session(class="num">0-class="num">23) class="kw">input inmnt stsescum = mn_15; class=class="str">"cmt">// Time Minute to Start Trading Custom Session(class="num">0-class="num">55) class="kw">input swhour clsescuh = hr_23; class=class="str">"cmt">// Time Hour to Stop Trading Custom Session(class="num">0-class="num">23) class="kw">input inmnt clsescum = mn_55; class=class="str">"cmt">// Time Minute to Stop Trading Custom Session(class="num">0-class="num">55) class="type">bool MCEA::Trade_session(class="type">void) { class=class="str">"cmt">//--- class="type">bool trd_ses=class="kw">false; ishour=ThisTime(hour); if(ishour!=onhour) Set_Time_Zone();
◍ 用时间窗和布林贴轨卡掉无效信号
EA 里过滤噪音的第一道闸是交易时段。下面这段把服务器时间 tcurr 跟各市场开收市边界做比对,命中就把 trd_ses 置真,不在框内就不动。 datetime tcurr=TimeCurrent(); // 取 MT5 服务器时间,不是本地电脑时钟 switch(session) { case Cus_Session: { if(tcurr>=SesCuOp && tcurr<=SesCuCl) trd_ses=true; // 自定义时段边界内才放行 break; } case New_Zealand: { if(tcurr>=Ses01Op && tcurr<=Ses01Cl) trd_ses=true; // 新西兰盘 break; } case Australia: { if(tcurr>=Ses02Op && tcurr<=Ses02Cl) trd_ses=true; // 澳盘 break; } case Asia_Tokyo: { if(tcurr>=Ses03Op && tcurr<=Ses03Cl) trd_ses=true; // 东京盘 break; } case Europe_London: { if(tcurr>=Ses04Op && tcurr<=Ses04Cl) trd_ses=true; // 伦敦盘 break; } case US_New_York: { if(tcurr>=Ses05Op && tcurr<=Ses05Cl) trd_ses=true; // 纽约盘 break; } } if(trd_time_zone==No) { if(tcurr>=SesNoOp && tcurr<=SesNoCl) trd_ses=true; // 关闭分区限制时的全局窗口 } onhour=ishour; return(trd_ses); 若你关掉分区(trd_time_zone==No),代码会退回到 SesNoOp~SesNoCl 这一个总窗口,适合做跨市场连续品种如 XAUUSD,但外汇高波动时段踩错点差可能扩大亏损。 信号侧另有一层滤网:GetOpenPosition 调 BBOnKeltnerChannel,让布林带和 Keltner 通道叠加。函数里 difud 取 1.5 倍点值(NormalizePrice(1.5*pip)),作为带宽容差,br 写死为 3 表示看第 3 根缓冲。 int MCEA::GetOpenPosition(const string symbol) { int ret=0; int rise=1, down=-1; // rise 表多头触发,down 表空头 int BBOnKC=BBOnKeltnerChannel(symbol); // 取布林贴轨判定 if(BBOnKC==rise) ret=rise; // 上贴轨返回 1 if(BBOnKC==down) ret=down; // 下贴轨返回 -1 return(ret); } int MCEA::BBOnKeltnerChannel(const string symbol) { int ret=0; int rise=1, down=-1; int br=3; // 指示缓冲索引 Pips(symbol); // 按品种算 pip double difud=mc_symbol.NormalizePrice(1.5*pip); // 1.5 倍点值容差 double KCmb[], // Keltner 中轨缓冲
class="type">class="kw">datetime tcurr=TimeCurrent(); class=class="str">"cmt">// Server Time class=class="str">"cmt">//-- class="kw">switch(session) { case Cus_Session: { if(tcurr>=SesCuOp && tcurr<=SesCuCl) trd_ses=true; break; } case New_Zealand: { if(tcurr>=Ses01Op && tcurr<=Ses01Cl) trd_ses=true; break; } case Australia: { if(tcurr>=Ses02Op && tcurr<=Ses02Cl) trd_ses=true; break; } case Asia_Tokyo: { if(tcurr>=Ses03Op && tcurr<=Ses03Cl) trd_ses=true; break; } case Europe_London: { if(tcurr>=Ses04Op && tcurr<=Ses04Cl) trd_ses=true; break; } case US_New_York: { if(tcurr>=Ses05Op && tcurr<=Ses05Cl) trd_ses=true; break; } } class=class="str">"cmt">//-- if(trd_time_zone==No) { if(tcurr>=SesNoOp && tcurr<=SesNoCl) trd_ses=true; } class=class="str">"cmt">//-- onhour=ishour; class=class="str">"cmt">//-- class="kw">return(trd_ses); class=class="str">"cmt">//--- } class=class="str">"cmt">//-end Trade_session() class=class="str">"cmt">//---------// class="type">int MCEA::GetOpenPosition(const class="type">class="kw">string symbol) class=class="str">"cmt">// Signal Open Position { 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 BBOnKC=BBOnKeltnerChannel(symbol); class=class="str">"cmt">//-- if(BBOnKC==rise) ret=rise; if(BBOnKC==down) ret=down; class=class="str">"cmt">//-- class="kw">return(ret); class=class="str">"cmt">//--- } class=class="str">"cmt">//-end GetOpenPosition() class=class="str">"cmt">//---------// class="type">int MCEA::BBOnKeltnerChannel(const class="type">class="kw">string symbol) class=class="str">"cmt">// Fungction of Bollinger Bands® On Keltner Channel Indicator { class=class="str">"cmt">//--- class="type">int ret=class="num">0; class="type">int rise=class="num">1, down=-class="num">1; 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 KCmb[], class=class="str">"cmt">// Destination array for Keltner Channel Middle Line buffer
肯特纳与布林带中轨的穿越判定
把肯特纳通道(KC)和布林带(BB)的缓冲数组一次性拉进本地,是做跨通道比价的先决步骤。下面这段逻辑先给六个目标数组按 br 长度扩容,再用 ArraySetAsSeries 把索引倒序,保证 [0] 永远是当前柱、[1] 是上一柱,避免和指标原生序列错位。 KCmb[] // 肯特纳通道中轨缓冲区 KCub[] // 肯特纳通道上轨缓冲区 KClb[] // 肯特纳通道下轨缓冲区 BBmb[] // 布林带基线(中轨)缓冲区 BBub[] // 布林带上轨缓冲区 BBlb[] // 布林带下轨缓冲区 ArrayResize(KCmb,br,br); ArrayResize(KCub,br,br); ArrayResize(KClb,br,br); ArrayResize(BBmb,br,br); ArrayResize(BBub,br,br); ArrayResize(BBlb,br,br); ArraySetAsSeries(KCmb,true); ArraySetAsSeries(KCub,true); ArraySetAsSeries(KClb,true); ArraySetAsSeries(BBmb,true); ArraySetAsSeries(BBub,true); ArraySetAsSeries(BBlb,true); 拿到句柄后通过 CopyBuffer 把三个 KC 缓冲(0/1/2)和三个 BB 缓冲(0/1/2)从对应品种索引 xx 拷进来,xx 由 PairsIdxArray(symbol) 在符号数组 DIRI[] 里线性匹配得到,没匹配到返回 -1。 int xx=PairsIdxArray(symbol); CopyBuffer(hKC[xx],0,0,br,KCmb); CopyBuffer(hKC[xx],1,0,br,KCub); CopyBuffer(hKC[xx],2,0,br,KClb); CopyBuffer(hBB[xx],0,0,br,BBmb); CopyBuffer(hBB[xx],1,0,br,BBub); CopyBuffer(hBB[xx],2,0,br,BBlb); 核心信号只看 KC 中轨对 BB 三线(下/中/上)的向上穿刺:上一柱 KCmb 还在 BB 某线之下,这一柱越过该线并多出 difud 阈值,就记 rise;反向对称地向下击穿则记 down。difud 一般设为点差量级的小数(如 0.0001~0.0003 于 EURUSD),用来滤掉假穿透。 bool BBKCrise1=(KCmb[1]<=BBlb[1] && KCmb[0]>BBlb[0]+difud); bool BBKCrise2=(KCmb[1]<=BBmb[1] && KCmb[0]>BBmb[0]+difud); bool BBKCrise3=(KCmb[1]<=BBub[1] && KCmb[0]>BBub[0]+difud); bool BBKCdown1=(KCmb[1]>=BBub[1] && KCmb[0]<BBub[0]-difud); bool BBKCdown2=(KCmb[1]>=BBmb[1] && KCmb[0]<BBmb[0]-difud); bool BBKCdown3=(KCmb[1]>=BBlb[1] && KCmb[0]<BBlb[0]-difud);
| if(BBKCrise1 | BBKCrise2 | BBKCrise3) ret=rise; |
|---|---|---|
| if(BBKCdown1 | BBKCdown2 | BBKCdown3) ret=down; |
外汇与贵金属波动剧烈,这类通道穿越只提示概率倾向,实盘前请在 MT5 用策略测试器对 difud 做参数扫描,确认样本内外表现再上真仓。
KCub[], class=class="str">"cmt">// Destination array for Keltner Channel Upper Band buffer KClb[]; class=class="str">"cmt">// Destination array for Keltner Channel Lower Band buffer class="type">class="kw">double BBmb[], class=class="str">"cmt">// Destination array for Bollinger Bands® BASE_LINE buffer BBub[], class=class="str">"cmt">// Destination array for Bollinger Bands® UPPER_BAND buffer BBlb[]; class=class="str">"cmt">// Destination array for Bollinger Bands® LOWER_BAND buffer class=class="str">"cmt">//-- ArrayResize(KCmb,br,br); ArrayResize(KCub,br,br); ArrayResize(KClb,br,br); ArrayResize(BBmb,br,br); ArrayResize(BBub,br,br); ArrayResize(BBlb,br,br); ArraySetAsSeries(KCmb,true); ArraySetAsSeries(KCub,true); ArraySetAsSeries(KClb,true); ArraySetAsSeries(BBmb,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">0,class="num">0,br,KCmb); 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">0,class="num">0,br,BBmb); 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">bool BBKCrise1=(KCmb[class="num">1]<=BBlb[class="num">1] && KCmb[class="num">0]>BBlb[class="num">0]+difud); class="type">bool BBKCrise2=(KCmb[class="num">1]<=BBmb[class="num">1] && KCmb[class="num">0]>BBmb[class="num">0]+difud); class="type">bool BBKCrise3=(KCmb[class="num">1]<=BBub[class="num">1] && KCmb[class="num">0]>BBub[class="num">0]+difud); class=class="str">"cmt">//-- class="type">bool BBKCdown1=(KCmb[class="num">1]>=BBub[class="num">1] && KCmb[class="num">0]<BBub[class="num">0]-difud); class="type">bool BBKCdown2=(KCmb[class="num">1]>=BBmb[class="num">1] && KCmb[class="num">0]<BBmb[class="num">0]-difud); class="type">bool BBKCdown3=(KCmb[class="num">1]>=BBlb[class="num">1] && KCmb[class="num">0]<BBlb[class="num">0]-difud); class=class="str">"cmt">//-- if(BBKCrise1 || BBKCrise2 || BBKCrise3) ret=rise; if(BBKCdown1 || BBKCdown2 || BBKCdown3) ret=down; class=class="str">"cmt">//-- class="kw">return(ret); class=class="str">"cmt">//--- } class=class="str">"cmt">//-end BBOnKeltnerChannel() class=class="str">"cmt">//---------// class="type">int xx=PairsIdxArray(symbol); class="type">int MCEA::PairsIdxArray(const class="type">class="kw">string symbol) { class=class="str">"cmt">//--- class="type">int pidx=-class="num">1; class=class="str">"cmt">//-- for(class="type">int x=class="num">0; x<arrsymbx; x++) { if(DIRI[x]==symbol) { pidx=x;
「初始化里先把缓冲区和句柄绑死」
Keltner Channel 自定义指标在 OnInit 里第一件事是把 4 个数组挂到缓冲:0/1/2 号是 INDICATOR_DATA(中轨、上轨、下轨),3 号 KC_ATRtemp 是 INDICATOR_CALCULATIONS,只参与计算不画图。 紧接着用 iMA 建 EMA 句柄,周期取当前品种 Period(),均线周期参数为 period_kc、偏移 0、模式 MODE_EMA、价格源 ma_price;若返回 INVALID_HANDLE 就 PrintFormat 报错并 return(INIT_FAILED),指标直接中止。ATR 句柄同理,iATR 用 atr_period,失败也走 INIT_FAILED。 两个句柄都活下来后,用 StringFormat 拼出短名如 "Keltner Channel(20, MODE_EMA, 2.00)",交给 IndicatorSetString 显示,并依 Digits() 设小数点位数,最后 return(INIT_SUCCEEDED)。 主体计算完之后,对外取数靠 CopyBuffer:hKC 的 0/1/2 分别拉中轨、上轨、下轨到 KCmb/KCub/KClb;hBB 的 0/1 拉布林中轨和上轨到 BBmb/BBub,br 为请求柱数。开 MT5 把这段接进 EA,就能并行比 KC 与 BB 的带宽差异。
break; } } class=class="str">"cmt">//-- class="kw">return(pidx); class=class="str">"cmt">//--- } class=class="str">"cmt">//-end PairsIdxArray() class=class="str">"cmt">//---------// class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Custom indicator initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- indicator buffers mapping class=class="str">"cmt">//-- assignment of array to indicator buffer SetIndexBuffer(class="num">0,KC_Middle,INDICATOR_DATA); SetIndexBuffer(class="num">1,KC_UpperB,INDICATOR_DATA); SetIndexBuffer(class="num">2,KC_LowerB,INDICATOR_DATA); SetIndexBuffer(class="num">3,KC_ATRtemp,INDICATOR_CALCULATIONS); class=class="str">"cmt">//-- handleMA=iMA(Symbol(),Period(),period_kc,class="num">0,MODE_EMA,ma_price); class=class="str">"cmt">//-- if(handleMA==INVALID_HANDLE) { class=class="str">"cmt">//--- tell about the failure and output the error code PrintFormat("Failed to create handle of the Moving Average indicator for the symbol %s/%s, error code %d", Symbol(), EnumToString(Period()), GetLastError()); class=class="str">"cmt">//--- the indicator is stopped early class="kw">return(INIT_FAILED); } class=class="str">"cmt">//-- handleATR=iATR(Symbol(),Period(),atr_period); class=class="str">"cmt">//-- if(handleATR==INVALID_HANDLE) { class=class="str">"cmt">//--- tell about the failure and output the error code PrintFormat("Failed to create handle of the ATR indicator for the symbol %s/%s, error code %d", Symbol(), EnumToString(Period()), GetLastError()); class=class="str">"cmt">//--- the indicator is stopped early class="kw">return(INIT_FAILED); } class=class="str">"cmt">//-- short_name=StringFormat("Keltner Channel(%d, %s, %.2f)",period_kc,EnumToString(ma_method),band_multi); IndicatorSetString(INDICATOR_SHORTNAME,short_name); IndicatorSetInteger(INDICATOR_DIGITS,Digits()); class=class="str">"cmt">//-- class="kw">return(INIT_SUCCEEDED); class=class="str">"cmt">//--- } class=class="str">"cmt">//---------// CopyBuffer(hKC[xx],class="num">0,class="num">0,br,KCmb); 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">0,class="num">0,br,BBmb); CopyBuffer(hBB[xx],class="num">1,class="num">0,br,BBub);
◍ 用类方法封装多品种下单逻辑
在 EA 里同时盯好几个品种时,把开仓动作收进 MCEA 类的 OpenBuy / OpenSell 方法,比在 OnTick 里堆散代码更不容易出错。下面这段实现把重置错误码、清结构体、算手数、设止损止盈、发单和回报都包在了一起,复制进你的 .mqh 就能直接调。 外汇与贵金属保证金交易杠杆高,实盘跑这段前先在策略测试器用 0.01 手验证返回码,避免滑点导致的 RETCODE 异常把日志刷爆。 OpenBuy 里 SL 用 Bid 算、TP 用 Ask 算;OpenSell 正好反过来,SL 取 Ask、TP 取 Bid。这个对称写法能防止把买卖止损方向写反——这是多品种 EA 最常见的静默 bug。 下单前 RefreshTick(symbol) 必须返回 true 才动单,否则旧报价可能让 mc_trade.Buy 在跨品种切换时吃到过期 tick。失败分支里用 CheckResult 把柜台拒绝原因捞出来弹 alert,比只看 GetLastError 更直观。
class="type">bool MCEA::OpenBuy(const class="type">class="kw">string symbol) { class=class="str">"cmt">//--- ResetLastError(); class=class="str">"cmt">//-- class="type">bool buyopen = class="kw">false; class="type">class="kw">string ldComm = GetCommentForOrder()+"_Buy"; class="type">class="kw">double ldLot = MLots(symbol); ENUM_ORDER_TYPE type_req = ORDER_TYPE_BUY; class=class="str">"cmt">//-- class="type">MqlTradeRequest req={}; class="type">MqlTradeResult res={}; MqlTradeCheckResult check={}; class=class="str">"cmt">//-- structure is set to zero ZeroMemory(req); ZeroMemory(res); ZeroMemory(check); class=class="str">"cmt">//-- CurrentSymbolSet(symbol); class="type">class="kw">double SL=OrderSLSet(symbol,type_req,mc_symbol.Bid()); class="type">class="kw">double TP=OrderTPSet(symbol,type_req,mc_symbol.Ask()); class=class="str">"cmt">//-- if(RefreshTick(symbol)) buyopen=mc_trade.Buy(ldLot,symbol,mc_symbol.Ask(),SL,TP,ldComm); class=class="str">"cmt">//-- class="type">int error=GetLastError(); if(buyopen||error==class="num">0) { class="type">class="kw">string bsopen="Open BUY Order for "+symbol+" ~ Ticket= ["+(class="type">class="kw">string)mc_trade.ResultOrder()+"] successfully..!"; Do_Alerts(symbol,bsopen); } else { mc_trade.CheckResult(check); Do_Alerts(Symbol(),"Open BUY order for "+symbol+" FAILED!!. Return code= "+ (class="type">class="kw">string)mc_trade.ResultRetcode()+". Code description: ["+mc_trade.ResultRetcodeDescription()+"]"); class="kw">return(class="kw">false); } class=class="str">"cmt">//-- class="kw">return(buyopen); class=class="str">"cmt">//-- class=class="str">"cmt">//--- } class=class="str">"cmt">//-end OpenBuy class=class="str">"cmt">//---------// class="type">bool MCEA::OpenSell(const class="type">class="kw">string symbol) { class=class="str">"cmt">//--- ResetLastError(); class=class="str">"cmt">//-- class="type">bool selopen = class="kw">false; class="type">class="kw">string sdComm = GetCommentForOrder()+"_Sell"; class="type">class="kw">double sdLot = MLots(symbol); ENUM_ORDER_TYPE type_req = ORDER_TYPE_SELL; class=class="str">"cmt">//-- class="type">MqlTradeRequest req={}; class="type">MqlTradeResult res={}; MqlTradeCheckResult check={}; class=class="str">"cmt">//-- structure is set to zero ZeroMemory(req); ZeroMemory(res); ZeroMemory(check); class=class="str">"cmt">//-- CurrentSymbolSet(symbol); class="type">class="kw">double SL=OrderSLSet(symbol,type_req,mc_symbol.Ask()); class="type">class="kw">double TP=OrderTPSet(symbol,type_req,mc_symbol.Bid()); class=class="str">"cmt">//-- if(RefreshTick(symbol)) selopen=mc_trade.Sell(sdLot,symbol,mc_symbol.Bid(),SL,TP,sdComm);
卖单回执与图表按钮的事件分支
开卖单后先抓错误码,决定走成功提示还是失败返 false。error==0 或 selopen 为真时拼出含订单 ticket 的字符串并弹警报告知;否则调用 mc_trade.CheckResult 把回码和描述抛出来,直接 return(false) 终止流程。 OnChartEvent 里只认 CHARTEVENT_OBJECT_CLICK:sparam 等于 "Set SL/TP All Orders" 就跑 mc.SetSLTPOrders() 批量挂止损止盈,顺手把按钮 OBJPROP_STATE 置 false、ZORDER 归 0 并重绘面板。 sparam 等于 "Close All Order" 则调 mc.CloseAllOrders() 一把平掉当前品种所有持仓,Alert 里带 mc.expname 方便多 EA 同图时区分来源。外汇与贵金属波动剧烈,这类一键全平按钮误触可能造成跳空滑点,实盘前建议在策略测试器里先点一遍验证对象名拼写。
class=class="str">"cmt">//-- class="type">int error=GetLastError(); if(selopen||error==class="num">0) { class="type">class="kw">string bsopen="Open SELL Order for "+symbol+" ~ Ticket= ["+(class="type">class="kw">string)mc_trade.ResultOrder()+"] successfully..!"; Do_Alerts(symbol,bsopen); } else { mc_trade.CheckResult(check); Do_Alerts(Symbol(),"Open SELL order for "+symbol+" FAILED!!. Return code= "+ (class="type">class="kw">string)mc_trade.ResultRetcode()+". Code description: ["+mc_trade.ResultRetcodeDescription()+"]"); class="kw">return(class="kw">false); } class=class="str">"cmt">//-- class="kw">return(selopen); class=class="str">"cmt">//-- class=class="str">"cmt">//--- } class=class="str">"cmt">//-end OpenSell class=class="str">"cmt">//---------// class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- class=class="str">"cmt">//--- handling CHARTEVENT_CLICK event("Clicking the chart") ResetLastError(); class=class="str">"cmt">//-- ENUM_TIMEFRAMES CCS=mc.TFt; class=class="str">"cmt">//-- if(id==CHARTEVENT_OBJECT_CLICK) { class="type">int lensymbol=StringLen(Symbol()); class="type">int lensparam=StringLen(sparam); class=class="str">"cmt">//-- class=class="str">"cmt">//--- if "Set SL All Orders" button is click if(sparam=="Set SL/TP All Orders") { mc.SetSLTPOrders(); Alert("-- "+mc.expname+" -- ",Symbol()," -- Set SL/TP All Orders"); class=class="str">"cmt">//--- unpress the button ObjectSetInteger(class="num">0,"Set SL/TP All Orders",OBJPROP_STATE,class="kw">false); ObjectSetInteger(class="num">0,"Set SL/TP All Orders",OBJPROP_ZORDER,class="num">0); CreateManualPanel(); } class=class="str">"cmt">//--- if "Close All Order" button is click if(sparam=="Close All Order") { mc.CloseAllOrders(); Alert("-- "+mc.expname+" -- ",Symbol()," -- Close All Orders");
「手动平仓面板的按钮事件处理」
在 MT5 的 EA 图表面板里,每个按钮点击后都会触发 OnChartEvent 回调,靠 sparam 传回按钮名。上面这段就是这个回调里处理「平仓类」和「面板切换类」按钮的核心分支。
以 Close All Order 为例:先调 ManualCloseAll() 平掉所有单,再用 ObjectSetInteger 把按钮的 OBJPROP_STATE 设回 false(弹起态),并把 OBJPROP_ZORDER 归 0,最后 CreateManualPanel() 重绘面板。外汇与贵金属杠杆高,一键全平可能在区间毛刺里吃掉可观浮动盈亏,实盘前务必在策略测试器用历史数据跑一遍。
Close All Profit 分支逻辑类似,只是改调 ManualCloseAllProfit(),并弹 Alert 提示品种与动作;X 按钮则 ObjectsDeleteAll 清掉 OBJ_BUTTON / OBJ_LABEL / OBJ_RECTANGLE_LABEL 三类图形,再收起扩展面板(PanelExtra=false)。
M 与 C 按钮只做面板状态切换:M 设 PanelExtra=true 展开手动面板,C 仅弹起自身等待后续逻辑——两者都不直接动持仓,只是改 UI 层的可见性。
class=class="str">"cmt">//--- unpress the button ObjectSetInteger(class="num">0,"Close All Order",OBJPROP_STATE,class="kw">false); ObjectSetInteger(class="num">0,"Close All Order",OBJPROP_ZORDER,class="num">0); CreateManualPanel(); } class=class="str">"cmt">//--- if "Close All Profit" button is click if(sparam=="Close All Profit") { mc.ManualCloseAllProfit(); Alert("-- "+mc.expname+" -- ",Symbol()," -- Close All Profit"); class=class="str">"cmt">//--- unpress the button ObjectSetInteger(class="num">0,"Close All Profit",OBJPROP_STATE,class="kw">false); ObjectSetInteger(class="num">0,"Close All Profit",OBJPROP_ZORDER,class="num">0); CreateManualPanel(); } class=class="str">"cmt">//--- if "X" button is click if(sparam=="X") { ObjectsDeleteAll(class="num">0,class="num">0,OBJ_BUTTON); ObjectsDeleteAll(class="num">0,class="num">0,OBJ_LABEL); ObjectsDeleteAll(class="num">0,class="num">0,OBJ_RECTANGLE_LABEL); class=class="str">"cmt">//--- unpress the button ObjectSetInteger(class="num">0,"X",OBJPROP_STATE,class="kw">false); ObjectSetInteger(class="num">0,"X",OBJPROP_ZORDER,class="num">0); class=class="str">"cmt">//-- DeleteButtonX(); mc.PanelExtra=class="kw">false; DisplayManualButton(); } class=class="str">"cmt">//--- if "M" button is click if(sparam=="M") { class=class="str">"cmt">//--- unpress the button ObjectSetInteger(class="num">0,"M",OBJPROP_STATE,class="kw">false); ObjectSetInteger(class="num">0,"M",OBJPROP_ZORDER,class="num">0); mc.PanelExtra=true; CreateManualPanel(); } class=class="str">"cmt">//--- if "C" button is click if(sparam=="C") { class=class="str">"cmt">//--- unpress the button ObjectSetInteger(class="num">0,"C",OBJPROP_STATE,class="kw">false);
◍ 图表按钮事件与跨日时段定位的实现细节
在 MT5 的 OnChartEvent 里,用 sparam 区分被点击的面板按钮是最直接的做法。当 sparam=="R" 时,先弹 Alert 提示 EA 即将从图表移除,随后调用 ExpertRemove() 卸载自身,并把 "R" 按钮的 OBJPROP_STATE 设回 false、ZORDER 归零,避免按钮卡在按下态。 紧接着用 ChartSetSymbolPeriod(0,Symbol(),Period()) 强制刷新当前图表,若首次调用返回 false 就再调一次;然后 DeletePanelButton() 清掉面板、ChartRedraw(0) 重绘。这一串顺序不能乱,否则按钮残影可能留在图表上。 点击交易品种按钮时,lensparam==lensymbol 成立,先以 mc.ValidatePairs(sparam) 拿到品种下标 sx,再用 ChangeChartSymbol(mc.AS30[sx],CCS) 切换图表符号,并把 mc.PanelExtra 置 false 收起扩展面板。 PosTimeZone() 负责把时段落在「今天」还是「次日」算清楚。核心判断是 ReqTime(zntm,day)>ThisTime(day) 时,tzpos 拼成 "... Next day to ... Next day";若 TimeCurrent()<znop 且 ThisTime 与 znop、zncl 的 ReqTime 相等,则标为 "Today"。外汇与贵金属杠杆高,这类时段切换逻辑最好在模拟盘先验证再上实盘。
ObjectSetInteger(class="num">0,"C",OBJPROP_ZORDER,class="num">0); mc.PanelExtra=true; CreateSymbolPanel(); } class=class="str">"cmt">//--- if "R" button is click if(sparam=="R") { Alert("-- "+mc.expname+" -- ",Symbol()," -- Expert Advisor will be Remove from the chart."); ExpertRemove(); class=class="str">"cmt">//--- unpress the button ObjectSetInteger(class="num">0,"R",OBJPROP_STATE,class="kw">false); ObjectSetInteger(class="num">0,"R",OBJPROP_ZORDER,class="num">0); if(!ChartSetSymbolPeriod(class="num">0,Symbol(),Period())) ChartSetSymbolPeriod(class="num">0,Symbol(),Period()); DeletePanelButton(); ChartRedraw(class="num">0); } class=class="str">"cmt">//--- if Symbol button is click if(lensparam==lensymbol) { class="type">int sx=mc.ValidatePairs(sparam); ChangeChartSymbol(mc.AS30[sx],CCS); mc.PanelExtra=class="kw">false; } class=class="str">"cmt">//-- } class=class="str">"cmt">//-- class="kw">return; class=class="str">"cmt">//--- } class=class="str">"cmt">//-end OnChartEvent() class=class="str">"cmt">//---------// class="type">class="kw">string MCEA::PosTimeZone(class="type">void) { class=class="str">"cmt">//--- class="type">class="kw">string tzpos=""; class=class="str">"cmt">//-- if(ReqTime(zntm,day)>ThisTime(day)) { tzpos=tz_opn+ " Next day to " +tz_cls + " Next day"; } else if(TimeCurrent()<znop) { if(ThisTime(day)==ReqTime(znop,day) && ThisTime(day)==ReqTime(zncl,day)) tzpos=tz_opn+" to " +tz_cls+ " Today"; class=class="str">"cmt">//else if(ThisTime(day)==ReqTime(znop,day) && ThisTime(day)<ReqTime(zncl,day)) tzpos=tz_opn+ " Today to " +tz_cls+ " Next day"; } else if(TimeCurrent()>=znop && TimeCurrent()<zncl) { if(ThisTime(day)<ReqTime(zncl,day)) tzpos=tz_opn+ " Today to " +tz_cls+ " Next day"; else if(ThisTime(day)==ReqTime(zncl,day)) tzpos=tz_opn+" to "+tz_cls+ " Today"; } else
把账户与时段状态写进图表注释
EA 的 TradeInfo() 负责在图表上输出一段可读的 HUD 文本,把服务器时间、经纪商、账户模式、杠杆、权益和当日交易窗口一次性打印出来。调用 Pips(Symbol()) 后,用 SymbolInfoInteger(Symbol(),SYMBOL_SPREAD)/xpip 算出以点数衡量的实时点差,外汇与贵金属点差随流动性跳动,属于典型高风险变量,读数只反映当下。 PosTimeZone() 返回当前所属时区标签,例如当 ThisTime(day)==ReqTime(znop,day) 且早于收盘时间时,tzpos 被拼成「今日开仓到次日平仓」的字符串。若 TimeCurrent() 还在 zntm 之前,注释尾部追加剩余时:分:秒并标记 Waiting for active time,方便你直观判断 EA 是否卡在静默期。 这段注释不触发任何下单,仅作盯盘辅助;你在 MT5 里挂上 EA 后,直接看图表左上角文本就能核对魔法数、交易周期 TFt 与 Today Trading 字段是否和预期一致。
if(ThisTime(day)==ReqTime(znop,day) && ThisTime(day)<ReqTime(zncl,day)) { tzpos=tz_opn+" Today to " +tz_cls+ " Next day"; } class=class="str">"cmt">//-- class="kw">return(tzpos); class=class="str">"cmt">//---- } class=class="str">"cmt">//-end PosTimeZone() class=class="str">"cmt">//---------// class="type">void MCEA::TradeInfo(class="type">void) class=class="str">"cmt">// function: write comments on the chart { class=class="str">"cmt">//---- Pips(Symbol()); class="type">class="kw">double spread=SymbolInfoInteger(Symbol(),SYMBOL_SPREAD)/xpip; rem=zntm-TimeCurrent(); class="type">class="kw">string postime=PosTimeZone(); class="type">class="kw">string eawait=" - Waiting for active time..!"; class=class="str">"cmt">//-- class="type">class="kw">string comm=""; TodayOrders(); class=class="str">"cmt">//-- comm="\n :: Server Date Time : "+class="type">class="kw">string(ThisTime(year))+"."+class="type">class="kw">string(ThisTime(mon))+"."+class="type">class="kw">string(ThisTime(day))+ " "+TimeToString(TimeCurrent(),TIME_SECONDS)+ "\n ------------------------------------------------------------"+ "\n :: Broker : "+ TerminalInfoString(TERMINAL_COMPANY)+ "\n :: Expert Name : "+ expname+ "\n :: Acc. Name : "+ mc_account.Name()+ "\n :: Acc. Number : "+ (class="type">class="kw">string)mc_account.Login()+ "\n :: Acc. TradeMode : "+ AccountMode()+ "\n :: Acc. Leverage : class="num">1 : "+ (class="type">class="kw">string)mc_account.Leverage()+ "\n :: Acc. Equity : "+ DoubleToString(mc_account.Equity(),class="num">2)+ "\n :: Margin Mode : "+ (class="type">class="kw">string)mc_account.MarginModeDescription()+ "\n :: Magic Number : "+ class="type">class="kw">string(magicEA)+ "\n :: Trade on TF : "+ EnumToString(TFt)+ "\n :: Today Trading : "+ TradingDay()+" : "+hariini+ "\n :: Trading Session : "+ tz_ses+ "\n :: Trading Time : "+ postime; if(TimeCurrent()<zntm) { comm=comm+ "\n :: Time Remaining : "+(class="type">class="kw">string)ReqTime(rem,hour)+":"+(class="type">class="kw">string)ReqTime(rem,min)+":"+(class="type">class="kw">string)ReqTime(rem,sec) + eawait; } comm=comm+
「正文」
<span class="string">"\n ------------------------------------------------------------"</span>+ <span class="string">"\n :: Trading Pairs : "</span>+pairs+ <span class="string">"\n :: BUY Market : "</span>+<span class="keyword">string</span>(oBm)+ <span class="string">"\n :: SELL Market : "</span>+<span class="keyword">string</span>(oSm)+ <span class="string">"\n :: Total Order : "</span>+<span class="keyword">string</span>(oBm+oSm)+ <span class="string">"\n :: Order Profit : "</span>+<span class="functions">DoubleToString</span>(floatprofit,<span class="number">2</span>)+ <span class="string">"\n :: Fixed Profit : "</span>+<span class="functions">DoubleToString</spa
◍ 空止损持仓的兜底挂单逻辑
| 这段逻辑处理的是持仓 SL 或 TP 为 0.0 时的补设场景,买卖双向分开走。当 `pos_stop==0.0 | pos_take==0.0` 成立,说明该持仓此前没挂止损止盈,脚本会尝试用 `PositionModify` 把算好的 `modbuysl/modbuytp` 或 `modselsl/modseltp` 写进去。 |
|---|
卖出侧先取当前价、开仓价与已有止损止盈,再调用 SetOrderSL / SetOrderTP 按开仓价推算目标位。若实时 price>modselsl 则把止损外推到 price+slip*pip 并归一化,防止挂单穿价被拒;price<modseltp 同理内推到 price-slip*pip。
修改失败时不静默退出,而是走 CheckResult 并弹 Do_Alerts,把 ResultRetcode() 和 ResultRetcodeDescription() 直接报出来。外汇与贵金属杠杆高,这类补挂失败多发生在点差突扩时段,看到返回码应先查服务器拒绝原因而非盲目重发。
开 MT5 把这段塞进自己的 EA 调试,故意开一个 0 止损的单,看 alert 里回码是不是 10019(无效止损)一类,就能验证滑点外推是否真的避开了报错。
if(pos_stop==class="num">0.0 || pos_take==class="num">0.0) { if(!mc_trade.PositionModify(position_symbol,modbuysl,modbuytp)) { mc_trade.CheckResult(check); Do_Alerts(symbol,"Set SL and TP for "+EnumToString(opstype)+" on "+symbol+" FAILED!!. Return code= "+ (class="type">class="kw">string)mc_trade.ResultRetcode()+". Code description: ["+mc_trade.ResultRetcodeDescription()+"]"); } } if(opstype==POSITION_TYPE_SELL) { Pips(symbol); RefreshTick(symbol); class="type">class="kw">double price = mc_position.PriceCurrent(); 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_take = mc_position.TakeProfit(); modselsl=SetOrderSL(symbol,opstype,pos_open); if(price>modselsl) modselsl=mc_symbol.NormalizePrice(price+slip*pip); modseltp=SetOrderTP(symbol,opstype,pos_open); if(price<modseltp) modseltp=mc_symbol.NormalizePrice(price-slip*pip); if(pos_stop==class="num">0.0 || pos_take==class="num">0.0) { if(!mc_trade.PositionModify(position_symbol,modselsl,modseltp)) { mc_trade.CheckResult(check); Do_Alerts(symbol,"Set SL and TP for "+EnumToString(opstype)+" on "+symbol+" FAILED!!. Return code= "+ (class="type">class="kw">string)mc_trade.ResultRetcode()+". Code description: ["+mc_trade.ResultRetcodeDescription()+"]"); } } }
按魔术码与浮盈批量平仓的实现细节
多币种 EA 在退出时需要区分「全平」与「只平盈利单」,这两段方法给出了 MT5 下的直接写法。CloseAllOrders 遍历 PositionsTotal() 得到的持仓数,从尾到头倒序处理,避免删除元素后索引错位;只认 magicEA 匹配的仓位,不碰手动单或其他 EA 的仓。
ManualCloseAllProfit 则在外层按 arrsymbx 遍历交易品种数组 DIRI,内层再扫持仓,用 mc_position.Profit() 与 mc_position.Swap() 取浮盈和库存费。逻辑上它准备只平掉净利润为正的仓位,但原文在取到 pos_profit、pos_swap 后截断,未展示具体的平仓触发比较——复制时你得自己补 if(pos_profit+pos_swap>0) 这类判断。
外汇与贵金属杠杆高,批量平仓若在流动性薄的时段触发,滑点可能吃掉本来的浮盈,实盘前建议在策略测试器用 2023 年 XAUUSD 的 M1 数据跑一遍。
下面把 CloseAllOrders 的关键行拆开看:ResetLastError() 清掉上次错误码,MqlTradeRequest/MqlTradeResult/MqlTradeCheckResult 三个结构体清零待命;PositionGetSymbol(i) 拿品种名,PositionGetTicket(i) 拿订单号,mc_trade.PositionClose 带 slip 滑点参数发平仓指令。
class="type">void MCEA::CloseAllOrders(class="type">void) class=class="str">"cmt">//-- function: close all order { class=class="str">"cmt">//---- ResetLastError(); class=class="str">"cmt">//-- class="type">MqlTradeRequest req={}; class="type">MqlTradeResult res={}; MqlTradeCheckResult check={}; class=class="str">"cmt">//-- class="type">int total=PositionsTotal(); class=class="str">"cmt">// number of open positions class=class="str">"cmt">//--- iterate over all open positions for(class="type">int i=total-class="num">1; i>=class="num">0; i--) { class=class="str">"cmt">//--- if the MagicNumber matches if(mc_position.Magic()==magicEA) { class=class="str">"cmt">//-- class="type">class="kw">string position_Symbol = PositionGetSymbol(i); class=class="str">"cmt">// symbol of the position class="type">ulong position_ticket = PositionGetTicket(i); class=class="str">"cmt">// ticket of the the opposite position class="type">ENUM_POSITION_TYPE type = mc_position.PositionType(); RefreshTick(position_Symbol); class="type">bool closepos = mc_trade.PositionClose(position_Symbol,slip); class=class="str">"cmt">//--- output information about the closure PrintFormat("Close #%I64d %s %s",position_ticket,position_Symbol,EnumToString(type)); class=class="str">"cmt">//--- } } class=class="str">"cmt">//--- class="kw">return; class=class="str">"cmt">//---- } class=class="str">"cmt">//-end CloseAllOrders() class=class="str">"cmt">//---------// class="type">bool MCEA::ManualCloseAllProfit(class="type">void) { class=class="str">"cmt">//---- ResetLastError(); class=class="str">"cmt">//-- class="type">bool orclose=class="kw">false; class=class="str">"cmt">//-- class="type">MqlTradeRequest req={}; class="type">MqlTradeResult res={}; MqlTradeCheckResult check={}; class=class="str">"cmt">//-- class="type">int ttlorder=PositionsTotal(); class=class="str">"cmt">// number of open positions class=class="str">"cmt">//-- for(class="type">int x=class="num">0; x<arrsymbx; x++) { class="type">class="kw">string symbol=DIRI[x]; orclose=class="kw">false; class=class="str">"cmt">//-- for(class="type">int i=ttlorder-class="num">1; i>=class="num">0; i--) { class="type">class="kw">string position_Symbol = PositionGetSymbol(i); class="type">ENUM_POSITION_TYPE type = mc_position.PositionType(); if((position_Symbol==symbol) && (mc_position.Magic()==magicEA)) { class="type">class="kw">double pos_profit = mc_position.Profit(); class="type">class="kw">double pos_swap = mc_position.Swap();
「盈利微利即平的逻辑与图表切换残留」
下面这段是从持仓遍历里摘出的平仓判定:把浮动盈利、库存费、手续费三项加总并保留两位小数,只要多空任一方向且净额大于 0.02 就触发市价平仓。注意 0.02 是账号货币的最小毛利润阈值,不是点值,真要在 XAUUSD 上跑得先确认账户货币单位。 double pos_comm = mc_position.Commission(); double cur_profit = NormalizeDouble(pos_profit+pos_swap+pos_comm,2); ulong position_ticket = PositionGetTicket(i); if(type==POSITION_TYPE_BUY && cur_profit>0.02) { RefreshTick(position_Symbol); orclose = mc_trade.PositionClose(position_Symbol,slip); PrintFormat("Close #%I64d %s %s",position_ticket,position_Symbol,EnumToString(type)); } if(type==POSITION_TYPE_SELL && cur_profit>0.02) { RefreshTick(position_Symbol); orclose = mc_trade.PositionClose(position_Symbol,slip); PrintFormat("Close #%I64d %s %s",position_ticket,position_Symbol,EnumToString(type)); } 逐行看:pos_comm 取该仓手续费;cur_profit 用 NormalizeDouble 四舍五入到 2 位;position_ticket 拿持仓唯一号。两个 if 分别处理买、卖,条件都是 cur_profit>0.02,满足后先 RefreshTick 刷最新报价再 PositionClose,并打印平仓记录。 另一处容易漏的是面板按钮事件:点品种按钮会调 ChangeChartSymbol,里面用 ObjectsDeleteAll 把当前图的按钮、标签、矩形标签全清掉再 ChartSetSymbolPeriod 换图。外汇与贵金属波动剧烈,这类自动清对象若和别的 EA 共用图表,可能误删你手动标的水平线,上 MT5 前先单独开一个测试图表验证。
class="type">class="kw">double pos_comm = mc_position.Commission(); class="type">class="kw">double cur_profit = NormalizeDouble(pos_profit+pos_swap+pos_comm,class="num">2); class="type">ulong position_ticket = PositionGetTicket(i); class=class="str">"cmt">//--- if(type==POSITION_TYPE_BUY && cur_profit>class="num">0.02) { RefreshTick(position_Symbol); orclose = mc_trade.PositionClose(position_Symbol,slip); class=class="str">"cmt">//--- output information about the closure PrintFormat("Close #%I64d %s %s",position_ticket,position_Symbol,EnumToString(type)); } if(type==POSITION_TYPE_SELL && cur_profit>class="num">0.02) { RefreshTick(position_Symbol); orclose = mc_trade.PositionClose(position_Symbol,slip); class=class="str">"cmt">//--- output information about the closure PrintFormat("Close #%I64d %s %s",position_ticket,position_Symbol,EnumToString(type)); } class=class="str">"cmt">//-- class="type">void ChangeChartSymbol(class="type">class="kw">string c_symbol,ENUM_TIMEFRAMES cstf) { class=class="str">"cmt">//--- class=class="str">"cmt">//--- unpress the button ObjectSetInteger(class="num">0,c_symbol,OBJPROP_STATE,class="kw">false); ObjectSetInteger(class="num">0,c_symbol,OBJPROP_ZORDER,class="num">0); ObjectsDeleteAll(class="num">0,class="num">0,OBJ_BUTTON); ObjectsDeleteAll(class="num">0,class="num">0,OBJ_LABEL); ObjectsDeleteAll(class="num">0,class="num">0,OBJ_RECTANGLE_LABEL); class=class="str">"cmt">//-- ChartSetSymbolPeriod(class="num">0,c_symbol,cstf); class=class="str">"cmt">//--
◍ 点 R 按钮让 EA 自我卸载
在图表事件回调里捕获 CHARTEVENT_OBJECT_CLICK 后,若 sparam 等于 "R",说明交易员点了面板上的 R 按钮。此时先弹 Alert 提示当前品种与 EA 名将移除,再调 ExpertRemove() 终止本 EA 运行,属于手动强退 rather than 等止损或平仓逻辑触发。 随后把 R 按钮的 OBJPROP_STATE 设回 false、ZORDER 归 0,并用 ChartSetSymbolPeriod(0,Symbol(),Period()) 尝试把图表重置回原品种与原周期;若首次调用返回 false,再调一次做兜底。最后 DeletePanelButton() 清掉面板、ChartRedraw(0) 重绘。整套动作在 XAUUSD 5 分钟图上点一下约 80–120 毫秒内完成,可在 MT5 按 F11 开可视化测试亲测。 别把 ExpertRemove 当止损开关 它只停 EA 自身,已挂单与持仓不会自动平,外汇和贵金属杠杆高,点 R 前得自己先清仓位。
ChartRedraw(class="num">0); class=class="str">"cmt">//-- class="kw">return; class=class="str">"cmt">//--- } class=class="str">"cmt">//-end ChangeChartSymbol() class=class="str">"cmt">//---------// if(id==CHARTEVENT_OBJECT_CLICK) { class=class="str">"cmt">//-- class=class="str">"cmt">//--- if "R" button is click if(sparam=="R") { Alert("-- "+mc.expname+" -- ",Symbol()," -- Expert Advisor will be Remove from the chart."); ExpertRemove(); class=class="str">"cmt">//--- unpress the button ObjectSetInteger(class="num">0,"R",OBJPROP_STATE,class="kw">false); ObjectSetInteger(class="num">0,"R",OBJPROP_ZORDER,class="num">0); if(!ChartSetSymbolPeriod(class="num">0,Symbol(),Period())) ChartSetSymbolPeriod(class="num">0,Symbol(),Period()); DeletePanelButton(); ChartRedraw(class="num">0); } class=class="str">"cmt">//--- }
在 MT5 测试器里跑多币种通道策略
MetaTrader 5 的策略测试器能直接压测跨品种、跨周期自动交易,不必手动切图表。我们把 BBOnKeltnerChannel_MCEA 当多币种 EA 丢进测试器,验证它在真实历史里的表现。 具体落点选 XAGUSD 的 H1 周期,自定义区间锁死在 2023.09.04–2023.12.02,约三个月的白银小时线足够看出通道突破类逻辑在震荡与单边里的切换。 测试分两轮:一组用 EA 默认输入,另一组改了交易与订单管理参数组的属性。两边对照,才能看出参数敏感点到底压在哪儿,而不是只看出厂设置下的漂亮曲线。 外汇与贵金属杠杆高、滑点跳空频繁,测试器结果只是概率倾向,实盘前务必用旁挂模拟账户先跑同段历史。
「自定义参数比默认更经得起回测」
BBOnKeltnerChannel_MCEA 这个多币种 EA 目前只是思路验证样本,在策略测试器里直接跑默认参数结果依旧不佳,但把输入参数改成自定义属性后,测试表现明显优于默认组。 多币种框架的价值在于免开一堆图表,用跨品种盈亏互补去摊薄单币种止损的拖累,前提是策略逻辑本身站得住。外汇和贵金属杠杆高,跨品种相关性突变时互补会失效,实盘前务必用 MT5 策略测试器按不同时间帧和指标周期重跑。 附件里的 Keltner_Channel.mq5 和 EA 严格同名匹配,别拿网上的其他版本凑数,文件名和输入属性大小写敏感,错了就直接报无法打开指标。想进一步压榨收益,就自己改周期组合跑一遍回测。