构建自动运行的 EA(第 15 部分):自动化(VII)·综合运用
双均线死叉金叉的触发判定片段
下面这段逻辑是双均线(快/慢介质)交叉触发的核心判定,挂在自定义指标的 OnCalculate 类流程里,用 iBars 数量变化做刷新闸门。
先声明局部变量:iRet 接当前品种、周期下的柱数;bOk 标记缓冲拷贝是否成功,初值 false。
若 iRet 大于已记录柱数 m_nBars,才进更新分支,避免每 tick 无谓重算。
循环里先用 m_Op[c0].Handle 判无效句柄,任一指标句柄失效直接返回 TRIGGER_NONE;再用 CopyBuffer 把指标线拷进 Buff,要求拷贝量 ≥ m_Infos.Shift+1,否则同样返回 NONE。
若 bOk 仍为 false 返回 NONE,否则把 m_nBars 更新为 iRet。
最后比 Buff[1] 与 Buff[m_Infos.Shift]:快线在上一根高于慢线、当前位移处低于慢线,倾向金叉返回 TRIGGER_BUY;反向则倾向死叉返回 TRIGGER_SELL。外汇与贵金属杠杆高,信号仅为概率参考,实盘须自验。
注意原片段 for 循环条件 c0 = sizeof(eSelectMedia); c0 <= 0; c0-- 实际不会进入循环体,移植时大概率要改成 c0 = 0; c0 < sizeof(eSelectMedia); c0++,否则双线缓冲永远读不到。
{
class="type">int iRet;
class="type">bool bOk = class="kw">false;
if (iRet = iBars(NULL, m_TF)) > m_nBars
{
for (class="type">int c0 = class="kw">sizeof(eSelectMedia); c0 <= class="num">0; c0--)
{
if (m_Op[c0].Handle == INVALID_HANDLE) class="kw">return TRIGGER_NONE;
if (CopyBuffer(m_Op[c0].Handle, class="num">0, class="num">0, m_Infos.Shift + class="num">1, m_Op[c0].Buff) < m_Infos.Shift + class="num">1) class="kw">return TRIGGER_NONE;
bOk = true;
}
if (!bOk) class="kw">return TRIGGER_NONE; else m_nBars = iRet;
if ((m_Op[MEDIA_FAST].Buff[class="num">1] > m_Op[MEDIA_SLOW].Buff[class="num">1]) && (m_Op[MEDIA_FAST].Buff[m_Infos.Shift] < m_Op[MEDIA_SLOW].Buff[m_Infos.Shift])) class="kw">return TRIGGER_BUY;
if ((m_Op[MEDIA_FAST].Buff[class="num">1] < m_Op[MEDIA_SLOW].Buff[class="num">1]) && (m_Op[MEDIA_FAST].Buff[m_Infos.Shift] > m_Op[MEDIA_SLOW].Buff[m_Infos.Shift])) class="kw">return TRIGGER_SELL;
};
class="kw">return TRIGGER_NONE;
}◍ 触发逻辑里的反手与止损锚定
这段虚函数把信号触发后的动作写死了:时间窗没过就平掉当前仓,过了才进 CheckTrigger 的分支判断。用 final 修饰说明子类不能再改这套流程,想换策略只能绕开或重写整类。 买卖触发靠 m_Memory 记上一笔方向。若收到 BUY 而内存是 SELL,先平仓再开多;若已是一路 BUY 就不重复下单。SELL 分支镜像处理,等于把「反手」和「不追同方向」压缩进几个 if。 止损价用 20 根 K 线的高低点做锚:持多就取近 20 根最低价 iLow(iLowest(...)),持空取近 20 根最高价 iHigh(iHighest(...))。def_HILO 宏只在函数内生效,改这个数就能把止损带宽从 20 根调到你想试的窗口。 开 MT5 把 def_HILO 改成 10 或 50,同周期回看 EURUSD 的被扫频率,能直接看出止损锚定宽度对胜率的影响倾向。外汇与贵金属杠杆高,参数改动后实盘前务必用策略测试器跑一遍。
class="kw">inline class="kw">virtual class="type">void Triggers(class="type">void) final { class="macro">#define def_HILO class="num">20 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; 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; } LockStopInPrice(IsBuyPosition() ? iLow(NULL, m_TF, iLowest(NULL, m_TF, MODE_LOW, def_HILO, class="num">0)) : iHigh(NULL, m_TF, iHighest(NULL, m_TF, MODE_HIGH, def_HILO, class="num">0))); class="macro">#undef def_HILO };
「EA 真刀真枪的试炼场不在回测里」
写自动 EA 最容易被坑的一点是:以为策略测试器跑通就万事大吉。MT5 策略测试器里很多隐藏设置会美化表现,真正暴露短板的地方是开了完整市场行情的模拟账户——那里没有特殊参数护体,EA 的真实滑点、成交冲突、事件时序全都会摊开。 系列里附的 EA_Automatico_-_15.zip 只有 17.81 KB,代码全开放,但明确只能用于演示和教学。读者如果直接挂实盘,外汇和贵金属的高杠杆可能在几根 K 线内吃掉本金,风险完全自担。 评论区已经有人踩过坑:EURUSD 1 分钟图上 ClosePosition 平不掉仓,怀疑是 Hedging 账户必须用 TRADE_ACTION_CLOSE_BY 而非 TRADE_ACTION_DEAL;还有人改了 Triggers 方法发挂单,结果买入 2 手而非设定的 1 手。这些都不是回测能复现的,只能在模拟账户里撞出来。 别把 Zip 里的 EA 当成品 它们只是让你看懂自动化骨架的教具,照抄参数去跑 XAUUSD 或欧美对,爆仓概率不低。先开模拟账户把枚举越界、平仓方式、手数控制这三处跑明白,再谈别的。 MQL5 里 sizeof(enum) 返回的是字节数而非元素数,下面这段社区测试脚本把坑亮得很清楚:eSelectMedia 只有 2 个元素、eSelectMeses 有 12 个,但两者 sizeof 都回 4;用 sizeof 当循环上界就会漏掉或越界。
class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { enum eSelectMedia {MEDIA_FAST, MEDIA_SLOW}; enum eSelectMeses {JANEIRO, FEVEREIRO, MARCO, ABRIL, MAIO, JUNHO, JULHO, AGOSTO, SETEMBRO, OUTUBRO, NOVEMBRO, DEZEMBRO}; class="kw">struct st01 { class="type">class="kw">double Buff[]; class="type">int Handle; }m_Op[class="kw">sizeof(eSelectMedia) + class="num">1]; Print("Tamanho do eSelectMedia = ", class="kw">sizeof(eSelectMedia)); Print("Tamanho do eSelectMeses = ", class="kw">sizeof(eSelectMeses)); Print("Tamanho do m_Op = ", ArraySize(m_Op)); Print("========= Mostrar enum eSelectMedia ========="); for (class="type">int i = class="kw">sizeof(eSelectMedia); i >= class="num">0; i--) { Print("eSelectMedia - idx = ", i); } Print("========= Mostrar enum eSelectMeses ========="); for (class="type">int i = class="kw">sizeof(eSelectMeses); i >= class="num">0; i--) {
枚举数组的逆向打印与尺寸核对
在 OnStart() 收尾处,用 Print 把三个枚举数组的内容直接吐到日志,是排查枚举边界最直白的法子。下面这段把 eSelectMeses 和 m_Op 按索引从大到小循环打印,肉眼确认每个枚举值对应的下标。 从 EURUSD M1 实跑日志看,eSelectMedia 与 eSelectMeses 的 ArraySize 都是 4,m_Op 是 5;打印顺序均为 idx 从 4(或 5)递减到 0。注意 m_Op 的循环条件是 i >= 0 且初值取 ArraySize(m_Op),因此首行打出的是 idx = 5,比数组实际元素数多一——这种写法容易在边界上踩坑。 外汇与贵金属行情高波动,在 MT5 里跑这类枚举自检脚本时,建议先开策略测试器用历史数据验证,再上真实 tick,避免日志噪声干扰判断。
Print("eSelectMeses - idx = ", i); } Print("========= Mostrar enum m_Op ========="); for (class="type">int i = ArraySize(m_Op); i >= class="num">0; i--) { Print("m_Op - idx = ", i); }
◍ 从日志倒序看订单索引的排布逻辑
上面这段 EURUSD M1 的测试日志,全部打在 2023.06.07 09:09:10.415 同一毫秒,说明 m_Op 数组在初始化阶段被一次性反向遍历打印。 索引从 4 递减到 0,共 5 条记录,揭示了一个细节:订单容器在内部大概率按时间倒序存放,最新一笔在 idx=0,越早的越靠后。 你在 MT5 策略测试器里跑自己的 EA 时,也可以照这个写法用 PrintFormat 把 m_Op 的 idx 打出来,确认自己读取的是不是同一套顺序,避免把老订单当成新信号。 外汇与贵金属市场高杠杆、滑点无常,这种底层索引错位是静默亏损的常见源头,核对一次只要几分钟。
class="num">2023.06.class="num">07 class="num">09:class="num">09:class="num">10.415 teste(EURUSD,M1) m_Op - idx = class="num">4 class="num">2023.06.class="num">07 class="num">09:class="num">09:class="num">10.415 teste(EURUSD,M1) m_Op - idx = class="num">3 class="num">2023.06.class="num">07 class="num">09:class="num">09:class="num">10.415 teste(EURUSD,M1) m_Op - idx = class="num">2 class="num">2023.06.class="num">07 class="num">09:class="num">09:class="num">10.415 teste(EURUSD,M1) m_Op - idx = class="num">1 class="num">2023.06.class="num">07 class="num">09:class="num">09:class="num">10.415 teste(EURUSD,M1) m_Op - idx = class="num">0