使用信号指标的简单交易系统·进阶篇
(2/3)· 从基础信号探测器到通用 EA 骨架,少写重复代码才能快测 14 种指标
「EA 初始化与 tick 驱动的信号捕捉骨架」
把 ASCtrend 指标接进 EA,第一步是在 OnInit 里用 iCustom 拿句柄:iCustom(Symbol(),InpInd_Timeframe,"ASCtrend",RISK)。若返回 INVALID_HANDLE,说明指标名或参数不对,MT5 终端会打印失败提示,这时候 EA 跑起来也不会有信号。 最小计算柱数不是写死的,而是 int(3+RISK*2+SignalBar)。举例 RISK=3、SignalBar=1 时,至少要 10 根历史 K 线才够指标算数,少于这个数 OnTick 直接 return,避免数组越界。 OnTick 里先用 BarsCalculated 卡一道门槛,再 LoadHistory 补历史,否则 IsNewBar 可能误判。信号数组只开 1 长度:double DnVelue[1],UpVelue[1],配合 CopyBuffer 从 buffer 1(上破)和 buffer 0(下破)各取 1 根,写 0 即代表当前或 SignalBar 偏移那根。 让小布替你跑这套:把 RISK 从 1 调到 5,看 min_rates_total 怎么从 5 变到 13,能直观感受指标灵敏度与资源占用的权衡。外汇和贵金属杠杆高,句柄失败或柱数不足时盲目下单可能瞬间放大亏损。
class=class="str">"cmt">//| EA交易初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//---- 获取 ASCtrend 指标句柄 InpInd_Handle=iCustom(Symbol(),InpInd_Timeframe,"ASCtrend",RISK); if(InpInd_Handle==INVALID_HANDLE) Print(" Failed to get handle of ASCtrend indicator"); class=class="str">"cmt">//---- 初始化以秒的形式存储图表周期的变量 TimeShiftSec=PeriodSeconds(InpInd_Timeframe); class=class="str">"cmt">//---- 初始化开始计算数据的变量 min_rates_total=class="type">int(class="num">3+RISK*class="num">2+SignalBar); class=class="str">"cmt">//---- class="kw">return(class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA交易去初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//---- GlobalVariableDel_(Symbol()); class=class="str">"cmt">//---- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA的订单函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//---- 检查用于计算的柱形数量是否足够 if(BarsCalculated(InpInd_Handle)<min_rates_total) class="kw">return; class=class="str">"cmt">//---- 为IsNewBar()和SeriesInfoInteger()函数的正常运行,加载历史数据 LoadHistory(TimeCurrent()-PeriodSeconds(InpInd_Timeframe)-class="num">1,Symbol(),InpInd_Timeframe); class=class="str">"cmt">//---- 本地变量声明 class="type">class="kw">double DnVelue[class="num">1],UpVelue[class="num">1]; class=class="str">"cmt">//----静态变量声明 class="kw">static class="type">bool Recount=true; class="kw">static class="type">bool BUY_Open=false,BUY_Close=false; class="kw">static class="type">bool SELL_Open=false,SELL_Close=false; class="kw">static class="type">class="kw">datetime UpSignalTime,DnSignalTime; class="kw">static CIsNewBar NB; class=class="str">"cmt">//+----------------------------------------------+ class=class="str">"cmt">//| 搜索交易执行信号 | class=class="str">"cmt">//+----------------------------------------------+ if(!SignalBar || NB.IsNewBar(Symbol(),InpInd_Timeframe) || Recount) class=class="str">"cmt">// 检查新的柱形 { class=class="str">"cmt">//---- 交易信号清零 BUY_Open=false; SELL_Open=false; BUY_Close=false; SELL_Close=false; Recount=false; class=class="str">"cmt">//---- 将新数据复制到数组中 if(CopyBuffer(InpInd_Handle,class="num">1,SignalBar,class="num">1,UpVelue)<=class="num">0) {Recount=true; class="kw">return;} if(CopyBuffer(InpInd_Handle,class="num">0,SignalBar,class="num">1,DnVelue)<=class="num">0) {Recount=true; class="kw">return;} class=class="str">"cmt">//---- 获取买入信号 if(UpVelue[class="num">0] && UpVelue[class="num">0]!=EMPTY_VALUE) { if(BuyPosOpen) BUY_Open=true;
平仓信号的逆向回看逻辑
这段逻辑解决一个常被忽略的问题:当同时允许开仓与平仓时,系统怎么判断上一笔持仓该在哪根历史 bar 退出。它不从当前 bar 往前瞎猜,而是从 SignalBar+1 开始向后遍历,直到碰到反向指标缓冲非零才确认平仓信号。 具体看,若 SellPosClose 为真,就取指标句柄 1 号缓冲(对应上行速率值)在遍历 bar 上的数值;CopyBuffer 返回 ≤0 直接置 Recount 并 return,避免脏数据污染。若 UpVelue[0] 既非 0 也非 EMPTY_VALUE,说明出现了有效反向,SELL_Close 置真并 break。 买入平仓镜像处理:用 0 号缓冲(下行速率值)做同样回看。实盘中外汇与贵金属杠杆高,这类基于历史 bar 回溯的平仓判定若 TimeShiftSec 没对齐服务器时区,可能漏掉关键反转,建议开 MT5 用 EURUSD 的 M15 手动单步跟一遍。 最后两行把算出的 BUY_Close / SELL_Close 交给 BuyPositionClose 与 SellPositionClose,并带入 Symbol() 与 Deviation_ 滑点容差。Deviation_ 若设太小,在黄金跳空时段平仓订单可能频繁拒单。
if(SellPosClose) SELL_Close=true; UpSignalTime=class="type">class="kw">datetime(SeriesInfoInteger(Symbol(),InpInd_Timeframe,SERIES_LASTBAR_DATE))+TimeShiftSec; class=class="str">"cmt">//---- 获取卖出信号 if(DnVelue[class="num">0] && DnVelue[class="num">0]!=EMPTY_VALUE) { if(SellPosOpen) SELL_Open=true; if(BuyPosClose) BUY_Close=true; DnSignalTime=class="type">class="kw">datetime(SeriesInfoInteger(Symbol(),InpInd_Timeframe,SERIES_LASTBAR_DATE))+TimeShiftSec; } class=class="str">"cmt">//---- 为获取持仓平仓信号,查找最近一笔交易的方向 if((BuyPosOpen && BuyPosClose || SellPosOpen && SellPosClose) && (!BUY_Close && !SELL_Close)) { class="type">int Bars_=Bars(Symbol(),InpInd_Timeframe); for(class="type">int bar=class="type">int(SignalBar+class="num">1); bar<Bars_; bar++) { if(SellPosClose) { if(CopyBuffer(InpInd_Handle,class="num">1,bar,class="num">1,UpVelue)<=class="num">0) {Recount=true; class="kw">return;} if(UpVelue[class="num">0]!=class="num">0 && UpVelue[class="num">0]!=EMPTY_VALUE) { SELL_Close=true; break; } } if(BuyPosClose) { if(CopyBuffer(InpInd_Handle,class="num">0,bar,class="num">1,DnVelue)<=class="num">0) {Recount=true; class="kw">return;} if(DnVelue[class="num">0]!=class="num">0 && DnVelue[class="num">0]!=EMPTY_VALUE) { BUY_Close=true; break; } } } } class=class="str">"cmt">//---- 平买入持仓 BuyPositionClose(BUY_Close,Symbol(),Deviation_); class=class="str">"cmt">//---- 平卖出持仓 SellPositionClose(SELL_Close,Symbol(),Deviation_);
◍ 开仓函数的双向触发写法
这段代码展示了在信号时间窗内分别下买单和卖单的调用方式,核心是两个封装函数 BuyPositionOpen 与 SellPositionOpen,入参里 Symbol() 取当前图表品种,UpSignalTime / DnSignalTime 分别承载多空触发时刻。 注意第三参数传的是时间变量而非价格,说明信号判定发生在历史 K 线闭合后,由上层逻辑算出具体时点再喂给开仓模块;StopLoss_ 与 TakeProfit_ 若传 0 则代表不挂止损止盈,实盘外汇与贵金属波动剧烈,裸奔持仓风险极高,建议至少按 ATR 倍数赋值。 Deviation_ 是滑点容差,MT5 默认以点为单位,若设得过小在流动性差的时段会频繁报『成交拒绝』,可先在策略测试器里用 2023 年黄金 1 分钟数据跑一遍,观察拒单率再定参数。
class=class="str">"cmt">//---- 买入 BuyPositionOpen(BUY_Open,Symbol(),UpSignalTime,MM,class="num">0,Deviation_,StopLoss_,TakeProfit_); class=class="str">"cmt">//---- 卖出 SellPositionOpen(SELL_Open,Symbol(),DnSignalTime,MM,class="num">0,Deviation_,StopLoss_,TakeProfit_); class=class="str">"cmt">//---- }
「给 EA 挂上第二个信号指标要改哪几处」
想在原有 EA 上叠加另一个信号指标,不是简单拖进去就完事,得动四处硬代码。先在 EA 输入参数里把旧指标的参数换成新指标的必填项,否则句柄都拿不到。 OnInit() 里获取指标句柄的那行必须重写,指标名和参数要对应新品种。指标缓冲区的索引也得确认好——本例用的是第 0 和第 1 号缓冲区来存买卖信号,CopyBuffer() 调用集要写进 OnTick()。 还有两个坑:min_rates_total 初始化起点要按新指标的计算需求改,不然前几根 K 线会算空;OnTick() 里“搜索上一个交易方向以获取平仓信号”的块也要跟着指标逻辑走。外汇与贵金属杠杆高,多指标共振也可能假信号,验证前先用策略测试器跑历史。
颜色缓冲区直接喂信号的改法
把趋势指标接进 EA 时,OnTick() 里抓信号的代码可以砍得很短。像 FiboCandles 这种指标,只暴露一个颜色缓冲区装方向指数,里面就两个值:0 代表上升市、1 代表下滑市。比起原先还要回溯 K 线形态,现在任意一根柱的趋势方向都能直接从缓冲区对应单元读出来。 平仓和开仓的先后顺序是个坑。执行交易那段必须先调平仓函数、再调开仓函数;要是反过来,在 "Open prices only" 模式下,同一根柱上可能刚平掉旧单就来不及开新单,回测或实盘的结果会被严重扭曲。外汇和贵金属杠杆高,这种顺序错误会放大滑点与漏单风险。 下面这段 MQL5 是接了颜色缓冲区的订单函数骨架,重点看 CopyBuffer 取的是第 4 号缓冲区、长度 2,以及 TrendVelue[0] 与 [1] 的 0/1 翻转判定。 //+------------------------------------------------------------------+
| // | EA的订单函数 |
|---|
//+------------------------------------------------------------------+ void OnTick() { //---- 检查用于计算的柱形数量是否足够 if(BarsCalculated(InpInd_Handle)<min_rates_total) return; //---- 为IsNewBar()和SeriesInfoInteger()函数的正常运行加载历史数据 LoadHistory(TimeCurrent()-PeriodSeconds(InpInd_Timeframe)-1,Symbol(),InpInd_Timeframe); //---- 本地变量声明 double TrendVelue[2]; //----静态变量声明 static bool Recount=true; static bool BUY_Open=false,BUY_Close=false; static bool SELL_Open=false,SELL_Close=false; static datetime UpSignalTime,DnSignalTime; static CIsNewBar NB; //+----------------------------------------------+
| // | 搜索交易执行信号 |
|---|
//+----------------------------------------------+
| if(!SignalBar | NB.IsNewBar(Symbol(),InpInd_Timeframe) | Recount) // 检查新的柱形 |
|---|
{ //---- 交易信号清零 BUY_Open=false; SELL_Open=false; BUY_Close=false; SELL_Close=false; Recount=false; //---- 将新获取到的数据复制到数组中 if(CopyBuffer(InpInd_Handle,4,SignalBar,2,TrendVelue)<=0) {Recount=true; return;} //---- 获取买入信号 if(TrendVelue[0]==1 && TrendVelue[1]==0) { if(BuyPosOpen) BUY_Open=true; if(SellPosClose)SELL_Close=true; UpSignalTime=datetime(SeriesInfoInteger(Symbol(),InpInd_Timeframe,SERIES_LASTBAR_DATE))+TimeShiftSec; } //---- 获取卖出信号 if(TrendVelue[0]==0 && TrendVelue[1]==1) { if(SellPosOpen) SELL_Open=true; if(BuyPosClose) BUY_Close=true; DnSignalTime=datetime(SeriesInfoInteger(Symbol(),InpInd_Timeframe,SERIES_LASTBAR_DATE))+TimeShiftSec; } //---- 为获取持仓平仓信号,查找最近一笔交易的方向 //if(!MQL5InfoInteger(MQL5_TESTING) && !MQL5InfoInteger(MQL5_OPTIMIZATION)) //如果执行方式在策略测试器中被设置为“随机延迟” {
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA的订单函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//---- 检查用于计算的柱形数量是否足够 if(BarsCalculated(InpInd_Handle)<min_rates_total) class="kw">return; class=class="str">"cmt">//---- 为IsNewBar()和SeriesInfoInteger()函数的正常运行加载历史数据 LoadHistory(TimeCurrent()-PeriodSeconds(InpInd_Timeframe)-class="num">1,Symbol(),InpInd_Timeframe); class=class="str">"cmt">//---- 本地变量声明 class="type">class="kw">double TrendVelue[class="num">2]; class=class="str">"cmt">//----静态变量声明 class="kw">static class="type">bool Recount=true; class="kw">static class="type">bool BUY_Open=false,BUY_Close=false; class="kw">static class="type">bool SELL_Open=false,SELL_Close=false; class="kw">static class="type">class="kw">datetime UpSignalTime,DnSignalTime; class="kw">static CIsNewBar NB; class=class="str">"cmt">//+----------------------------------------------+ class=class="str">"cmt">//| 搜索交易执行信号 | class=class="str">"cmt">//+----------------------------------------------+ if(!SignalBar || NB.IsNewBar(Symbol(),InpInd_Timeframe) || Recount) class=class="str">"cmt">// 检查新的柱形 { class=class="str">"cmt">//---- 交易信号清零 BUY_Open=false; SELL_Open=false; BUY_Close=false; SELL_Close=false; Recount=false; class=class="str">"cmt">//---- 将新获取到的数据复制到数组中 if(CopyBuffer(InpInd_Handle,class="num">4,SignalBar,class="num">2,TrendVelue)<=class="num">0) {Recount=true; class="kw">return;} class=class="str">"cmt">//---- 获取买入信号 if(TrendVelue[class="num">0]==class="num">1 && TrendVelue[class="num">1]==class="num">0) { if(BuyPosOpen) BUY_Open=true; if(SellPosClose)SELL_Close=true; UpSignalTime=class="type">class="kw">datetime(SeriesInfoInteger(Symbol(),InpInd_Timeframe,SERIES_LASTBAR_DATE))+TimeShiftSec; } class=class="str">"cmt">//---- 获取卖出信号 if(TrendVelue[class="num">0]==class="num">0 && TrendVelue[class="num">1]==class="num">1) { if(SellPosOpen) SELL_Open=true; if(BuyPosClose) BUY_Close=true; DnSignalTime=class="type">class="kw">datetime(SeriesInfoInteger(Symbol(),InpInd_Timeframe,SERIES_LASTBAR_DATE))+TimeShiftSec; } class=class="str">"cmt">//---- 为获取持仓平仓信号,查找最近一笔交易的方向 class=class="str">"cmt">//if(!MQL5InfoInteger(MQL5_TESTING) && !MQL5InfoInteger(MQL5_OPTIMIZATION)) //如果执行方式在策略测试器中被设置为“随机延迟” {
◍ 趋势反转信号触发后的平仓与开仓动作
当上一根柱线的 TrendVelue 指标值归零且存在卖出持仓的平仓与开仓条件同时满足时,SELL_Close 被置为真;同理,TrendVelue[1]==1 且买入侧条件齐备时,BUY_Close 为真。这两个布尔量直接驱动下方的持仓关闭函数。 实际下单前,EA 会依次调用 BuyPositionClose 与 SellPositionClose,传入对应的布尔开关、当前交易品种 Symbol() 以及滑点容差 Deviation_。若开关为假,该函数内部通常不执行任何撤单动作,避免无谓的服务器请求。 开仓环节分两条线:BuyPositionOpen 使用 UpSignalTime 作为信号时间,SellPositionOpen 使用 DnSignalTime。两者都带入资金管理系数 MM、魔术码 0、Deviation_、止损 StopLoss_ 与止盈 TakeProfit_。外汇与贵金属杠杆高,这类由市价触发的反转单在跳空行情中可能滑点扩大,触发概率与实盘流动性直接相关,建议先在 MT5 策略测试器用 2023 年 XAUUSD 的 M15 数据跑一遍观察成交偏差。 把这段代码整段拷进 EA 的 OnTick 末尾,配合前篇的信号计算模块,就能在 MT5 里复现一套基于 TrendVelue 反转的闭环交易逻辑;参数 Deviation_ 设 10 点还是 30 点,会明显改变平仓成交率。
if(SellPosOpen && SellPosClose && TrendVelue[class="num">1]==class="num">0) SELL_Close=true; if(BuyPosOpen && BuyPosClose && TrendVelue[class="num">1]==class="num">1) BUY_Close=true; } } class=class="str">"cmt">//+----------------------------------------------+ class=class="str">"cmt">//| 执行交易 class=class="str">"cmt">//+----------------------------------------------+ class=class="str">"cmt">//---- 平买入持仓 BuyPositionClose(BUY_Close,Symbol(),Deviation_); class=class="str">"cmt">//---- 平卖出持仓 SellPositionClose(SELL_Close,Symbol(),Deviation_); class=class="str">"cmt">//---- 买入 BuyPositionOpen(BUY_Open,Symbol(),UpSignalTime,MM,class="num">0,Deviation_,StopLoss_,TakeProfit_); class=class="str">"cmt">//---- 卖出 SellPositionOpen(SELL_Open,Symbol(),DnSignalTime,MM,class="num">0,Deviation_,StopLoss_,TakeProfit_); class=class="str">"cmt">//---- }