轻松快捷开发 MetaTrader 程序的函数库(第 二十四部分):基准交易类 - 无效参数自动纠正(基础篇)
◍ 给基准交易类加一层参数容错
在 MQL5 里写基准交易类时,最容易被忽略的是参数合法性校验。传入的手数、止损点数、魔术码如果越界,EA 在实盘可能直接报错退出,而不是按你预想的逻辑走。 这一节的做法是:在类内部对无效参数做自动纠正,而不是抛异常。比如手数小于最小交易单位时,直接拉回平台允许的最小值;止损距离不足时,按品种规范补到最小止损位数。这样 EA 在极端行情或参数误配下仍能保活。 原示例发布于 2020-01-27,在 MT5 官方代码库累计浏览 3268 次,说明这类容错封装对实战开发者有真实需求。外汇与贵金属杠杆高、点差跳变频繁,参数容错倾向降低断线风险,但无法消除交易本身的亏损概率。 核心代码段演示了纠正逻辑: [CODE] //+------------------------------------------------------------------+
| // | 纠正手数参数 |
|---|
//+------------------------------------------------------------------+ double NormalizeLots(double lots) { double min_lot=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN); double max_lot=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX); double lot_step=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP); if(lots<min_lot) lots=min_lot; // 小于最小手数则拉回最小值 if(lots>max_lot) lots=max_lot; // 大于最大手数则截断 lots=MathRound(lots/lot_step)*lot_step; // 按步长取整 return NormalizeDouble(lots,_Digits); // 按品种精度规范化 } [/CODE] 把这段直接塞进你的 CTrade 派生类,开 MT5 用 EURUSD 把 lots 传 0.0001 跑一下,会看到返回被纠正成 0.01(多数券商最小手数)。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 纠正手数参数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double NormalizeLots(class="type">class="kw">double lots) { class="type">class="kw">double min_lot=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN); class="type">class="kw">double max_lot=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX); class="type">class="kw">double lot_step=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP); if(lots<min_lot) lots=min_lot; class=class="str">"cmt">// 小于最小手数则拉回最小值 if(lots>max_lot) lots=max_lot; class=class="str">"cmt">// 大于最大手数则截断 lots=MathRound(lots/lot_step)*lot_step; class=class="str">"cmt">// 按步长取整 class="kw">return NormalizeDouble(lots,_Digits); class=class="str">"cmt">// 按品种精度规范化 }
给交易请求的错误留一条活路
上一节我们在交易类里加了参数有效性检查,发现无效就直接带错误消息退出。这种做法能拦住 EA 故意乱发单拖垮服务器,但太死板——它完全没给 EA 自己纠错的机会。 更合理的思路是:检测到订单参数出错时,让 EA 按既定逻辑选一条路走。比如直接退出交给你写的处理程序;或者发现值能修就当场修好再发;再或者暂停一下重发,甚至原样重发碰碰运气。 错误处理的走向至少有五种可验证分支:用户不消除错误源就别想继续跑;发不出去就退出方法;修完无效值发修正单;条件好转后发原始参数单;等报价更新再发原始参数单。 本节要落地的,是一个交易订单出错处理程序,它查错误和来源后返回处理方式:禁交易、中断、纠正参数、发原始请求、等一会重发(临时)、建延后请求(下篇再做)。外汇与贵金属杠杆高,这类自动纠错逻辑若参数边界没设严,可能放大滑点与异常单风险,上 MT5 前务必在策略测试器跑边界用例。
「EA 在禁交易环境下的行为分支」
当服务器端彻底禁止交易时,EA 发起的任何下单请求都不会被接受,此时它只能退化为分析助手。实务上应在首次交易尝试就置一个全局标志,识别到「不可能交易」后直接阻断后续请求,避免无意义的服务器交互。 错误出现时,交易方法应立刻退出,把参数交回给用户,允许其用同样参数手动重试。对于无效参数,先汇编一张错误清单,遍历后由参数检查返回对应的行为代码:若错误不可修复(例如价位落在冻结区),返回退出码——重发订单也不会有正反馈。 可修复错误则调用纠正方法,并返回三种行为之一:「等待并重复」「更新数据并重复」「创建延后请求」。前两种出现在行情贴近止损/激活价、或本地价格已过时需重取报价的情况;但若交易方法不允许长占线程,就应改用延后请求——把参数、等待时长、重复次数打包丢给后台。 延后请求本质上统一了前两种行为(最小等待与指定等待),也顺手释放了主方法。其完整实现留待后续,但思路已明确:用异步队列替代阻塞等待。 顺带一提,此前文章里的交易事件跟踪有漏报:一次下两笔挂单时,第二笔因与倒数第二笔事件类型相同而被函数库忽略。本周先加一个简单标志通知程序「有事件发生」,下篇再补齐同刻事件全量列表,对齐帐户和品种集合事件的做法。
◍ 把交易事件挂到基准对象上
函数库里所有对象都继承自同一个基准对象,这个基准对象带着事件列表和 IsEvent() 标志读取方法。把交易事件统一塞进基准对象的事件列表,意味着任意派生类都能直接问一句“我这边发生事件没”,不用各自维护一套状态机。 要让别的类也能主动标记“交易事件发生了”,得在 BaseObj.mqh 的 CEventBaseObj 里加一个 SetEvent() 入口。事件标志平时由类自动置位,但跨类触发时必须能从外部写进去,否则品种集合里的对象永远读不到对方的动作。 CEventsCollection 每次在计时器里扫完历史与在场订单,就会生成事件描述并压进基准事件列表。这里有个细节:Refresh() 里必须先清空旧基准事件列表,再打上“列表已排序”标志,否则上一次的残留事件会和本轮混在一起,导致读取数量虚高。 对外暴露两个公开方法就够了:按索引取基准事件对象、返回新事件总数。取对象时传 false 给标志参数,方法内部会重置标志并检查越界,索引不存在直接返 NULL;若传 true 则返回末事件,一般用不上。事件总数就是基准列表的 Total() 大小,属于 O(1) 读取,不会拖慢 EA 的tick处理。 引擎层 CEngine 只需薄封装,把调用转给交易事件集合类的同名方法。这样程序里任一处都能拉出“本周期发生了几件交易事、都是什么”,后续做批量事件分发就有了抓手。
class=class="str">"cmt">//--- Set/class="kw">return the occurred event flag to the object data class="type">void SetEvent(const class="type">bool flag) { this.m_is_event=flag; } class="type">bool IsEvent(class="type">void) const { class="kw">return this.m_is_event; } class=class="str">"cmt">//--- Return(class="num">1) the last trading event on an account, (class="num">2) base event object by index and(class="num">3) number of new events ENUM_TRADE_EVENT GetLastTradeEvent(class="type">void) const { class="kw">return this.m_trade_event; } CEventBaseObj *GetTradeEventByIndex(const class="type">int index) { class="kw">return this.GetEvent(index,false); } class="type">int GetTradeEventsTotal(class="type">void) const { class="kw">return this.m_list_events.Total(); } class=class="str">"cmt">//--- Reset the last trading event class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Update the event list | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEventsCollection::Refresh(CArrayObj* list_history, CArrayObj* list_market, CArrayObj* list_changes, CArrayObj* list_control, const class="type">bool is_history_event, const class="type">bool is_market_event, const class="type">int new_history_orders,
事件触发后如何清空并重建列表
这段逻辑出现在交易事件回调里,核心动作是先判空再清场。若历史列表或市场列表为空指针,函数直接 return,避免后续对空对象操作导致 MT5 终端报错崩溃。 清场时先把实例内的事件标志 m_is_event 置为 false,随后调用 m_list_events.Clear() 清空事件容器,再用 Sort() 对空列表做一次排序复位。这两步保证了每次新事件进来前,旧事件不会残留干扰判断。 若 is_market_event 为真,则向订单对象推送 SendEvent(),并通过 CBaseObj::EventAdd 把该订单的 ticket、价格、品种写入基础交易事件对象。注意这里直接取 order.Ticket() 和 order.Price(),若订单已离场可能取到 0,实盘前应在调用前确认订单有效性。 类还暴露了三个取数接口:GetListAllOrdersEvents 返回全部订单事件数组指针;GetTradeEventByIndex 按索引拿基础事件对象;GetTradeEventsTotal 回传新交易事件总数。外汇与贵金属波动剧烈,这类事件计数只反映终端推送量,不代表盈利可能,验证时请在策略测试器里跑一小段 tick 看计数是否符合预期。
const class="type">int new_market_pendings, const class="type">int new_market_positions, const class="type">int new_deals, const class="type">class="kw">double changed_volume) { class=class="str">"cmt">//--- Exit if the lists are empty if(list_history==NULL || list_market==NULL) class="kw">return; class=class="str">"cmt">//--- this.m_is_event=false; this.m_list_events.Clear(); this.m_list_events.Sort(); class=class="str">"cmt">//--- If the event is in the market environment if(is_market_event) { event.SendEvent(); CBaseObj::EventAdd(this.m_trade_event,order.Ticket(),order.Price(),order.Symbol()); class=class="str">"cmt">//--- Return(class="num">1) the list of order, deal and position events, (class="num">2) base trading event object by index and the(class="num">3) number of new trading events CArrayObj *GetListAllOrdersEvents(class="type">void) { class="kw">return this.m_events.GetList(); } CEventBaseObj *GetTradeEventByIndex(const class="type">int index) { class="kw">return this.m_events.GetTradeEventByIndex(index); } class="type">int GetTradeEventsTotal(class="type">void) const { class="kw">return this.m_events.GetTradeEventsTotal(); } class=class="str">"cmt">//--- Reset the last trading event
「把交易请求报错拆成可执行的纠正分支」
在自建 EA 交易库里,交易请求出错不能只靠日志打印。我们把错误分成 0、1、2、4 四个标记:0 表示订单可直接发;1 是严重错误,EA 应切到纯分析助手模式不再下单;2 是函数库内部失败,中断后续交易逻辑避免崩类;4 是可修复错误,写进错误列表并调修复方法。这样检查函数返回的就是「怎么处理」而不是 bool。 MQL5 的 MqlTradeResult 自带 retcode 和 comment,发单后直接读就行;MQL4 没有,要用 GetLastError() 补结构字段。多平台库里我们在发送后手动填充,保证两种语言下都能用类方法查请求结果。 StopLevel 是个坑:SymbolInfoInteger(SYMBOL_TRADE_STOPS_LEVEL) 返回 0 不代表停止单可贴价,只代表级别浮动。实际常用两倍点差做平滑偏移,CTradeObj 里加 m_spread_mult 成员变量和 SetSpreadMultiplier() 方法,传 0 时自动置 1。 订单类型常量除以 2 的余数恒为 0(ORDER_TYPE_BUY)或 1(ORDER_TYPE_SELL),CAccount 算保证金时先转成这两值之一,MQL4/MQL5 调用就统一了。 开仓方法里 MQL5 原样返 OrderSend() 结果;MQL4 查票据,返回 -1 就填错误码、现价、错误描述进结果结构并返 false。检测资金不足不再就地打印,而是置错误列表标志,由统一错误处理决定「自动纠正」还是「中止尝试」。 手数计算先试最小手,不行直接返 0;接着用 OrderCalcMargin() 估大约可用量,再算倾尽允许的最大手。若该函数返 EMPTY_VALUE(对应 DBL_MAX),说明出错,用最大可能量减手数步长「选」出安全值,循环从近值起减迭代次数。实测中一手间距仍可能算出无效量,需留修正空间。 下面这段是库内新增的消息索引片段,标记了「无效请求」与「EA 禁用交易」两类关键文本,对应错误列表的展示与禁用标志: MSG_LIB_TEXT_INVALID_REQUEST 对应 Invalid request 的终端提示,MSG_LIB_TEXT_TRADING_DISABLE 在检测到账户禁交易时点亮,EA 此后不再发无用单。
MSG_LIB_TEXT_REQUEST_REJECTED_DUE, class=class="str">"cmt">// Request was rejected before sending to the server due to: MSG_LIB_TEXT_INVALID_REQUEST, class=class="str">"cmt">// Invalid request: MSG_LIB_TEXT_NOT_ENOUTH_MONEY_FOR, class=class="str">"cmt">// Insufficient funds for performing a trade MSG_LIB_TEXT_UNSUPPORTED_PRICE_TYPE_IN_REQ, class=class="str">"cmt">// Unsupported price parameter type in a request MSG_LIB_TEXT_TRADING_DISABLE, class=class="str">"cmt">// Trading disabled for the EA until the reason is eliminated