MQL5 Cookbook: 怎样在设置/修改交易参数时避免错误·进阶篇
📘

MQL5 Cookbook: 怎样在设置/修改交易参数时避免错误·进阶篇

第 2/3 篇

持仓属性读取的开关逻辑

信号判定里若第 5 根 K 线收低于开盘(close_price[5]<open_price[5]),函数直接回吐 ORDER_TYPE_SELL;其余情况返回 WRONG_VALUE,代表当前无明确方向信号。这套返回机制让上层 EA 只在没有持仓且出信号时才动手,避免重复发单。 读取持仓前必须先调 PositionSelect(_Symbol),它返回布尔值决定后续能不能取属性。若返回 true,说明当前图表品种确实有仓,这时候再用 switch 按枚举分流抓取具体字段。 下面这段把常见持仓属性一次性映射到了全局变量:品种名与注释走 PositionGetString,魔术码走 PositionGetInteger,而掉期、佣金、开仓价、当前价、浮动盈亏、手数、SL、TP 全部走 PositionGetDouble。实盘里黄金或外汇品种隔夜 swap 可能为负,读取后最好打印核对再参与风控计算。

MQL5 / C++
   close_price[class="num">5]<open_price[class="num">5])
      class="kw">return(ORDER_TYPE_SELL);
class=class="str">"cmt">//--- 没有信号 (WRONG_VALUE):
   class="kw">return(WRONG_VALUE);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 取得仓位属性                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void GetPositionProperties(ENUM_POSITION_PROPERTIES position_property)
  {
class=class="str">"cmt">//--- 检查是否有持仓
   pos_open=PositionSelect(_Symbol);
class=class="str">"cmt">//--- 如果已经有持仓,取得其属性
   if(pos_open)
     {
      class="kw">switch(position_property)
        {
         case P_SYMBOL         : pos_symbol=PositionGetString(POSITION_SYMBOL);                    class="kw">break;
         case P_MAGIC          : pos_magic=PositionGetInteger(POSITION_MAGIC);                    class="kw">break;
         case P_COMMENT        : pos_comment=PositionGetString(POSITION_COMMENT);                 class="kw">break;
         case P_SWAP           : pos_swap=PositionGetDouble(POSITION_SWAP);                       class="kw">break;
         case P_COMMISSION     : pos_commission=PositionGetDouble(POSITION_COMMISSION);           class="kw">break;
         case P_PRICE_OPEN     : pos_price=PositionGetDouble(POSITION_PRICE_OPEN);                class="kw">break;
         case P_PRICE_CURRENT  : pos_cprice=PositionGetDouble(POSITION_PRICE_CURRENT);            class="kw">break;
         case P_PROFIT         : pos_profit=PositionGetDouble(POSITION_PROFIT);                   class="kw">break;
         case P_VOLUME         : pos_volume=PositionGetDouble(POSITION_VOLUME);                   class="kw">break;
         case P_SL             : pos_sl=PositionGetDouble(POSITION_SL);                           class="kw">break;
         case P_TP             : pos_tp=PositionGetDouble(POSITION_TP);                           class="kw">break;

「用 switch 把持仓与品种属性抓进变量」

在 MT5 的 EA 或指标里,想随时读取当前持仓细节,常见做法是写一个 switch 分支,按枚举把 PositionGetInteger / PositionGetDouble / PositionGetString 的返回值塞进对应的全局变量。比如 P_TIME 取开仓时间、P_ID 取持仓唯一标识、P_TYPE 取多空枚举;遇到 P_ALL 就把 symbol、magic、comment、swap、commission、开仓价、当前价、利润、手数、SL、TP、时间、ID、类型一次性全读出来。 若当前没有开启的仓位,代码走 else 分支调用 ZeroPositionProperties() 把所有持仓属性变量清零,避免上一单的残留数据干扰下一轮判断。外汇与贵金属杠杆高,残留的旧 SL/TP 数值若未被清掉,可能让风控逻辑误判。 品种属性侧另有 GetSymbolProperties() 函数,以 symbol_property 枚举做分支;S_DIGITS 分支用 SymbolInfoInteger(_Symbol, SYMBOL_DIGITS) 拿报价小数位,并定义了 lot_offset=1 表示距离止损水平的偏移点数为 1 点。复制下面代码到 MT5 编辑器,改一下枚举就能验证自己账户的持仓字段读取是否正常。

MQL5 / C++
   case P_TIME          : pos_time=(class="type">class="kw">datetime)PositionGetInteger(POSITION_TIME);            class="kw">break;
   case P_ID            : pos_id=PositionGetInteger(POSITION_IDENTIFIER);                    class="kw">break;
   case P_TYPE          : pos_type=(class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); class="kw">break;
   case P_ALL           :
      pos_symbol=PositionGetString(POSITION_SYMBOL);
      pos_magic=PositionGetInteger(POSITION_MAGIC);
      pos_comment=PositionGetString(POSITION_COMMENT);
      pos_swap=PositionGetDouble(POSITION_SWAP);
      pos_commission=PositionGetDouble(POSITION_COMMISSION);
      pos_price=PositionGetDouble(POSITION_PRICE_OPEN);
      pos_cprice=PositionGetDouble(POSITION_PRICE_CURRENT);
      pos_profit=PositionGetDouble(POSITION_PROFIT);
      pos_volume=PositionGetDouble(POSITION_VOLUME);
      pos_sl=PositionGetDouble(POSITION_SL);
      pos_tp=PositionGetDouble(POSITION_TP);
      pos_time=(class="type">class="kw">datetime)PositionGetInteger(POSITION_TIME);
      pos_id=PositionGetInteger(POSITION_IDENTIFIER);
      pos_type=(class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);                      class="kw">break;
   class="kw">default: Print("The passed position class="kw">property is not listed in the enumeration!");       class="kw">return;
   }
   }
class=class="str">"cmt">//--- 如果没有开启的仓位,把仓位属性变量清零
   else
      ZeroPositionProperties();
   }
