从头开始开发智能交易系统(第 25 部分):提供系统健壮性(II)(基础篇)
📘

从头开始开发智能交易系统(第 25 部分):提供系统健壮性(II)(基础篇)

第 1/3 篇

「给 EA 加一层崩溃防护」

很多新手把 EA 写成线性脚本,行情一卡顿或者订单被拒就直接崩线程。MT5 里真正能跑久的机器人,必须在关键调用处做状态校验,而不是假设一切顺利。 下面这段演示了在发送订单前检查交易环境是否就绪。注意 MQLInfoInteger 拿到的终端状态,以及 IsTradeAllowed 这种开关判断,缺了任何一步都可能让 EA 在实盘里静默失效。 外汇与贵金属杠杆高,这类防护不到位时,一次异常就可能放大成不可控回撤,概率上更建议在策略测试器里先跑满 200 根以上不同品种 K 线再上真仓。

MQL5 / C++
if(!MQLInfoInteger(MQL_TRADE_ALLOWED)) class="kw">return false;
   if(!IsTradeAllowed()) class="kw">return false;
   if(!AccountInfoInteger(ACCOUNT_TRADE_MODE)==ACCOUNT_TRADE_MODE_REAL) class="kw">return false;

◍ 把交易系统从EA里拆出去

很多人习惯把指标计算、数据抓取、辅助逻辑全塞进 EA,但在 MT5 的架构里这是反模式。平台把指标、服务、脚本、EA 分成四个不同类,并不是为了麻烦你,而是因为它们最好分开跑。 指标运行在独立线程上,能并行处理且不影响平台整体性能;服务适合做 Web 数据访问这类长驻任务;脚本只活一小段时间做具体动作然后消失。EA 的本质应该是只管订单和仓位的向导,不盯指标、只看图表上真实发生的仓位变化。 想提高系统可靠性,第一步就是狠心从 EA 代码里删掉所有非交易系统的东西,只留负责分析、管理、处理订单仓位的部分,其余统统挪到指标或服务里。外汇和贵金属杠杆高、滑点跳空频繁,EA 职责越纯,崩盘概率越低。 别把‘EA 里不能有指标’当玄学。打开你正在跑的 EA,搜一下 iMA、iRSI 或者 WebRequest 调用,只要不在订单管理逻辑内,就值得拆出去单独成类。

把EA背景拆成独立指标

EA里画背景那段逻辑其实不影响交易,但每次价格跳动都跟着重绘,会拖慢主程序盯仓和盯单的效率。把它单独抽成一个指标挂到图表窗口,是降低EA负载最直接的办法。 下面这段指标代码就是原EA背景模块的平移版:声明了0个绘图序列,只负责在初始化时加载位图、设置透明度,并在图表尺寸变化时重设背景。注意indicator_plots设为0,意味着它不产生任何数据输出,纯粹做界面层的事。 #property copyright "Daniel Jose" #property indicator_chart_window #property indicator_plots 0 //+------------------------------------------------------------------+ #include <NanoEA-SIMD\Auxiliar\C_Wallpaper.mqh> //+------------------------------------------------------------------+ input string user10 = "Wallpaper_01"; //Used BitMap input char user11 = 60; //Transparency (from 0 to 100) input C_WallPaper::eTypeImage user12 = C_WallPaper::IMAGEM; //Background image type //+------------------------------------------------------------------+ C_Terminal Terminal; C_WallPaper WallPaper; //+------------------------------------------------------------------+ int OnInit() { IndicatorSetString(INDICATOR_SHORTNAME, "WallPaper"); Terminal.Init(); WallPaper.Init(user10, user12, user11); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) { return rates_total; } //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { switch (id) { case CHARTEVENT_CHART_CHANGE: Terminal.Resize(); WallPaper.Resize(); break; } ChartRedraw(); } //+------------------------------------------------------------------+ 逐行看关键点:user11默认60代表背景透明度60%(0为全透明、100为全不透明),OnCalculate直接返回rates_total不做任何计算,所以指标对CPU占用趋近于零;CHARTEVENT_CHART_CHANGE里调Resize保证拖拽窗口时壁纸跟着适配。 实测把背景移出EA后,主EA在 tick 密集的贵金属行情里响应延迟可能下降一个量级,但外汇和贵金属本身杠杆高、滑点风险大,任何性能优化都不改变仓位暴露。开MT5把这段存成WallPaper.mq5编译挂上,就能验证背景和EA彻底解耦。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_plots class="num">0
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <NanoEA-SIMD\Auxiliar\C_Wallpaper.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">input class="type">class="kw">string                user10 = "Wallpaper_01";           class=class="str">"cmt">//Used BitMap
class="kw">input class="type">char                  user11 = class="num">60;                      class=class="str">"cmt">//Transparency(from class="num">0 to class="num">100)
class="kw">input C_WallPaper::eTypeImage  user12 = C_WallPaper::IMAGEM;   class=class="str">"cmt">//Background image type
class=class="str">"cmt">//+------------------------------------------------------------------+
C_Terminal      Terminal;
C_WallPaper WallPaper;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
        IndicatorSetString(INDICATOR_SHORTNAME, "WallPaper");
        Terminal.Init();
        WallPaper.Init(user10, user12, user11);
        class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double &price[])
{
        class="kw">return rates_total;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam)
{
        class="kw">switch (id)
        {
            case CHARTEVENT_CHART_CHANGE:
                Terminal.Resize();
                WallPaper.Resize();
            class="kw">break;
        }
        ChartRedraw();
}
class=class="str">"cmt">//+------------------------------------------------------------------+

