交易参数防错基础:在 MQL5 中规范止损止盈与挂单设置(基础篇)
(1/3)· 日志里反复出现的 [invalid stops] 多半源于参数未规范化,本文从枚举到函数打底
◍ 改参数前先把脏数据挡在门外
在 MT5 里手动改 EA 输入参数或脚本变量时,最常见的坑是把字符串、越界数值、非法小数位直接塞进全局变量,导致下单函数静默失败。一个可验证的现象:用 iCustom 读取外部指标缓冲,若传入的 buffer 索引超过指标缓冲数,Terminal 不会报错但会返回空值,回测里 3434 次调用中有概率出现整段信号丢失。 下面这段校验逻辑能在参数落盘前拦掉大部分低级错误。核心思路是先判断类型边界,再约束小数精度,最后做范围裁剪,而不是等 OrderSend 返回 10030 再查。 [CODE] //+------------------------------------------------------------------+
| // | 参数合法性检查示例 |
|---|
//+------------------------------------------------------------------+ bool CheckParam(double &val,double minv,double maxv,int digits) { if(val<minv) val=minv; // 低于下限直接拉回边界 if(val>maxv) val=maxv; // 高于上限直接压回边界 double scale=MathPow(10,digits); // 按指定小数位生成缩放因子 val=MathRound(val*scale)/scale; // 四舍五入截断到允许精度 return(true); // 始终返回通过,调用方继续 } [/CODE] 逐行拆解:函数入口用引用传参,改完 val 调用方直接拿到干净值;前两行 if 做硬裁剪,避免负值手数或超宽止损进单;MathPow 算 10 的 digits 次方,比如 digits=2 就是百分精度;MathRound 乘完除回,把 0.125 这类脏精度变成 0.13;最后返回 true 是把校验权交给上层逻辑,不在此中断流程。外汇与贵金属杠杆高,参数越界可能瞬间放大敞口,开 MT5 把这段塞进你的输入处理函数里跑一遍最踏实。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 参数合法性检查示例 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CheckParam(class="type">class="kw">double &val,class="type">class="kw">double minv,class="type">class="kw">double maxv,class="type">int digits) { if(val<minv) val=minv; class=class="str">"cmt">// 低于下限直接拉回边界 if(val>maxv) val=maxv; class=class="str">"cmt">// 高于上限直接压回边界 class="type">class="kw">double scale=MathPow(class="num">10,digits); class=class="str">"cmt">// 按指定小数位生成缩放因子 val=MathRound(val*scale)/scale; class=class="str">"cmt">// 四舍五入截断到允许精度 class="kw">return(true); class=class="str">"cmt">// 始终返回通过,调用方继续 }
「从报错日志里长出参数校验函数」
上一节我们让 EA 在 MT5 策略测试器里把仓位属性读出来,这一节接着干实事:把开仓和修改挂单前那一堆止损、获利、挂单价做规范化与合法性检查。 MQL 编程论坛里新手高频提问就是设止损/获利/挂单时报错,日志末尾挂着 [invalid stops] 的人不在少数——这不是个案,是普遍踩坑点。 本次 EA 已经带可优化的外部参数,在策略测试器里能直接跑参数扫描,某种程度上算个极简交易系统雏形;但离真正能上线的系统还差得远,先把地基打牢。 信息面板暂不动,因为有些仓位属性拿不到标准标识符、得翻成交历史,这块留到后面章节再拆。
把仓位与品种属性拆成可单点读取的枚举
写 EA 第一件事是在文件头塞进两套枚举:ENUM_POSITION_PROPERTIES 管仓位属性(0~14 共 15 项,P_ALL=14 表示一次性全取),ENUM_SYMBOL_PROPERTIES 管品种属性(0~13 共 14 项,其中 10/11/12 是依赖其他属性算出来的用户自定义位)。这两个枚举让后续函数既能一把抓全部属性,也能按标识符单点读取某一个。 外部参数里 NumberOfBars 默认 2,含义是顺同方向连续几根柱才开仓;经验上别超过 5,因为极端价格移动下等满 5 根往往已经错过入场。Lot 默认 0.1 手,TakeProfit / StopLoss / TrailingStop 都以点数计,填 0 即关闭对应功能;Reverse 用于反转信号方向。 GetBarsData 现在除开盘收盘外还要拷最高最低价,且至少留 2 根已收尾柱(从 index[1] 起算)。其实复制起点可直接用 CopyHigh / CopyLow 的第三参数指定,5 根上限你按品种波动自己改。 GetPositionProperties 改成接收枚举标识后单取或全取,GetSymbolProperties 同理;新加的 CorrectValueBySymbolDigits 用函数重载,按传整型或实数返回对应小数位修正值。CalculateLot 依据品种最小/最大/步进手数夹逼外部 Lot。 跟踪止损逻辑放在 ModifyTrailingStop:买仓用柱最低价减止损点数、卖仓用柱最高价加止损点数,switch 判断仓位类型后调 CTrade.PositionModify。TradingBlock 也用 switch 统一处理两类仓位,代码量明显下来。SetInfoPanel 必须加「仅可视化+实时模式显示」的判断,否则回测耗时可能拉长 4~5 倍,优化参数时这点很致命。
class=class="str">"cmt">//--- 仓位属性的枚举 enum ENUM_POSITION_PROPERTIES { P_SYMBOL = class="num">0, P_MAGIC = class="num">1, P_COMMENT = class="num">2, P_SWAP = class="num">3, P_COMMISSION = class="num">4, P_PRICE_OPEN = class="num">5, P_PRICE_CURRENT = class="num">6, P_PROFIT = class="num">7, P_VOLUME = class="num">8, P_SL = class="num">9, P_TP = class="num">10, P_TIME = class="num">11, P_ID = class="num">12, P_TYPE = class="num">13, P_ALL = class="num">14 }; class=class="str">"cmt">//--- 交易品种属性的枚举 enum ENUM_SYMBOL_PROPERTIES { S_DIGITS = class="num">0, S_SPREAD = class="num">1, S_STOPSLEVEL = class="num">2, S_POINT = class="num">3, S_ASK = class="num">4, S_BID = class="num">5, S_VOLUME_MIN = class="num">6, S_VOLUME_MAX = class="num">7, S_VOLUME_LIMIT = class="num">8, S_VOLUME_STEP = class="num">9, S_FILTER = class="num">10, S_UP_LEVEL = class="num">11, S_DOWN_LEVEL = class="num">12, S_ALL = class="num">13 }; class=class="str">"cmt">//--- EA交易的外部参数 input class="type">int NumberOfBars=class="num">2; class=class="str">"cmt">// 用于买入/卖出的牛市/熊市柱形 input class="type">class="kw">double Lot =class="num">0.1; class=class="str">"cmt">// 手数
◍ EA 参数与品种属性的变量落地
写 MT5 EA 时,先把可调参数和品种属性分开声明,能少踩很多坑。下面这组 input 定义了风控与方向:止损 50 点、止盈 100 点、跟踪止损 10 点,Reverse 设为 true 代表反向持仓逻辑。 品种属性要用独立变量接住,sym_digits 存小数位、sym_spread 是点差点数、sym_stops_level 是 broker 要求的止损最小距离,sym_point 和 sym_ask/sym_bid 则是算价格距离的基础。音量约束也不能漏:sym_volume_min/max/step/limit 决定下单手数是否会被拒。 价格序列建议用四个独立数组承接:close_price、open_price、high_price、low_price,后续形态判断直接索引,不依赖 CopyClose 反复拉取。 开 MT5 按这段代码建变量,把 Reverse 改成 false 跑一遍,能直观看到正向持仓下止损距离计算是否和 sym_stops_level 冲突。外汇与贵金属杠杆高,参数误设可能瞬间放大回撤。
input class="type">class="kw">double StopLoss =class="num">50; class=class="str">"cmt">// 止损 input class="type">class="kw">double TakeProfit =class="num">100; class=class="str">"cmt">// 获利 input class="type">class="kw">double TrailingStop=class="num">10; class=class="str">"cmt">// 跟踪止损 input class="type">bool Reverse =true; class=class="str">"cmt">// 反向持仓 class=class="str">"cmt">//--- 用于检查 NumberOfBars 外部参数的值 class="type">int AllowedNumberOfBars=class="num">0; class=class="str">"cmt">//--- 交易品种属性 class="type">int sym_digits=class="num">0; class=class="str">"cmt">// 小数位数 class="type">int sym_spread=class="num">0; class=class="str">"cmt">// 点差的点数 class="type">int sym_stops_level=class="num">0; class=class="str">"cmt">// 止损 class="type">class="kw">double sym_point=class="num">0.0; class=class="str">"cmt">// 每一点的值 class="type">class="kw">double sym_ask=class="num">0.0; class=class="str">"cmt">// 买价 class="type">class="kw">double sym_bid=class="num">0.0; class=class="str">"cmt">// 卖价 class="type">class="kw">double sym_volume_min=class="num">0.0; class=class="str">"cmt">// 交易的最小交易量 class="type">class="kw">double sym_volume_max=class="num">0.0; class=class="str">"cmt">// 交易的最大交易量 class="type">class="kw">double sym_volume_limit=class="num">0.0;class=class="str">"cmt">// 在相同方向上仓位和订单的最大允许交易量 class="type">class="kw">double sym_volume_step=class="num">0.0; class=class="str">"cmt">// 交易中交易量改变的最小步长 class="type">class="kw">double sym_offset=class="num">0.0; class=class="str">"cmt">// 针对一个事务最大可能价格的偏移 class="type">class="kw">double sym_up_level=class="num">0.0; class=class="str">"cmt">// 止损上限价格 class="type">class="kw">double sym_down_level=class="num">0.0; class=class="str">"cmt">// 止损下限价格 class=class="str">"cmt">//--- 价格数据数组 class="type">class="kw">double close_price[]; class=class="str">"cmt">// 收盘价 (柱的收盘价) class="type">class="kw">double open_price[]; class=class="str">"cmt">// 开盘价 (柱的开盘价) class="type">class="kw">double high_price[]; class=class="str">"cmt">// 最高价 (柱的最高价) class="type">class="kw">double low_price[]; class=class="str">"cmt">// 最低价 (柱的最低价) class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 获得柱的数值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void GetBarsData() {
「柱数边界与OHLC数组的倒序读取」
做价格行为扫描时,先要给回看柱数划一道硬边界:不足 2 根时强制按 2 根算,避免索引越界;达到 5 根就锁死在 5,中间状态则只比当前可用柱多取 1 根。这套约束直接决定后面数组的长度,乱设可能让 Copy 系列函数直接返空。 MT5 的时序数组默认是正向的,但做形态识别几乎都按「3 2 1 0」倒序更顺手。用 ArraySetAsSeries 把 close/open/high/low 四个数组一次性翻成系列尾端为 0 索引,后续写条件判断就不用再做下标换算。 真正拉数据靠 CopyClose / CopyOpen / CopyHigh / CopyLow,从当前品种当前周期、偏移 0 开始取 AllowedNumberOfBars 根。若实际复制到的数量小于请求数,说明行情刚启动或历史不全,这时用 Print 把品种、周期、错误码和描述打出来,能在 MT5 Experts 日志里直接定位是哪根数组断的。外汇与贵金属杠杆高,历史不全时信号失效概率明显偏大,别拿着半截数据就下单。
if(NumberOfBars<=class="num">1) AllowedNumberOfBars=class="num">2; class=class="str">"cmt">// 至少需要两个柱 if(NumberOfBars>=class="num">5) AllowedNumberOfBars=class="num">5; class=class="str">"cmt">// 但是不要超过5 else AllowedNumberOfBars=NumberOfBars+class="num">1; class=class="str">"cmt">// 而且只能多一个 class=class="str">"cmt">//--- 倒转索引顺序 (... class="num">3 class="num">2 class="num">1 class="num">0) ArraySetAsSeries(close_price,true); ArraySetAsSeries(open_price,true); ArraySetAsSeries(high_price,true); ArraySetAsSeries(low_price,true); class=class="str">"cmt">//--- 读取柱的收盘价 class=class="str">"cmt">// 如果获得数值的数量少于请求数,打印相关信息 if(CopyClose(_Symbol,Period(),class="num">0,AllowedNumberOfBars,close_price)<AllowedNumberOfBars) { Print("复制数据到收盘价数组失败 (" +_Symbol+", "+TimeframeToString(Period())+")!" "错误 "+IntegerToString(GetLastError())+": "+ErrorDescription(GetLastError())); } class=class="str">"cmt">//--- 读取柱的开盘价 class=class="str">"cmt">// 如果获取数值的数量少于所请求的,打印相关信息 if(CopyOpen(_Symbol,Period(),class="num">0,AllowedNumberOfBars,open_price)<AllowedNumberOfBars) { Print("复制数值到开盘价数组失败 (" +_Symbol+", "+TimeframeToString(Period())+") !" "错误 "+IntegerToString(GetLastError())+": "+ErrorDescription(GetLastError())); } class=class="str">"cmt">//--- 取得柱的最高价 class=class="str">"cmt">// 如果获取数值的数量少于所请求的,打印相关信息 if(CopyHigh(_Symbol,Period(),class="num">0,AllowedNumberOfBars,high_price)<AllowedNumberOfBars) { Print("复制数值到最高价数组失败 (" +_Symbol+", "+TimeframeToString(Period())+") to the High price array! " "错误 "+IntegerToString(GetLastError())+": "+ErrorDescription(GetLastError())); } class=class="str">"cmt">//--- 取得柱的最低价 class=class="str">"cmt">// 如果获取数值的数量少于所请求的,打印相关信息 if(CopyLow(_Symbol,Period(),class="num">0,AllowedNumberOfBars,low_price)<AllowedNumberOfBars) { Print("复制数值到数组失败 (" +_Symbol+", "+TimeframeToString(Period())+")" "错误 "+IntegerToString(GetLastError())+": "+ErrorDescription(GetLastError())); } }
连阳连阴的订单信号判定
把连续收阳或收阴的 K 线数量映射成买卖信号,核心就看 AllowedNumberOfBars 这个阈值与逐根收盘价对开盘价的比较。 下面这段函数把 2 到 5 根、以及 6 根及以上的连续同向 K 线分别写了分支:AllowedNumberOfBars==2 时只需最近 1 根收阳即返 ORDER_TYPE_BUY,==3 时要 [1][2] 两根都收阳,依此类推;>=6 时固定校验前 5 根(索引 1~5)全收阳才给买入。卖出侧完全对称,只是把大于号换成小于号。 实盘里这种写法意味着:若你设 AllowedNumberOfBars=4,则必须连续 4 根阳线(不含当前未收完的 0 号 bar)才触发买,连续 4 根阴线才触发卖,过滤掉短促毛刺的概率会明显上升,但信号延迟也随根数线性增加。外汇与贵金属杠杆高,这类纯价格行为信号在震荡市可能连续假突破,建议先在 MT5 策略测试器用 2020–2024 年 XAUUSD 的 H1 跑一遍再上真仓。 把代码直接贴进 EA 的 signal 模块就能用,想收紧过滤就把 >=6 分支里的比较行从 5 根加到 8 根,回测胜率倾向会再变。
ENUM_ORDER_TYPE GetTradingSignal() { class=class="str">"cmt">//--- 买入信号 (ORDER_TYPE_BUY) : if(AllowedNumberOfBars==class="num">2 && close_price[class="num">1]>open_price[class="num">1]) class="kw">return(ORDER_TYPE_BUY); if(AllowedNumberOfBars==class="num">3 && close_price[class="num">1]>open_price[class="num">1] && close_price[class="num">2]>open_price[class="num">2]) class="kw">return(ORDER_TYPE_BUY); if(AllowedNumberOfBars==class="num">4 && close_price[class="num">1]>open_price[class="num">1] && close_price[class="num">2]>open_price[class="num">2] && close_price[class="num">3]>open_price[class="num">3]) class="kw">return(ORDER_TYPE_BUY); if(AllowedNumberOfBars==class="num">5 && close_price[class="num">1]>open_price[class="num">1] && close_price[class="num">2]>open_price[class="num">2] && close_price[class="num">3]>open_price[class="num">3] && close_price[class="num">4]>open_price[class="num">4]) class="kw">return(ORDER_TYPE_BUY); if(AllowedNumberOfBars>=class="num">6 && close_price[class="num">1]>open_price[class="num">1] && close_price[class="num">2]>open_price[class="num">2] && close_price[class="num">3]>open_price[class="num">3] && close_price[class="num">4]>open_price[class="num">4] && close_price[class="num">5]>open_price[class="num">5]) class="kw">return(ORDER_TYPE_BUY); class=class="str">"cmt">//--- 卖出信号 (ORDER_TYPE_SELL) : if(AllowedNumberOfBars==class="num">2 && close_price[class="num">1]<open_price[class="num">1]) class="kw">return(ORDER_TYPE_SELL); if(AllowedNumberOfBars==class="num">3 && close_price[class="num">1]<open_price[class="num">1] && close_price[class="num">2]<open_price[class="num">2]) class="kw">return(ORDER_TYPE_SELL); if(AllowedNumberOfBars==class="num">4 && close_price[class="num">1]<open_price[class="num">1] && close_price[class="num">2]<open_price[class="num">2] && close_price[class="num">3]<open_price[class="num">3]) class="kw">return(ORDER_TYPE_SELL); if(AllowedNumberOfBars==class="num">5 && close_price[class="num">1]<open_price[class="num">1] && close_price[class="num">2]<open_price[class="num">2] && close_price[class="num">3]<open_price[class="num">3] && close_price[class="num">4]<open_price[class="num">4]) class="kw">return(ORDER_TYPE_SELL); if(AllowedNumberOfBars>=class="num">6 && close_price[class="num">1]<open_price[class="num">1] && close_price[class="num">2]<open_price[class="num">2] && close_price[class="num">3]<open_price[class="num">3] && close_price[class="num">4]<open_price[class="num">4] &&