MQL5 Cookbook: 怎样在设置/修改交易参数时避免错误·进阶篇
持仓属性读取的开关逻辑
信号判定里若第 5 根 K 线收低于开盘(close_price[5]<open_price[5]),函数直接回吐 ORDER_TYPE_SELL;其余情况返回 WRONG_VALUE,代表当前无明确方向信号。这套返回机制让上层 EA 只在没有持仓且出信号时才动手,避免重复发单。 读取持仓前必须先调 PositionSelect(_Symbol),它返回布尔值决定后续能不能取属性。若返回 true,说明当前图表品种确实有仓,这时候再用 switch 按枚举分流抓取具体字段。 下面这段把常见持仓属性一次性映射到了全局变量:品种名与注释走 PositionGetString,魔术码走 PositionGetInteger,而掉期、佣金、开仓价、当前价、浮动盈亏、手数、SL、TP 全部走 PositionGetDouble。实盘里黄金或外汇品种隔夜 swap 可能为负,读取后最好打印核对再参与风控计算。
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 编辑器,改一下枚举就能验证自己账户的持仓字段读取是否正常。
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 返回值是否符合预期。
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,边界计算差异立刻显形。
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)看返回价是否比手动算的多一位小数,就能确认修正函数是否生效。
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 打印出来核对最小距离。
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">// 否则设为最接近的可能价位