「把逐价成交量从EA里抽出来做成指标」

高波动却没明确方向的行情里,EA 每一毫秒的机器周期都很贵——指标抢活儿会让你错过时机。把价格对应的成交量逻辑从 EA 搬到独立指标,是腾出主循环最直接的一步。 下面这段代码就是迁移后的最小骨架:删掉了 EA 里的 Times & Trade 相关部分,只保留成交量按价分布的绘制与刷新。想还原回 EA,把指标代码整段拷回去即可。 代码里 OnInit 用 EventSetTimer(1) 挂了 1 秒定时器,OnTimer 里调 VolumeAtPrice.Update() 做增量刷新,不走 OnCalculate——所以 indicator_plots 设 0,画图全靠类内部往主图叠加。外汇和贵金属波动大、滑点凶,这种高频刷新指标在实盘可能拖慢老机型,先开 MT5 用策略测试器跑一眼 CPU 占用再说。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_plots class="num">0
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <NanoEA-SIMD\Tape Reading\C_VolumeAtPrice.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">input class="type">color                user0   = clrBlack;                        class=class="str">"cmt">//Bar class="type">color
class="kw">input   class="type">char               user1   = class="num">20;                             class=class="str">"cmt">//Transparency(from class="num">0 to class="num">100 )
class="kw">input class="type">color    user2 = clrForestGreen; class=class="str">"cmt">//Buying
class="kw">input class="type">color    user3 = clrFireBrick;    class=class="str">"cmt">//Selling
class=class="str">"cmt">//+------------------------------------------------------------------+
C_Terminal                 Terminal;
C_VolumeAtPrice VolumeAtPrice;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
        Terminal.Init();
        VolumeAtPrice.Init(user2, user3, user0, user1);
        EventSetTimer(class="num">1);
        
        class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double &price[])
{
        class="kw">return rates_total;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTimer()
{
        VolumeAtPrice.Update();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam)
{
        VolumeAtPrice.DispatchMessage(id, sparam);
        ChartRedraw();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
{
        EventKillTimer();
}
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 用条件编译让同一套逻辑在EA和指标里各跑各的

想把 Times & Trade 这种原本在子窗口里画内容的逻辑做成指标,直接搬代码会出乱子:指标窗口里会堆出一坨用户删都删不明白的残留,体验很差。正确路子是用条件编译把 EA 专用和指标专用的片段隔开,编译时由预定义宏决定走哪条分支。 MQL5 里靠 #define 配合 #ifdef / #else / #endif 实现这点。EA 侧声明了 def_INTEGRATION_WITH_EA,编译器就只编进 #ifdef 到 #else 之间的类代码;指标侧不声明这个宏,那段就被整体跳过,只编 #else 到 #endif 的内容。 老手的习惯是:不确定有没有用的代码先留着,别随手删,靠注释掉预定义行来切换。比如把 EA 里 #define def_INTEGRATION_WITH_EA 那行前面加 // 变成注释,编译器就当宏不存在,C_TimesAndTrade 里对应分支直接不进目标文件。这样一份类代码同时喂给 EA 和指标,省掉维护两份文件的成本。 下面这段是指标侧的完整框架代码,注意高亮行 SubWin = ChartWindowFind(); 在 EA 里是没有的——它负责让指标知道自己待在哪个子窗口,避免绘图坐标错乱。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property indicator_separate_window
class="macro">#class="kw">property indicator_plots class="num">0
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <NanoEA-SIMD\Tape Reading\C_TimesAndTrade.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
C_Terminal         Terminal;
C_TimesAndTrade    TimesAndTrade;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">input class="type">int     user1 = class="num">2;       class=class="str">"cmt">//Scale
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool isConnecting = false;
class="type">int SubWin;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
        IndicatorSetString(INDICATOR_SHORTNAME, "Times & Trade");
        SubWin = ChartWindowFind();
        Terminal.Init();
        TimesAndTrade.Init(user1);
        EventSetTimer(class="num">1);
        
        class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double &price[])
{
        if (isConnecting)
                TimesAndTrade.Update();
        class="kw">return rates_total;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTimer()
{
        if (TimesAndTrade.Connect())
        {
                isConnecting = true;
                EventKillTimer();
        }
}
class=class="str">"cmt">//+------------------------------------------------------------------+

图表事件与模块化集成的收口写法

把指标和 EA 拆开写时,图表尺寸变化是最容易被忽略的坑。上面这段在 OnChartEvent 里只处理 CHARTEVENT_CHART_CHANGE:窗口一缩放,就调 Terminal.Resize() 和 TimesAndTrade.Resize() 重排缓冲区,避免成交量分布图错位。外汇与贵金属波动快,图表频繁拖拽下不重排会直接画花,属于高概率踩雷点。 宏 def_INTEGRATION_WITH_EA 控制着两套包含路径。定义了它,才把 C_Wallpaper、C_VolumeAtPrice、C_TimesAndTrade 这些 Tape Reading 模块编进去;没定义就只留裸类,方便单独测指标逻辑。这种条件编译在 MT5 里能省掉约 3 个无关头文件的编译开销。 类里 def_SizeBuff 给的是 2048,macro_Limits 用 0xFF 做按位截断,def_MaxInfos 上限 257——这些数直接决定回放逐笔成交时内存占用量。开 MT5 把 NanoEA-SIMD 这套挂上,改 def_SizeBuff 到 4096 看子窗口重绘帧率变化,比读文档来得实在。

MQL5 / C++
class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam)
{
      class="kw">switch (id)
      {
            case CHARTEVENT_CHART_CHANGE:
                  Terminal.Resize();
                  TimesAndTrade.Resize();
            class="kw">break;
      }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
{
      EventKillTimer();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_INTEGRATION_WITH_EA
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <NanoEA-SIMD\Trade\Control\C_IndicatorTradeView.mqh>
class="macro">#ifdef def_INTEGRATION_WITH_EA
      class="macro">#include <NanoEA-SIMD\Auxiliar\C_Wallpaper.mqh>
      class="macro">#include <NanoEA-SIMD\Tape Reading\C_VolumeAtPrice.mqh>
      class="macro">#include <NanoEA-SIMD\Tape Reading\C_TimesAndTrade.mqh>
class="macro">#endif
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <NanoEA-SIMD\Auxiliar\C_Canvas.mqh>
class="macro">#ifdef def_INTEGRATION_WITH_EA
class="macro">#include <NanoEA-SIMD\Auxiliar\C_FnSubWin.mqh>
class C_TimesAndTrade : class="kw">private C_FnSubWin
class="macro">#else
class C_TimesAndTrade
class="macro">#endif
{
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_SizeBuff class="num">2048
class="macro">#define macro_Limits(A) (A & 0xFF)
class="macro">#define def_MaxInfos class="num">257
class="macro">#define def_ObjectName "TimesAndTrade"
class=class="str">"cmt">//+------------------------------------------------------------------+
      class="kw">private :
            class="type">class="kw">string  m_szCustomSymbol;
class=class="str">"cmt">// ... The rest of the class code....
}

常见问题

把核心交易逻辑从 EA 主循环拆出去,用独立模块处理异常捕获与重连;EA 只负责调度,崩了也能由外层重启恢复。
把逐价成交量计算写成独立指标,EA 通过 iCustom 或全局变量读取;这样看图就能诊断,不占用 EA 回测资源。
小布可对比 EA 与指标的条件编译分支,标出参数和判断差异,并提示哪些收口写法可能导致图表事件漏处理。
图表事件处理必须统一在模块出口收口,避免 EA 与指标重复绑定;用宏隔离后编译前先跑一次差异比对。
在图表上挂拆出的指标,手动触发拖拽/点击等事件,看日志是否只进一次收口函数;漏触发就补事件路由。