构建自动运行的 EA(第 15 部分):自动化(VII)·进阶篇
指标穿越超卖区的开仓判定
这段逻辑干的事很直接:先确认当前图表 K 线数多于已记录值、且指标句柄有效,再拉取指定偏移的缓冲区数据。若 CopyBuffer 拿到的样本数不足 Shift+1,直接返回 TRIGGER_NONE,不浪费后续判断。 核心触发写在缓冲区比较里。当最新值 m_Buff[0] 大于 m_Infos.Shift 根之前的值,且最新值站上超卖线 OverSold、而旧值原本在超卖线下方,才返回 TRIGGER_BUY;反向对称条件则返回 TRIGGER_SELL。注意这是「区域穿越」而非单纯金叉,过滤了区间内反复抖动。 Triggers() 里先卡时间窗:CtrlTimeIsPassed() 不通过就平仓走人,通过才进 CheckTrigger() 分支。若记忆状态是反向持仓先平再开,同向则不再重复下单,m_Memory 只记录最近一次触发方向。外汇与贵金属杠杆高,这类反转信号在震荡市可能连续假突破,实盘前务必在 MT5 用历史数据跑一遍。
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 初始值就能验证反手是否漏平。
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。
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,避免把止损拉到不利侧。
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 才能看清边界。
{
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 里把这两组参数挂到同周期图表,价差张口往往对应趋势启动,外汇与贵金属杠杆品种波动被放大,属高风险场景,信号只作概率参考。
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)