使用信号指标的简单交易系统·进阶篇
⚙️

使用信号指标的简单交易系统·进阶篇

(2/3)· 从基础信号探测器到通用 EA 骨架,少写重复代码才能快测 14 种指标

实战向进阶 第 2/3 篇
很多人每换一个信号指标就重写一遍 EA,结果 bug 越堆越多。其实信号指标本质都是柱上标签,通用骨架早该一次写好。把精力耗在复制粘贴上,等于主动放弃回测更多策略的机会。

「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,能直观感受指标灵敏度与资源占用的权衡。外汇和贵金属杠杆高,句柄失败或柱数不足时盲目下单可能瞬间放大亏损。

MQL5 / C++
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_ 若设太小,在黄金跳空时段平仓订单可能频繁拒单。

MQL5 / C++
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 分钟数据跑一遍,观察拒单率再定参数。

MQL5 / C++
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(!SignalBarNB.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)) //如果执行方式在策略测试器中被设置为“随机延迟” {

MQL5 / C++
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 点,会明显改变平仓成交率。

MQL5 / C++
   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">//----
   
}
把指标扫描交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到信号指标在当前周期是否触发,省去自己挂 EA 验信号的等待。把重复劳动交给小布,你专注决策。

常见问题

信号指标多在当前柱直接给箭头标签,EA 读缓冲即可;信号趋势指标如 Heiken Ashi Smoothed 需比较序列状态,取号代码略有不同但下单部分几乎不变。
会。若 EA 图表时段与指标时段不一致且未显式指定,信号柱索引错位,可能漏掉或重复触发,概率随周期差增大而上升。
原文限定零值情况下不得使用挂单,因为市价单可依赖即时平仓逻辑,挂单缺止损易在跳空时失控,外汇贵金属高风险下尤甚。
小布目前内置的是信号诊断与提醒,不自动下单;你可对照小布的信号页手动或用自己 EA 执行,决策仍归你。
只要它输出标准缓冲标签,基础 EA 仅改输入参数与指标句柄,核心交易函数打包后几乎不动。