交易参数防错基础:在 MQL5 中规范止损止盈与挂单设置(基础篇)
🛡️

交易参数防错基础:在 MQL5 中规范止损止盈与挂单设置(基础篇)

(1/3)· 日志里反复出现的 [invalid stops] 多半源于参数未规范化,本文从枚举到函数打底

实战向进阶 第 1/3 篇
很多人在 MQL5 里改止损、挂单时直接喂原始点数,结果终端甩出 [invalid stops] 就懵了。这类报错不是偶发 bug,而是参数没按品种规则做规范化校验。开篇先把这套防错地基铺好,后面两篇才接得住。

◍ 改参数前先把脏数据挡在门外

在 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 把这段塞进你的输入处理函数里跑一遍最踏实。

MQL5 / C++
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 倍,优化参数时这点很致命。

MQL5 / C++
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 冲突。外汇与贵金属杠杆高,参数误设可能瞬间放大回撤。

MQL5 / C++
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 日志里直接定位是哪根数组断的。外汇与贵金属杠杆高,历史不全时信号失效概率明显偏大,别拿着半截数据就下单。

MQL5 / C++
  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 根,回测胜率倾向会再变。

MQL5 / C++
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] &&
把参数校验交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到当前止损止盈是否越界,你只管决策要不要改。

常见问题

多数是止损获利点数未按交易品种的最小价位变动、止损水平等属性做规范化,直接用了原始计算值导致校验失败。
一般不建议超过 5,同方向柱数过多在实盘里很少见且价格移动逻辑会变复杂,回测也容易失真。
会,反向仓位下买入与卖出的极值基准互换,跟踪止损依旧基于柱高低加减点数,但方向判定要跟着反转。
可以,小布在品种页内置了参数越界诊断,修改前就能看到当前 SL/TP 是否符合品种规则,减少无效发单。
独立读取每个仓位属性更灵活,方便后续按需扩展用户自定义属性,也降低单次调用耦合,利于后面优化。