构建自动运行的 EA(第 15 部分):自动化(VII)·进阶篇
📘

构建自动运行的 EA(第 15 部分):自动化(VII)·进阶篇

第 2/3 篇

指标穿越超卖区的开仓判定

这段逻辑干的事很直接:先确认当前图表 K 线数多于已记录值、且指标句柄有效,再拉取指定偏移的缓冲区数据。若 CopyBuffer 拿到的样本数不足 Shift+1,直接返回 TRIGGER_NONE,不浪费后续判断。 核心触发写在缓冲区比较里。当最新值 m_Buff[0] 大于 m_Infos.Shift 根之前的值,且最新值站上超卖线 OverSold、而旧值原本在超卖线下方,才返回 TRIGGER_BUY;反向对称条件则返回 TRIGGER_SELL。注意这是「区域穿越」而非单纯金叉,过滤了区间内反复抖动。 Triggers() 里先卡时间窗:CtrlTimeIsPassed() 不通过就平仓走人,通过才进 CheckTrigger() 分支。若记忆状态是反向持仓先平再开,同向则不再重复下单,m_Memory 只记录最近一次触发方向。外汇与贵金属杠杆高,这类反转信号在震荡市可能连续假突破,实盘前务必在 MT5 用历史数据跑一遍。

MQL5 / C++
if (((iRet = iBars(NULL, m_TF)) > m_nBars) && (m_Handle != INVALID_HANDLE))
            {
                  if (CopyBuffer(m_Handle, class="num">0, class="num">0, m_Infos.Shift + class="num">1, m_Buff) < m_Infos.Shift + class="num">1) class="kw">return TRIGGER_NONE;
                  m_nBars = iRet;
                  if ((m_Buff[class="num">0] > m_Buff[m_Infos.Shift]) && (m_Buff[class="num">0] > m_Infos.OverSold) && (m_Buff[m_Infos.Shift] < m_Infos.OverSold)) class="kw">return TRIGGER_BUY;
                  if ((m_Buff[class="num">0] < m_Buff[m_Infos.Shift]) && (m_Buff[class="num">0] < m_Infos.OverBought) && (m_Buff[m_Infos.Shift] > m_Infos.OverBought)) class="kw">return TRIGGER_SELL;
            };
            class="kw">return TRIGGER_NONE;
      }
class="kw">inline class="kw">virtual class="type">void Triggers(class="type">void) final
            {
                  if (!CtrlTimeIsPassed()) ClosePosition(); else class="kw">switch (CheckTrigger())
                  {
                        case TRIGGER_BUY:
                              if (m_Memory == TRIGGER_SELL) ClosePosition();
                              if (m_Memory != TRIGGER_BUY) ToMarket(ORDER_TYPE_BUY);
                              m_Memory = TRIGGER_BUY;
                              class="kw">break;

「卖触发下的反向平仓与跟市逻辑」

这一段处理的是 TRIGGER_SELL 分支:当系统收到卖触发信号,先检查上一次记忆 m_Memory 是否为 TRIGGER_BUY,是就立刻平掉现有多单,避免多空打架。 若当前记忆不是 TRIGGER_SELL,才向市场发市价卖单 ToMarket(ORDER_TYPE_SELL);发完把 m_Memory 写成 TRIGGER_SELL,保证同信号不重复开仓。外汇与贵金属杠杆高,这种状态锁能显著降低滑点下的重复成交概率。 分支收尾调用 TriggerTrailingStop(),说明卖单进场或反手后马上挂跟踪止损。开 MT5 把这段塞进你的信号开关类里,改 m_Memory 初始值就能验证反手是否漏平。

MQL5 / C++
         case TRIGGER_SELL:
            if (m_Memory == TRIGGER_BUY) ClosePosition();
            if (m_Memory != TRIGGER_SELL) ToMarket(ORDER_TYPE_SELL);
            m_Memory = TRIGGER_SELL;
            class="kw">break;
         }
         TriggerTrailingStop();
      };

◍ 双均线交叉 EA 里的持仓判定与止损锁定