class="type">void GetSymbolProperties(ENUM_SYMBOL_PROPERTIES symbol_property)
  {
   class="type">int lot_offset=class="num">1; class=class="str">"cmt">// 距离止损水平的偏移点数
class=class="str">"cmt">//---
   class="kw">switch(symbol_property)
   {
   case S_DIGITS       : sym_digits=(class="type">int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);           class="kw">break;

◍ 用 switch 把品种属性一次性抓出来

在 EA 或指标里频繁调用 SymbolInfo 系列函数会拖慢执行,常见做法是把需要的品种属性按枚举分支一次性读入局部变量。下面这段 switch 结构就覆盖了点差、止损位、报价精度、买卖价、成交量限制以及偏移修正等字段。 注意 S_ASK 和 S_BID 分支里都先取 SYMBOL_DIGITS 再做 NormalizeDouble,这是为了保证浮点报价对齐到品种小数位(比如黄金 XAUUSD 通常是 2 位、外汇 EURUSD 是 5 位)。如果不归一化,后续用价格算止损距离可能出现 1e-8 级别的误差。 S_FILTER 分支里调用了 CorrectValueBySymbolDigits(lot_offset*sym_point),把以 point 为单位的偏移量按品种精度收口,避免下单价偏离最小变动价位整数倍而被券商拒绝。外汇与贵金属杠杆高、点差跳变频繁,这类预处理能降低无效订单概率。 开 MT5 把下面代码贴进自定义函数,改改枚举名就能直接复用;重点验证你交易品种的 SYMBOL_DIGITS 和 SYMBOL_TRADE_STOPS_LEVEL 返回值是否符合预期。

MQL5 / C++
case S_SPREAD         : sym_spread=(class="type">int)SymbolInfoInteger(_Symbol,SYMBOL_SPREAD);                class="kw">break;
case S_STOPSLEVEL     : sym_stops_level=(class="type">int)SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);  class="kw">break;
case S_POINT          : sym_point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);                          class="kw">break;
class=class="str">"cmt">//---
case S_ASK           :
   sym_digits=(class="type">int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
   sym_ask=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),sym_digits);                       class="kw">break;
case S_BID           :
   sym_digits=(class="type">int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
   sym_bid=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),sym_digits);                       class="kw">break;
   class=class="str">"cmt">//---
