MT5下单避错:规范交易参数实战
用MQL5函数校验止损获利,杜绝invalid stops
为什么设置交易参数总会报 invalid stops
在MQL5编程论坛和日常开发里,初学者提问最高频的问题之一就是:为什么我在开仓或修改止损、获利的时候,日志里会出现以 [invalid stops] 结尾的错误消息?这类错误并不是随机的,它来自交易服务器对你所提交价格合规性的拒绝。MT5中的每个交易品种都有自己独立的规则,比如最小止损距离(SYMBOL_TRADE_STOPS_LEVEL)、执行方式(市价执行或即时执行)、点值(SYMBOL_POINT)与小数位(SYMBOL_DIGITS)。如果你用固定的点数去计算止损价,而没有考虑这些品种属性,价格就可能落在禁止区域内,从而被经纪商拒绝。
尤其需要注意的是,很多商品(如黄金XAUUSD)的点(point)和 tick 并不相等,且即时执行品种不允许在开仓同时带止损,必须开仓后修改。原文评论区就有用户反馈在 InstaTrader 的 GOLD 上直接带 SL 开仓会报错,而外汇对不会,这正是品种属性差异导致的典型坑。因此,在代码里动态读取并规范化参数,是专业 EA 的必备基础。
核心思路:把品种与仓位属性抽象为枚举
为了方便地读取和管理属性,原文定义了两个枚举:ENUM_POSITION_PROPERTIES 和 ENUM_SYMBOL_PROPERTIES。前者覆盖仓位的符号、魔术码、评论、库存费、佣金、开仓价、当前价、利润、交易量、止损、获利、时间、ID、类型等;后者覆盖小数位、点差、止损级别、点值、买卖价、最小最大交易量、步长以及基于这些计算出的上下限价格。通过传入不同标识符,函数可以只取单项或一次取全部,避免重复调用系统函数,代码更清晰。
这种设计也属于一种轻量的‘领域建模’:把 MT5 复杂的属性体系收敛为几个可控的枚举,后续任何 EA 模块都能复用。在全局范围声明对应的变量,保证所有函数都能访问,是本文示例的基础架构。
enum ENUM_SYMBOL_PROPERTIES
{
S_DIGITS = 0,
S_SPREAD = 1,
S_STOPSLEVEL = 2,
S_POINT = 3,
S_ASK = 4,
S_BID = 5,
S_VOLUME_MIN = 6,
S_VOLUME_MAX = 7,
S_VOLUME_LIMIT = 8,
S_VOLUME_STEP = 9,
S_FILTER = 10,
S_UP_LEVEL = 11,
S_DOWN_LEVEL = 12,
S_ALL = 13
};
外部参数与全局变量的合理约束
示例 EA 给出了几个外部输入:NumberOfBars(同方向柱数)、Lot(手数)、TakeProfit(获利点数)、StopLoss(止损点数)、TrailingStop(跟踪止损点数)、Reverse(反向加仓开关)。其中 NumberOfBars 一般不建议超过 5,因为超过后价格运动往往已错过时机。代码里用 AllowedNumberOfBars 做了夹紧:小于1按2处理,大于等于5按5,否则加1(预留已收盘柱)。
全局声明的 sym_digits、sym_point、sym_ask 等变量,由 GetSymbolProperties 函数填充。这样在 CalculateStopLoss 等核心计算函数里,不需要每次重新查询,既提升效率也减少出错面。
input int NumberOfBars=2; input double Lot =0.1; input double StopLoss =50; input double TakeProfit =100; input double TrailingStop=10; input bool Reverse =true;
数据准备:获取K线与品种属性
GetBarsData 函数负责把开盘、收盘、最高、最低价复制到数组,并调整 AllowedNumberOfBars。它使用 CopyHigh/CopyLow 等函数,并以 ArraySetAsSeries 倒序排列,使 index[1] 代表最近一根已收盘柱。GetSymbolProperties 则通过 SymbolInfoInteger / SymbolInfoDouble 读取属性,对于依赖其他属性的项(如上下限),在函数内按序取得基础值再计算。
这里有个细节:部分属性(如 S_UP_LEVEL)需要先知道 stops_level、point、ask 才能算。如果调用顺序错,会得到 0 或错误值。原文用 switch 内部顺序保证依赖被正确满足,这是实际开发中容易忽略的点。
void GetBarsData()
{
if(NumberOfBars<=1) AllowedNumberOfBars=2;
if(NumberOfBars>=5) AllowedNumberOfBars=5;
else AllowedNumberOfBars=NumberOfBars+1;
ArraySetAsSeries(close_price,true);
ArraySetAsSeries(open_price,true);
ArraySetAsSeries(high_price,true);
ArraySetAsSeries(low_price,true);
CopyClose(_Symbol,Period(),0,AllowedNumberOfBars,close_price);
CopyOpen(_Symbol,Period(),0,AllowedNumberOfBars,open_price);
CopyHigh(_Symbol,Period(),0,AllowedNumberOfBars,high_price);
CopyLow(_Symbol,Period(),0,AllowedNumberOfBars,low_price);
}
数值规范化:重载函数与小数位修正
MQL5 支持函数重载,原文用 CorrectValueBySymbolDigits 提供 int 与 double 两个版本。它的作用是:当品种小数位是 3 或 5(即报价精度为 0.001 / 0.00001,常见于日元外或贵金)时,把‘点数’乘以 10,因为此时 1 点(point)实际对应 10 个 tick。这样用用户输入的‘点数’计算价格偏移时就不会偏小。
CalculateLot 则根据 VolumeMin/Max/Step 把用户手数吸附到合规步长,并夹紧在限制内。MathRound(lot/step)*step 是标准做法,最后 NormalizeDouble(...,2) 保证显示两位。很多 EA 爆仓或手数报错,根源就是没做这一步。
int CorrectValueBySymbolDigits(int value)
{ return (sym_digits==3 || sym_digits==5) ? value*=10 : value; }
double CorrectValueBySymbolDigits(double value)
{ return (sym_digits==3 || sym_digits==5) ? value*=10 : value; }
止损与获利的计算及边界保护
CalculateStopLoss 与 CalculateTakeProfit 是本文核心。它们先按订单类型用 ask/bid 加减点数得到原始价,然后与 sym_up_level / sym_down_level 比较。若计算价超越了止损边界,就返回边界外推 offset 的合规值。offset 由 CorrectValueBySymbolDigits(lot_offset*point) 得出,相当于留一个最小缓冲,避免刚好卡在边界被拒。
举例:买入止损必须在卖价下方且距离不小于 stops_level;若算出来高于下限,就取 down_level - offset。这种‘先算后钳’的模式,比直接提交再catch错误更优雅,也避免频繁失败订单占用资源。
double CalculateStopLoss(ENUM_ORDER_TYPE order_type)
{
if(StopLoss>0)
{
double sl=0.0;
if(order_type==ORDER_TYPE_BUY)
{
sl=NormalizeDouble(sym_ask-CorrectValueBySymbolDigits(StopLoss*sym_point),sym_digits);
return(sl<sym_down_level ? sl : sym_down_level-sym_offset);
}
if(order_type==ORDER_TYPE_SELL)
{
sl=NormalizeDouble(sym_bid+CorrectValueBySymbolDigits(StopLoss*sym_point),sym_digits);
return(sl>sym_up_level ? sl : sym_up_level+sym_offset);
}
}
return(0.0);
}
跟踪止损的动态修改逻辑
CalculateTrailingStop 基于最近收盘柱的高低点推算:买入用 low[1] 减点数,卖出用 high[1] 加点数;若越过边界则回退到基于 bid/ask 的计算。ModifyTrailingStop 在 OnTick 新柱时调用,先取仓位与品种属性,算新 SL,再用 switch 按仓位类型判断‘新值是否比原值更优(买仓更高、卖仓更低)且超过 TrailingStop 步长’,满足才用 CTrade.PositionModify 修改。
这里要注意:如果原仓位没有 SL(pos_sl==0),则直接设置;若有,则比较后再改。这种条件分离避免了无谓修改,也防止了服务器拒绝‘不变更’的修改请求。
void ModifyTrailingStop()
{
if(TrailingStop>0 && StopLoss>0)
{
double new_sl=0.0; bool condition=false;
pos_open=PositionSelect(_Symbol);
if(pos_open)
{
GetSymbolProperties(S_ALL); GetPositionProperties(P_ALL);
new_sl=CalculateTrailingStop(pos_type);
switch(pos_type)
{
case POSITION_TYPE_BUY: condition=new_sl>pos_sl+CorrectValueBySymbolDigits(TrailingStop*sym_point); break;
case POSITION_TYPE_SELL: condition=new_sl<pos_sl-CorrectValueBySymbolDigits(TrailingStop*sym_point); break;
}
if(pos_sl>0 && condition) trade.PositionModify(_Symbol,new_sl,pos_tp);
if(pos_sl==0) trade.PositionModify(_Symbol,new_sl,pos_tp);
}
}
}
交易执行与信息面板的模式判断
TradingBlock 用 GetTradingSignal 拿到 ORDER_TYPE_BUY/SELL,通过 switch 给开仓价、反向类型赋值,再算 SL/TP,调用 CalculateLot 后使用 trade.PositionOpen 开仓。若已有反向仓且 Reverse=true,则在原仓位量上加仓。SetInfoPanel 则通过 IsVisualMode()/IsRealtime() 判断,仅在非优化测试时绘制图形面板,否则策略测试器优化耗时会增加 4-5 倍,这是实战性能要点。
原文提供的 IsTester/IsOptimization/IsVisualMode/IsRealtime 四个辅助函数,利用 MQL5InfoInteger 标志位,清晰分离了‘实盘、可视化、优化’三种环境,值得在所有 EA 中复用。
bool IsRealtime()
{ if(!IsTester() && !IsOptimization() && !IsVisualMode()) return(true); else return(false); }
参数优化与测试结果
在策略测试器中,将 NumberOfBars、Lot、SL、TP、TrailingStop 设范围后优化。双核 Core2 Duo 约 7 分钟完成,最大采收率结果可直接观察。由于面板仅在实时/可视化显示,优化速度得以保证。这种‘先规范后交易再优化’的闭环,是从demo走向实盘系统的关键一步。
需要强调的是:本文示例代码不能直接用于实盘,它旨在教学。真实系统还需处理成交历史、部分平仓、多品种上下文及经纪商特殊规则(如 GOLD 先开仓后设损)。但参数规范化与边界校验的骨架是完全通用的。