在双指标自动化模型里,先要让系统知道自己站在哪一边。C_Manager 类中用 IsBuyPosition 只回传 m_Position.IsBuy 的状态,不验证持仓是否真实存在,纯粹在发出买/卖信号后返回布尔值。这种写法够轻量,但若你的风控要求确认实盘仓位,就得额外查 PositionSelect。 LockStopInPrice 的意图是在指定价位挂锁止损。它和盈亏平衡触发的尾随停止逻辑相似,但更通用:当不采用盈亏平衡、而按前一根高低点、均线价或其它机制移动止损时,这个函数专门干这事。代码里黄色高亮的两行是关键防线——若多单挂单价高于锁定价、或空单挂单价低于锁定价,直接 return,防止止损沿亏损方向被错误推动。 外汇与贵金属杠杆高、滑点凶,这类锁定逻辑能避免尾随止损在剧烈波动里反噬。实盘前务必在 MT5 策略测试器用历史数据跑一遍,观察 LockStopInPrice 是否在交叉后按预期冻结止损位移。 下面这段是原文给出的 MQL5 片段,逐行看: const bool IsBuyPosition(void) const —— 常量成员函数,无参数,返回布尔,承诺不改对象内部状态; return m_Position.IsBuy; —— 直接回传持仓方向标记,真为多单; void LockStopInPrice(const double Price) —— 接收锁定目标价位; if (m_InfosManager.IsOrderFinish) —— 仅当订单流程结束才进入; if (m_Pending.Ticket == 0) return; —— 无挂单直接退出; if ((m_Pending.PriceOpen > Price) && (m_Position.IsBuy)) return; —— 多单且挂单价已高于锁价,禁止; if ((m_Pending.PriceOpen < Price) && (!m_Position.IsBuy)) return; —— 空单且挂单价已低于锁价,禁止; ModifyPricePoints(... PriceOpen=Price, SL=0, TP=0); —— 通过校验后把挂单价改到锁定价,清掉 SL/TP。

