轻松快捷开发 MetaTrader 程序的函数库(第 二十四部分):基准交易类 - 无效参数自动纠正·进阶篇
🛠️

轻松快捷开发 MetaTrader 程序的函数库(第 二十四部分):基准交易类 - 无效参数自动纠正·进阶篇

(2/3)· 当参数出错就中断交易太浪费,这层纠正逻辑决定 EA 能否在错误中继续下单

含代码示例实战向 第 2/3 篇
很多 EA 一旦检测到订单参数无效就直接带错误退出,等于把可修复的机会全扔掉。服务器没禁止交易、行情也没走坏,单纯因为止损价差了几点就放弃操作,这种硬中断在实盘里很常见也很亏。把纠正逻辑写进交易类,比每次手动排错更省心。

「EA 报错处理的枚举与标志位」

在 MT5 的 EA 交易库里,报错信息先用一组文本常量做双语映射,再交给行为枚举决定下一步动作。下面这段枚举定义了三种应对方式:直接中断、修正参数、或挂起请求等待重发。 enum ENUM_ERROR_HANDLING_BEHAVIOR { ERROR_HANDLING_BEHAVIOR_BREAK, // 中止本次交易尝试 ERROR_HANDLING_BEHAVIOR_CORRECT, // 修正无效参数后重试 ERROR_HANDLING_BEHAVIOR_PENDING_REQUEST, // 创建挂起请求 }; 真正区分错误严重程度的是 ENUM_TRADE_REQUEST_ERR_FLAGS,用位标记组合。TRADE_REQUEST_ERR_FLAG_NO_ERROR 值为 0 表示正常;FATAL_ERROR 为 1 会直接禁掉 EA 交易权限;INTERNAL_ERR 为 2 是库内部错误也退出;ERROR_IN_LIST 为 4 才进入可处理的错误清单。 enum ENUM_TRADE_REQUEST_ERR_FLAGS { TRADE_REQUEST_ERR_FLAG_NO_ERROR = 0, // 无错误 TRADE_REQUEST_ERR_FLAG_FATAL_ERROR = 1, // 致命错误:禁用EA并退出 TRADE_REQUEST_ERR_FLAG_INTERNAL_ERR = 2, // 库内部错误:退出 TRADE_REQUEST_ERR_FLAG_ERROR_IN_LIST = 4, // 列表内错误:按方法处理 }; 开 MT5 把这两个枚举抄进你的 include 头文件,用按位与判断 (flags & TRADE_REQUEST_ERR_FLAG_FATAL_ERROR) 就能在实盘前拦掉会爆仓的致命路径。外汇与贵金属杠杆高,错误分支漏判可能让仓位失控,建议先在策略测试器跑一遍报错注入。

MQL5 / C++
enum ENUM_ERROR_HANDLING_BEHAVIOR
  {
   ERROR_HANDLING_BEHAVIOR_BREAK,      class=class="str">"cmt">// Abort trading attempt
   ERROR_HANDLING_BEHAVIOR_CORRECT,    class=class="str">"cmt">// Correct invalid parameters
   ERROR_HANDLING_BEHAVIOR_PENDING_REQUEST, class=class="str">"cmt">// Create a pending request
  };

enum ENUM_TRADE_REQUEST_ERR_FLAGS
  {
   TRADE_REQUEST_ERR_FLAG_NO_ERROR           = class="num">0,  class=class="str">"cmt">// No error
   TRADE_REQUEST_ERR_FLAG_FATAL_ERROR        = class="num">1,  class=class="str">"cmt">// Disable trading for an EA(critical error) - exit
   TRADE_REQUEST_ERR_FLAG_INTERNAL_ERR       = class="num">2,  class=class="str">"cmt">// Library internal error - exit
   TRADE_REQUEST_ERR_FLAG_ERROR_IN_LIST      = class="num">4,  class=class="str">"cmt">// Error in the list - handle(ENUM_ERROR_CODE_PROCESSING_METHOD)
  };

EA 内部错误分流与核心结构体的落地定义

写 EA 最怕报错后全靠人工盯盘重启。把错误处理方式先枚举出来,后面逻辑才好分叉:正常放行、禁交易、退出方法、刷新重跑、等待重跑、挂单待处理,共 6 种状态对应不同分支。 下面这段是 EA 类里直接落地的成员声明,决定了后续取价、发单、算保证金走哪套接口。注意 m_chart_mode 控制 K 线用买价还是卖价拼,黄金和美系货币对常因点差跳变导致两者偏差超 0.5 点,回测和实盘要分开验。 结构体里 MqlTradeRequest 和 MqlTradeResult 是 OrderSendAsync 的标配,m_magic 建议按品种+周期编码,避免多 EA 同账户抢单。外汇与贵金属杠杆高,保证金模式 m_margin_mode 取错会直接算爆仓线。

MQL5 / C++
enum ENUM_ERROR_CODE_PROCESSING_METHOD
  {
   ERROR_CODE_PROCESSING_METHOD_OK,                                          class=class="str">"cmt">// No errors
   ERROR_CODE_PROCESSING_METHOD_DISABLE,                                     class=class="str">"cmt">// Disable trading for the EA
   ERROR_CODE_PROCESSING_METHOD_EXIT,                                        class=class="str">"cmt">// Exit the trading method
   ERROR_CODE_PROCESSING_METHOD_REFRESH,                                     class=class="str">"cmt">// Update data and repeat
   ERROR_CODE_PROCESSING_METHOD_WAIT,                                        class=class="str">"cmt">// Wait and repeat
   ERROR_CODE_PROCESSING_METHOD_PENDING,                                     class=class="str">"cmt">// Create a pending request
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
   SActions                m_datas;
   class="type">MqlTick                 m_tick;                                            class=class="str">"cmt">// Tick structure for receiving prices
   class="type">MqlTradeRequest         m_request;                                         class=class="str">"cmt">// Trade request structure
   class="type">MqlTradeResult          m_result;                                          class=class="str">"cmt">// trade request execution result
   ENUM_SYMBOL_CHART_MODE  m_chart_mode;                                      class=class="str">"cmt">// Price type for constructing bars
   ENUM_ACCOUNT_MARGIN_MODE m_margin_mode;                                    class=class="str">"cmt">// Margin calculation mode
   ENUM_ORDER_TYPE_FILLING  m_type_filling;                                   class=class="str">"cmt">// Filling policy
   ENUM_ORDER_TYPE_TIME     m_type_expiration;                                class=class="str">"cmt">// Order expiration type
   class="type">int                     m_symbol_expiration_flags;                         class=class="str">"cmt">// Flags of order expiration modes for a trading object symbol
   class="type">ulong                   m_magic;                                           class=class="str">"cmt">// Magic number

◍ 交易对象里的私有字段与价差乘数接口

在封装交易对象的类里,一组私有成员决定了发单时的基本行为。m_symbol 存交易品种,m_volume 是手数,m_deviation 以点为单位的滑点容忍,m_expiration 仅对指定时间的挂单生效。 m_async_mode 控制是否异步发送交易请求,开 true 时下单不阻塞 EA 主线程;m_log_level 管日志粒度,m_use_sound 决定是否播放交易事件音效。 m_multiplier 是相对经纪商 StopLevel 调整挂单距离的价差乘数,源码里标黄的那行 uint m_multiplier 即为声明。它和下面标黄的 SetSpreadMultiplier 方法配套:传入 0 时自动归为 1,避免除零或失效。 实盘里若 broker 的 StopLevel 是 10 点、m_multiplier 设为 3,则挂单距离倾向拉到 30 点以外。外汇与贵金属杠杆高,参数误设可能直接报『离市价过近』拒单,建议在 MT5 策略测试器里先改这个值看 return 的 placement 距离。

MQL5 / C++
  class="type">class="kw">string                 m_symbol;                                                                       class=class="str">"cmt">// Symbol
  class="type">class="kw">string                 m_comment;                                                                       class=class="str">"cmt">// Comment
  class="type">ulong                  m_deviation;                                                                     class=class="str">"cmt">// Slippage in points
  class="type">class="kw">double                 m_volume;                                                                        class=class="str">"cmt">// Volume
  class="type">class="kw">datetime               m_expiration;                                                                    class=class="str">"cmt">// Order expiration time(for ORDER_TIME_SPECIFIED type order)
  class="type">bool                   m_async_mode;                                                                    class=class="str">"cmt">// Flag of asynchronous sending of a trade request
  ENUM_LOG_LEVEL         m_log_level;                                                                     class=class="str">"cmt">// Logging level
  class="type">int                    m_stop_limit;                                                                    class=class="str">"cmt">// Distance of placing a StopLimit order in points
  class="type">bool                   m_use_sound;                                                                     class=class="str">"cmt">// The flag of using sounds of the object trading events
  class="type">uint                   m_multiplier;                                                                    class=class="str">"cmt">// The spread multiplier to adjust levels relative to StopLevel

class="kw">public:
class=class="str">"cmt">//--- Constructor
                      CTradeObj();
class=class="str">"cmt">//--- Set/class="kw">return the spread multiplier
  class="type">void                 SetSpreadMultiplier(const class="type">uint value)     { this.m_multiplier=(value==class="num">0 ? class="num">1 : value); }

「交易对象的初始化与默认参数」

CTradeObj 的构造函数把一批下单相关的默认值一次性固化,省得每次发单前重复赋值。magic 默认 0、偏差点差 5、止损限价 0、过期时间 0、异步模式关、填充方式 ORDER_FILLING_FOK、有效期 ORDER_TIME_GTC,这些直接写进成员初始化列表。 保证金模式在构造时按平台分支判定:MQL5 下读 ACCOUNT_MARGIN_MODE 整数,MQL4 则硬编码为 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING。点差乘数 m_multiplier 固定为 1,注释里标了 Spread multiplier,后续若要做滑点放大测试改这一行即可。 SetResultRetcode 与 SetResultComment 是两个极简 setter,只把外部回传的 retcode 和字符串写进 m_result,方便在自定义交易类里追踪最近一次请求结果。外汇与贵金属杠杆高,这类封装若误设 FOK 填充,在流动性薄时可能直接拒单。 下面这段是构造函数的核心骨架,成员初始化和平台宏判断都在其中。

MQL5 / C++
class="type">uint SpreadMultiplier(class="type">void) const { class="kw">return this.m_multiplier; }
class=class="str">"cmt">//--- Set class="kw">default values
class=class="str">"cmt">//--- Set the error code in the last request result
class="type">void SetResultRetcode(const class="type">uint retcode) { this.m_result.retcode=retcode; }
class="type">void SetResultComment(const class="type">class="kw">string comment) { this.m_result.comment=comment; }
class=class="str">"cmt">//--- Data on the last request result:
CTradeObj::CTradeObj(class="type">void) : m_magic(class="num">0),
              m_deviation(class="num">5),
              m_stop_limit(class="num">0),
              m_expiration(class="num">0),
              m_async_mode(false),
              m_type_filling(ORDER_FILLING_FOK),
              m_type_expiration(ORDER_TIME_GTC),
              m_comment(::MQLInfoString(MQL_PROGRAM_NAME)+" by DoEasy"),
              m_log_level(LOG_LEVEL_ERROR_MSG)
  {
  class=class="str">"cmt">//--- Margin calculation mode
  this.m_margin_mode=
    (
      class="macro">#ifdef __MQL5__(ENUM_ACCOUNT_MARGIN_MODE)::AccountInfoInteger(ACCOUNT_MARGIN_MODE)
      class="macro">#else class=class="str">"cmt">/* MQL4 */ ACCOUNT_MARGIN_MODE_RETAIL_HEDGING class="macro">#endif
    );
  class=class="str">"cmt">//--- Spread multiplier
  this.m_multiplier=class="num">1;
  class=class="str">"cmt">//--- Set class="kw">default sounds and flags of using sounds
  this.m_use_sound=false;
  this.InitSounds();
  }

交易对象初始化与开仓接口的内部绑定

CTradeObj 的 Init 把交易所需的符号、魔术码、手数、滑点容差、挂单有效期等十来个参数一次性收口到实例成员里。注意最后三行并不是简单赋值:它实时去查 SymbolInfoInteger 拿 SYMBOL_EXPIRATION_MODE 标记,又把 m_volume 强制重置成 SymbolInfoDouble 返回的 SYMBOL_VOLUME_MIN,这意味着你传进来的 volume 参数在 Init 里会被最小交易量覆盖掉,真正下单手数得靠后续 SetVolume 再改。 MQL5 环境下 m_chart_mode 通过预编译宏走 SymbolInfoInteger(SYMBOL_CHART_MODE),而 MQL4 分支直接写死 SYMBOL_CHART_MODE_BID。这种写法保证跨版本编译不报错,但在回测里如果图表模式取不到,MQL4 会默认按买价画图,可能与你肉眼看的卖价线错位。 OpenPosition 只暴露 type、volume、sl、tp、magic、comment 几个常用项,sl/tp 默认 0 表示不挂止损止盈。想在 MT5 验证的话,把这段代码塞进 EA 的 CTradeObj 类,Init 后打印 m_volume,你会看到它等于该品种 SYMBOL_VOLUME_MIN(比如 XAUUSD 常见 0.01),而不是你传的 volume。外汇与贵金属杠杆高,最小手数约束和滑点容差会直接吃掉窄止损策略的空间,参数需按实盘品种重校。

MQL5 / C++
class="type">void CTradeObj::Init(const class="type">class="kw">string symbol,
                     const class="type">ulong magic,
                     const class="type">class="kw">double volume,
                     const class="type">ulong deviation,
                     const class="type">int stoplimit,
                     const class="type">class="kw">datetime expiration,
                     const class="type">bool async_mode,
                     const ENUM_ORDER_TYPE_FILLING type_filling,
                     const ENUM_ORDER_TYPE_TIME type_expiration,
                     ENUM_LOG_LEVEL log_level)
  {
   this.SetSymbol(symbol);
   this.SetMagic(magic);
   this.SetDeviation(deviation);
   this.SetVolume(volume);
   this.SetExpiration(expiration);
   this.SetTypeFilling(type_filling);
   this.SetTypeExpiration(type_expiration);
   this.SetAsyncMode(async_mode);
   this.SetLogLevel(log_level);
   this.m_symbol_expiration_flags=(class="type">int)::SymbolInfoInteger(this.m_symbol,SYMBOL_EXPIRATION_MODE);
   this.m_volume=::SymbolInfoDouble(this.m_symbol,SYMBOL_VOLUME_MIN);
   this.m_chart_mode=class="macro">#ifdef __MQL5__(ENUM_SYMBOL_CHART_MODE)::SymbolInfoInteger(this.m_symbol,SYMBOL_CHART_MODE) class="macro">#else SYMBOL_CHART_MODE_BID class="macro">#endif ;
  }

class="type">bool CTradeObj::OpenPosition(const class="type">ENUM_POSITION_TYPE type,
                             const class="type">class="kw">double volume,
                             const class="type">class="kw">double sl=class="num">0,
                             const class="type">class="kw">double tp=class="num">0,
                             const class="type">ulong magic=ULONG_MAX,
                             const class="type">class="kw">string comment=NULL,

◍ 下单请求封装里的同步异步分叉

这段代码把 MT5 市价单的发送逻辑收口到一个方法里:先抢一次 SymbolInfoTick 拿实时报价,失败就写错误码并退回 false,避免拿不到价格就盲发请求。 请求结构体逐字段填充,price 的取值按持仓方向和多窗口模式切换——买用 ask,卖在 SYMBOL_CHART_MODE_BID 时取 bid 否则取 last,这种细节在交叉盘和衍生品上容易踩坑。 #ifdef __MQL5__ 分支下,根据 m_async_mode 决定走 OrderSend 还是 OrderSendAsync:同步等回执,异步丢给交易池。外汇与贵金属杠杆高,异步发单若不做后续回执轮询,滑点可能超出你预设的 deviation(默认 ULONG_MAX 会回落到实例级偏差值)。 #else 分支是 MQL4 兼容写法,用老式多参 OrderSend 拿 ticket,再手动 SymbolInfoTick 回填 ask/bid 和 deal,注意这里 price 回退用了 OrderSelect 查开盘价,查不到才用请求价。

MQL5 / C++
const class="type">ulong deviation=ULONG_MAX)
  {
   ::ResetLastError();
   class=class="str">"cmt">//--- If failed to get the current prices, write the error code and description, send the message to the journal and class="kw">return &class="macro">#x27;false&class="macro">#x27;
   if(!::SymbolInfoTick(this.m_symbol,this.m_tick))
     {
      this.m_result.retcode=::GetLastError();
      this.m_result.comment=CMessage::Text(this.m_result.retcode);
      if(this.m_log_level>LOG_LEVEL_NO_MSG)
        ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_NOT_GET_PRICE),CMessage::Text(this.m_result.retcode));
      class="kw">return false;
     }
   class=class="str">"cmt">//--- Clear the structures
   ::ZeroMemory(this.m_request);
   ::ZeroMemory(this.m_result);
   class=class="str">"cmt">//--- Fill in the request structure
   this.m_request.action   = TRADE_ACTION_DEAL;
   this.m_request.symbol   = this.m_symbol;
   this.m_request.magic    = (magic==ULONG_MAX ? this.m_magic : magic);
   this.m_request.type     = OrderTypeByPositionType(type);
   this.m_request.price    = (type==POSITION_TYPE_BUY ? this.m_tick.ask : (this.m_chart_mode==SYMBOL_CHART_MODE_BID ? this.m_tick.bid : this.m_tick.last));
   this.m_request.volume   = volume;
   this.m_request.sl       = sl;
   this.m_request.tp       = tp;
   this.m_request.deviation= (deviation==ULONG_MAX ? this.m_deviation : deviation);
   this.m_request.comment  = (comment==NULL ? this.m_comment : comment);
   class=class="str">"cmt">//--- Return the result of sending a request to the server
class="macro">#ifdef __MQL5__
   class="kw">return(!this.m_async_mode ? ::OrderSend(this.m_request,this.m_result) : ::OrderSendAsync(this.m_request,this.m_result));
class="macro">#else 
   ::ResetLastError();
   class="type">int ticket=::OrderSend(m_request.symbol,m_request.type,m_request.volume,m_request.price,(class="type">int)m_request.deviation,m_request.sl,m_request.tp,m_request.comment,(class="type">int)m_request.magic,m_request.expiration,clrNONE);
   if(ticket!=WRONG_VALUE)
     {
      ::SymbolInfoTick(this.m_symbol,this.m_tick);
      this.m_result.retcode=::GetLastError();
      this.m_result.ask=this.m_tick.ask;
      this.m_result.bid=this.m_tick.bid;
      this.m_result.deal=ticket;
      this.m_result.price=(::OrderSelect(ticket,SELECT_BY_TICKET) ? ::OrderOpenPrice() : this.m_request.price);
把重复劳动交给小布
这些参数校验与纠正分支如果手搓容易漏边界,小布盯盘的 AIGC 已内置常见订单错误模式识别,打开对应品种页即可对照你 EA 的返回码快速定位是哪类纠正被触发。

常见问题

纠正后发送修复订单只产生一次有效请求;硬中断则把判断责任丢给用户,反复手动发单反而更容易造成冗余连接。外汇贵金属高波动下,自动纠偏能减少无效往返。
通常在首次尝试交易返回服务器端完全禁交易的结果时置位,之后所有交易请求短路返回,EA 退化为分析助手,避免持续试探。
等待并重发是暂停片刻用原参数再试,假设行情条件可能改善;延后请求是把订单挂起由后续周期处理,属于跨 tick 的临时方案,本篇先铺垫前一种。
可以,把 EA 日志里的交易方法行为代码贴进小布盯盘对应品种页,它能按本文的纠正分类帮你标出是退出、修复还是等待重发,省去翻源码。
接近激活位时立即修改止损可能失败,倾向先归为等待并更新报价后重发,具体要看错误来源与行情距离的概率判断。