case S_VOLUME_MIN    : sym_volume_min=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);                 class="kw">break;
case S_VOLUME_MAX    : sym_volume_max=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX);                 class="kw">break;
case S_VOLUME_LIMIT  : sym_volume_limit=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_LIMIT);             class="kw">break;
case S_VOLUME_STEP   : sym_volume_step=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);               class="kw">break;
class=class="str">"cmt">//---
case S_FILTER        :
   sym_digits=(class="type">int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
   sym_point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
   sym_offset=NormalizeDouble(CorrectValueBySymbolDigits(lot_offset*sym_point),sym_digits);        class="kw">break;
   class=class="str">"cmt">//---
case S_UP_LEVEL      :
   sym_digits=(class="type">int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
   sym_stops_level=(class="type">int)SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);

按品种精度拉止损边界与成交量档位

这段 switch 分支干的事很直接:根据调用方传入的枚举,把当前品种的报价精度、止损级差、点值、买卖价和交易量限制一次性抓出来。外汇和贵金属的高风险在于,经纪商随时可能改 SYMBOL_TRADE_STOPS_LEVEL,挂单距离若低于该值会被拒,所以运行时实时读比写死常数稳。 S_UP_LEVEL 分支只算上方边界:sym_up_level = NormalizeDouble(sym_ask + sym_stops_level * sym_point, sym_digits),也就是卖价加经纪商要求的最小止损点数。S_DOWN_LEVEL 反之用买价减,得到下方边界。 S_ALL 把能用的都捞了:除上下边界外,还读 SYMBOL_SPREAD、SYMBOL_VOLUME_MIN/MAX/LIMIT/STEP。其中 sym_offset 用 CorrectValueBySymbolDigits(lot_offset*sym_point) 再 NormalizeDouble,等于把以点为单位的偏移量按品种精度收口。 default 分支只 Print 一句未列出属性的提示并 return,不抛错但会静默中断后续逻辑,调试时别漏看日志。开 MT5 把这段塞进 EA 的刷新函数,切到 XAUUSD 看 sym_stops_level 常是 0~10 点,EURUSD 多数在 0,边界计算差异立刻显形。

MQL5 / C++
 sym_point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
 sym_ask=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),sym_digits);
 sym_up_level=NormalizeDouble(sym_ask+sym_stops_level*sym_point,sym_digits); class="kw">break;
 class=class="str">"cmt">//---
 case S_DOWN_LEVEL :
 sym_digits=(class="type">int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
 sym_stops_level=(class="type">int)SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);
 sym_point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
 sym_bid=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),sym_digits);
 sym_down_level=NormalizeDouble(sym_bid-sym_stops_level*sym_point,sym_digits); class="kw">break;
 class=class="str">"cmt">//---
 case S_ALL :
 sym_digits=(class="type">int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
 sym_spread=(class="type">int)SymbolInfoInteger(_Symbol,SYMBOL_SPREAD);
 sym_stops_level=(class="type">int)SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);
 sym_point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
 sym_ask=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),sym_digits);
 sym_bid=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),sym_digits);
 sym_volume_min=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
 sym_volume_max=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX);
 sym_volume_limit=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_LIMIT);
 sym_volume_step=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);
 sym_offset=NormalizeDouble(CorrectValueBySymbolDigits(lot_offset*sym_point),sym_digits);
 sym_up_level=NormalizeDouble(sym_ask+sym_stops_level*sym_point,sym_digits);
 sym_down_level=NormalizeDouble(sym_bid-sym_stops_level*sym_point,sym_digits); class="kw">break;
 class=class="str">"cmt">//---
 class="kw">default: Print("The passed symbol class="kw">property is not listed in the enumeration!"); class="kw">return;
 }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 根据价格小数点位数调整数值 (整数形) |
class=class="str">"cmt">//+------------------------------------------------------------------+

「手数与止盈的精度对齐逻辑」

MT5 里不同品种的小数位会直接搅乱你的手数和价格计算。当 sym_digits 为 3 或 5(即日元类或多数五位数报价品种)时,CorrectValueBySymbolDigits 会把传入值乘 10,否则原样返回——这一步是为了把「点数」换算到正确量级,避免 EURUSD 上把 50 点写成 0.0050 而不是 0.00500 的错位。 CalculateLot 先拉取合约的最小手数、最大手数与步长,再用 MathRound(lot/sym_volume_step)*sym_volume_step 把目标手数吸附到合规步长上。若结果跌破 sym_volume_min 就返回最小值,超过 sym_volume_max 返回最大值,最后 NormalizeDouble(...,2) 截断到两位小数——外汇和贵金属杠杆高,手数越界会被经纪商直接拒单,这套钳制能省掉不少无效报单。 CalculateTakeProfit 按买卖方向分别处理:卖单用 sym_bid 减去修正后的点数距离,买单用 sym_ask 加上去,都经 NormalizeDouble 对齐报价精度。它还比对 sym_down_level / sym_up_level 与 sym_offset,确保止盈不卡在止损禁挂区——实际验证时把 TakeProfit 设成 30,切到 XAUUSD(sym_digits=5)看返回价是否比手动算的多一位小数,就能确认修正函数是否生效。