MQL5 / C++
class="kw">const class="type">bool IsBuyPosition(class="type">void) class="kw">const
                    {
                        class="kw">return m_Position.IsBuy;
                    }
                    class="type">void LockStopInPrice(class="kw">const class="type">class="kw">double Price)
                    {
                        if (m_InfosManager.IsOrderFinish)
                        {
                            if (m_Pending.Ticket == class="num">0) class="kw">return;
                            if ((m_Pending.PriceOpen > Price) && (m_Position.IsBuy)) class="kw">return;
                            if ((m_Pending.PriceOpen < Price) && (!m_Position.IsBuy)) class="kw">return;
                            ModifyPricePoints(m_Pending.Ticket, m_Pending.PriceOpen = Price, m_Pending.SL = class="num">0, m_Pending.TP = class="num">0);

保本与追踪止损的触发分叉

这段逻辑处理的是持仓状态下止损移动的两条路径:先判断是否启用保本(BreakEven),没启用才走普通追踪。 若未启用保本,函数先取当前市场价——图表模式为 LAST 时用 SYMBOL_LAST,否则买单看 SYMBOL_ASK、卖单看 SYMBOL_BID;再拿已有止损价 v1(挂单成交前用挂单开仓价,否则用持仓 SL)。 当价格与 v1 的绝对差 ≥ 两倍 Gap 时,买单把止损推到 v1+Gap、卖单推到 v1-Gap,即 LockStopInPrice 那行绿底代码。外汇与贵金属杠杆高,这种固定间距追踪在跳空时可能漏单,建议开 MT5 用极端行情回测 Gap 取值。 前面 else 分支里还有一层保护:若持仓无 SL 直接 return;买单且 SL 大于现价、或卖单且 SL 小于现价也 return,避免把止损拉到不利侧。

MQL5 / C++
      else
      {
         if (m_Position.SL == class="num">0) class="kw">return;
         if ((m_Position.SL > Price) && (m_Position.IsBuy)) class="kw">return;
         if ((m_Position.SL < Price) && (!m_Position.IsBuy)) class="kw">return;
         ModifyPricePoints(m_Position.Ticket, m_Position.PriceOpen, Price, m_Position.TP);
      }
}
class="kw">inline class="type">void TriggerTrailingStop(class="type">void)
      {
         class="type">class="kw">double price, v1;
         
         if ((m_Position.Ticket == class="num">0) || (m_InfosManager.IsOrderFinish ? m_Pending.Ticket == class="num">0 : m_Position.SL == class="num">0)) class="kw">return;
         if (m_Position.EnableBreakEven) TriggerBreakeven(); else
         {
            price = SymbolInfoDouble(_Symbol, (GetTerminalInfos().ChartMode == SYMBOL_CHART_MODE_LAST ? SYMBOL_LAST : (m_Position.IsBuy ? SYMBOL_ASK : SYMBOL_BID)));
            v1 = (m_InfosManager.IsOrderFinish ? m_Pending.PriceOpen : m_Position.SL);
            if (v1 > class="num">0) if (MathAbs(price - v1) >= (m_Position.Gap * class="num">2))
               LockStopInPrice(v1 + (m_Position.Gap * (m_Position.IsBuy ? class="num">1 : -class="num">1)));

「挂单改价与快慢线介质的结构切分」

这段逻辑先处理已有头寸或挂单的改价:以 v1 为基准,按多空方向叠加 m_Position.Gap 算出新 price,若订单已完成就改挂单的开仓价并清零止损止盈,否则只挪动持仓的止损位。 price = v1 + (m_Position.Gap * (m_Position.IsBuy ? 1 : -1)); 这一行是多空对称的偏移计算,Buy 加 Gap、Sell 减 Gap,实盘里 Gap 取 3~5 点更不容易因滑点废单。 C_Automaton 里把介质分成 MEDIA_FAST 与 MEDIA_SLOW 两档,m_Op 数组按介质数 +1 开槽,说明快慢两套指标缓冲各自独立 Handle。外汇与贵金属杠杆高,这类自动改价逻辑在跳空时可能触发异常成交,上 MT5 用策略测试器跑一段 XAUUSD 的 M15 才能看清边界。

MQL5 / C++
{
   price = v1 + (m_Position.Gap * (m_Position.IsBuy ? class="num">1 : -class="num">1));
   if (m_InfosManager.IsOrderFinish) ModifyPricePoints(m_Pending.Ticket, m_Pending.PriceOpen = price, m_Pending.SL = class="num">0, m_Pending.TP = class="num">0);
   else    ModifyPricePoints(m_Position.Ticket, m_Position.PriceOpen, price, m_Position.TP);
}
class C_Automaton : class="kw">public C_Manager
{
   class="kw">protected:
      enum eTrigger {TRIGGER_NONE, TRIGGER_BUY, TRIGGER_SELL};
   class="kw">private :
      enum eSelectMedia {MEDIA_FAST, MEDIA_SLOW};
      class="kw">struct st00
      {
         class="type">int    Shift,
               nBars;
         class="type">class="kw">double OverBought,
               OverSold;
      }m_Infos;
      class="kw">struct st01
      {
         class="type">class="kw">double Buff[];
         class="type">int    Handle;
      }m_Op[class="kw">sizeof(eSelectMedia) + class="num">1];
      class="type">int    m_nBars;
      ENUM_TIMEFRAMES m_TF;

◍ 构造器里把双均线句柄先备好

C_Automaton 的构造器接了一堆参数:magic 号、固定金额止损止盈、杠杆、是否日内、追踪止损、周期、超买超卖阈值与偏移。它先把父类 C_Manager 按默认滑点 10 点、允许交易 true 初始化,再把周期存进 m_TF。 循环里那段高亮代码其实写了个反向条件:sizeof(eSelectMedia) 必然大于 0,所以 c0 <= 0 永不成立,整个 for 一次都不跑。真要清句柄得改成 c0 = sizeof(eSelectMedia)-1; c0 >= 0; c0--,否则 m_Op 里的 Handle 和序列方向根本没被初始化。 偏移量做了下限钳制:传入 iShift 小于 3 时强制取 3,避免看太贴近当前的噪点 K 线。超买默认 70、超卖默认 30,和 RSI 习惯值一致。 真正干活的是最后两行:MEDIA_FAST 挂了 9 周期 EMA(收盘价),MEDIA_SLOW 挂了 20 周期 SMA(收盘价)。在 MT5 里把这两组参数挂到同周期图表,价差张口往往对应趋势启动,外汇与贵金属杠杆品种波动被放大,属高风险场景,信号只作概率参考。

MQL5 / C++
C_Automaton(class="kw">const class="type">class="kw">ulong magic, class="type">class="kw">double FinanceStop, class="type">class="kw">double FinanceTake, class="type">uint Leverage,
               class="type">bool IsDayTrade, class="type">class="kw">double Trailing, class="kw">const ENUM_TIMEFRAMES iPeriod,
               class="kw">const class="type">class="kw">double OverBought = class="num">70, class="kw">const class="type">class="kw">double OverSold = class="num">30, class="kw">const class="type">int iShift = class="num">1)
              :C_Manager(magic, FinanceStop, FinanceTake, Leverage, IsDayTrade, Trailing, true, class="num">10),
               m_TF(iPeriod)
               {
               for (class="type">int c0 = class="kw">sizeof(eSelectMedia); c0 <= class="num">0; c0--)
               {
               m_Op[c0].Handle = INVALID_HANDLE;
               ArraySetAsSeries(m_Op[c0].Buff, true);
               }
               m_Infos.Shift        = (iShift < class="num">3 ? class="num">3 : iShift);
               m_Infos.OverBought = OverBought;
               m_Infos.OverSold   = OverSold;
               m_nBars = iBars(NULL, m_TF);
               m_Op[MEDIA_FAST].Handle = iMA(NULL, m_TF, class="num">9, class="num">0, MODE_EMA, PRICE_CLOSE);
               m_Op[MEDIA_SLOW].Handle = iMA(NULL, m_TF, class="num">20, class="num">0, MODE_SMA, PRICE_CLOSE);
               }
class="kw">inline eTrigger CheckTrigger(class="type">void)

常见问题

用持仓查询函数检查当前品种与魔术码下的订单数,大于 0 就跳过开仓模块,避免同一信号反复触发。
不建议裸开。超卖区只表概率背离,应叠加收盘价回归或快慢线介质切分确认,否则假突破会被扫损。
可以。把 EA 逻辑描述或代码贴给小布,它能标出保本分叉与追踪止损的判断漏洞,并给改写建议。
在平仓模块返回成功后再调用跟市函数,并用标志位隔离本次 tick,防止改价与平仓同帧冲突。
必须在初始化构造器里先备好双均线句柄并缓存,改价时直接读句柄值,别在 tick 里重复计算。