MQL5 / C++
class="type">int CorrectValueBySymbolDigits(class="type">int value)
  {
   class="kw">return (sym_digits==class="num">3 || sym_digits==class="num">5) ? value*=class="num">10 : value;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 根据价格小数点位数调整数值 (双精度型)                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CorrectValueBySymbolDigits(class="type">class="kw">double value)
  {
   class="kw">return (sym_digits==class="num">3 || sym_digits==class="num">5) ? value*=class="num">10 : value;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 计算仓位手数                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CalculateLot(class="type">class="kw">double lot)
  {
class=class="str">"cmt">//--- 根据步长调整
   class="type">class="kw">double corrected_lot=class="num">0.0;
class=class="str">"cmt">//---
   GetSymbolProperties(S_VOLUME_MIN);   class=class="str">"cmt">// 取得最小可能手数
   GetSymbolProperties(S_VOLUME_MAX);   class=class="str">"cmt">// 取得最大可能手数
   GetSymbolProperties(S_VOLUME_STEP); class=class="str">"cmt">// 取得手数增加/减少步长
class=class="str">"cmt">//--- 根据步长调整
   corrected_lot=MathRound(lot/sym_volume_step)*sym_volume_step;
class=class="str">"cmt">//--- 如果比最小值小,返回最小值
   if(corrected_lot<sym_volume_min)
      class="kw">return(NormalizeDouble(sym_volume_min,class="num">2));
class=class="str">"cmt">//--- 如果比最大值大,返回最大值
   if(corrected_lot>sym_volume_max)
      class="kw">return(NormalizeDouble(sym_volume_max,class="num">2));
class=class="str">"cmt">//---
   class="kw">return(NormalizeDouble(corrected_lot,class="num">2));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 计算获利值                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CalculateTakeProfit(ENUM_ORDER_TYPE order_type)
  {
class=class="str">"cmt">//--- 如果需要获利值
   if(TakeProfit>class="num">0)
     {
      class=class="str">"cmt">//--- 计算获利值
      class="type">class="kw">double tp=class="num">0.0;
      class=class="str">"cmt">//--- 如果您需要计算卖出仓位的值
      if(order_type==ORDER_TYPE_SELL)
        {
         class=class="str">"cmt">//--- 计算数据
         tp=NormalizeDouble(sym_bid-CorrectValueBySymbolDigits(TakeProfit*sym_point),sym_digits);
         class=class="str">"cmt">//--- 如果计算的数值比止损价位下限低则返回计算的数值
         class=class="str">"cmt">//     如果数值高或者相等,返回调整后的值
         class="kw">return(tp<sym_down_level ? tp : sym_down_level-sym_offset);
        }
      class=class="str">"cmt">//--- 如果您需要计算买入仓位的值
      if(order_type==ORDER_TYPE_BUY)
        {
         class=class="str">"cmt">//--- 计算数据
         tp=NormalizeDouble(sym_ask+CorrectValueBySymbolDigits(TakeProfit*sym_point),sym_digits);
         class=class="str">"cmt">//--- 如果计算的数值比止损价位上限高则返回计算的数值
         class=class="str">"cmt">//     如果数值低或者相等,返回调整后的值
         class="kw">return(tp>sym_up_level ? tp : sym_up_level+sym_offset);

◍ 止损与追踪止损的边界处理

EA 里止损不是填个点数就完事,经纪商对止损位置有硬下限。CalculateStopLoss 先判断 StopLoss 是否大于 0,再按买卖方向算初始位:买仓用 ask 减点数,卖仓用 bid 加点位,最后用 NormalizeDouble 对齐品种小数位。 若算出的 sl 低于买仓下限 sym_down_level,直接返回该值;否则退到 sym_down_level - sym_offset。卖仓逻辑镜像,sl 高于 sym_up_level 才返回,否则取 sym_up_level + sym_offset。这套回退能避免下单被拒。 追踪止损 CalculateTrailingStop 取前一根 K 线低点(买仓)或高点(卖仓)减/加点数作基准。买仓基准低于 sym_down_level 就直接采用,否则改用实时 bid 重算并再次比对下限。 外汇与贵金属杠杆高,止损被经纪商规则弹回会导致裸奔,建议在 MT5 策略测试器里把 sym_down_level 打印出来核对最小距离。

MQL5 / C++
class="type">class="kw">double CalculateStopLoss(ENUM_ORDER_TYPE order_type)
  {
class=class="str">"cmt">//--- 如果需要止损值
   if(StopLoss>class="num">0)
     {
      class=class="str">"cmt">//--- 计算止损值
      class="type">class="kw">double sl=class="num">0.0;
      class=class="str">"cmt">//--- 如果您需要计算买入仓位的值
      if(order_type==ORDER_TYPE_BUY)
        {
         class=class="str">"cmt">// 计算数据
         sl=NormalizeDouble(sym_ask-CorrectValueBySymbolDigits(StopLoss*sym_point),sym_digits);
         class=class="str">"cmt">//--- 如果计算的值比止损水平下限低则返回计算值
         class=class="str">"cmt">//    如果数值高或者相等,返回调整后的值
         class="kw">return(sl<sym_down_level ? sl : sym_down_level-sym_offset);
        }
      class=class="str">"cmt">//--- 如果您需要计算卖出仓位的值
      if(order_type==ORDER_TYPE_SELL)
        {
         class=class="str">"cmt">//--- 计算数据
         sl=NormalizeDouble(sym_bid+CorrectValueBySymbolDigits(StopLoss*sym_point),sym_digits);
         class=class="str">"cmt">//--- 如果计算值比止损水平上限高则返回计算值
         class=class="str">"cmt">//    如果数值低或者相等,返回调整后的值
         class="kw">return(sl>sym_up_level ? sl : sym_up_level+sym_offset);
        }
     }
class=class="str">"cmt">//---
   class="kw">return(class="num">0.0);
  }
class="type">class="kw">double CalculateTrailingStop(class="type">ENUM_POSITION_TYPE position_type)
  {
class=class="str">"cmt">//--- 计算所需变量
   class="type">class="kw">double        level      =class="num">0.0;
   class="type">class="kw">double        buy_point  =low_price[class="num">1];    class=class="str">"cmt">// 用于买入的低价
   class="type">class="kw">double        sell_point =high_price[class="num">1];   class=class="str">"cmt">// 用于卖出的高价
class=class="str">"cmt">//--- 计算买入仓位的水平
   if(position_type==POSITION_TYPE_BUY)
     {
      class=class="str">"cmt">//--- 柱的最低价减去指定的点数
      level=NormalizeDouble(buy_point-CorrectValueBySymbolDigits(StopLoss*sym_point),sym_digits);
      class=class="str">"cmt">//--- 如果计算值比止损价位的下限低, 
      class=class="str">"cmt">//    计算完成,返回当前止损价位
      if(level<sym_down_level)
         class="kw">return(level);
      class=class="str">"cmt">//--- 如果不低,根据卖价计算
      else
        {
         level=NormalizeDouble(sym_bid-CorrectValueBySymbolDigits(StopLoss*sym_point),sym_digits);
         class=class="str">"cmt">//--- 如果计算的价位比下限低,返回当前价位
         class=class="str">"cmt">//     否则设为最接近的可能价位

常见问题

先用开关逻辑读持仓属性,判断止损字段是否为空或零值;为空说明未设,再按品种精度计算边界后修改,避免直接写脏值。
用 switch 按品种名或魔术码分支,把点值、精度、最小停损等属性统一赋给变量,减少每单重复查询。
可以。小布能读取你的品种规范和代码逻辑,标出未按成交量档位和报价精度对齐的下单参数,给出修正建议。
多半是精度没用对:黄金报价格式特殊,需用品种精度函数取小数位再乘对应系数,否则边界会错位。
是。追踪止损也要走同一套边界判断,修改前需确认不小于品种最小停损且不与原止损逻辑互相覆盖。