MQL5 交易工具包(第 1 部分):开发仓位管理 EX5 库·综合运用
(3/3)·从零搭好仓位管理 EX5 库后,怎么在真实 EA 里不掉链子地复用才是终点
开仓前的交易许可与订单日志
EA 在下单前必须确认账户允许自动交易,否则直接退出函数。判断逻辑用 AccountInfoInteger(ACCOUNT_TRADE_ALLOWED) 与 ACCOUNT_TRADE_EXPERT 两个标志位做与运算,两者皆为真才返回 true,任一受限则返回 false。 PrintOrderDetails() 把 tradeRequest 与 tradeResult 的关键字段拼成多行字符串打进日志,涵盖品种、订单类型、成交量、挂单价、SL/TP、魔术码、填充方式、偏差点、RETCODE 和运行时错误码。这样在 MT5 回测或实盘里,一笔订单为什么没成,翻日志就能定位,不用猜。 OpenBuyPosition() 的入口先调 TradingIsAllowed(),若被禁立即 return false。接着用 ZeroMemory 清空 tradeRequest 与 tradeResult 结构体,避免上次残留数据污染本次请求。 初始化买仓时 deviation 设为 SYMBOL_SPREAD * 2,也就是用当前点差的两倍作为允许的成交偏差。外汇与贵金属波动剧烈、点差跳变频繁,这套检查能降低错单概率,但无法消除滑点风险,实盘仍可能以偏离预期的价格成交。
if(AccountInfoInteger(ACCOUNT_TRADE_ALLOWED) && AccountInfoInteger(ACCOUNT_TRADE_EXPERT)) { class="kw">return(true);class=class="str">"cmt">//-- trading is allowed, exit and class="kw">return true } class="kw">return(false);class=class="str">"cmt">//-- trading is not allowed, exit and class="kw">return false } class=class="str">"cmt">//+-----------------------------------------------------------------------+ class=class="str">"cmt">//| PrintOrderDetails() prints the order details for the EA log | class=class="str">"cmt">//+-----------------------------------------------------------------------+ class="type">void PrintOrderDetails(class="type">class="kw">string header) { class="type">class="kw">string orderDescription; class=class="str">"cmt">//-- Print the order details orderDescription += "_______________________________________________________________________________________\r\n"; orderDescription += "--> " + tradeRequest.symbol + " " + EnumToString(tradeRequest.type) + " " + header + " <--\r\n"; orderDescription += "Order ticket: " + (class="type">class="kw">string)tradeRequest.order + "\r\n"; orderDescription += "Volume: " + StringFormat("%G", tradeRequest.volume) + "\r\n"; orderDescription += "Price: " + StringFormat("%G", tradeRequest.price) + "\r\n"; orderDescription += "Stop Loss: " + StringFormat("%G", tradeRequest.sl) + "\r\n"; orderDescription += "Take Profit: " + StringFormat("%G", tradeRequest.tp) + "\r\n"; orderDescription += "Comment: " + tradeRequest.comment + "\r\n"; orderDescription += "Magic Number: " + StringFormat("%d", tradeRequest.magic) + "\r\n"; orderDescription += "Order filling: " + EnumToString(tradeRequest.type_filling)+ "\r\n"; orderDescription += "Deviation points: " + StringFormat("%G", tradeRequest.deviation) + "\r\n"; orderDescription += "RETCODE: " + (class="type">class="kw">string)(tradeResult.retcode) + "\r\n"; orderDescription += "Runtime Code: " + (class="type">class="kw">string)(GetLastError()) + "\r\n"; orderDescription += "---"; Print(orderDescription); } class="type">bool OpenBuyPosition(class="type">ulong magicNumber, class="type">class="kw">string symbol, class="type">class="kw">double lotSize, class="type">int sl, class="type">int tp, class="type">class="kw">string positionComment) class="kw">export { class=class="str">"cmt">//--- Function body } class=class="str">"cmt">//-- first check if the ea is allowed to trade if(!TradingIsAllowed()) { class="kw">return(false); class=class="str">"cmt">//--- algo trading is disabled, exit function } class=class="str">"cmt">//-- reset the the tradeRequest and tradeResult values by zeroing them ZeroMemory(tradeRequest); ZeroMemory(tradeResult); class=class="str">"cmt">//-- initialize the parameters to open a buy position tradeRequest.type = ORDER_TYPE_BUY; tradeRequest.action = TRADE_ACTION_DEAL; tradeRequest.magic = magicNumber; tradeRequest.symbol = symbol; tradeRequest.tp = class="num">0; tradeRequest.sl = class="num">0; tradeRequest.comment = positionComment; tradeRequest.deviation = SymbolInfoInteger(symbol, SYMBOL_SPREAD) * class="num">2; class=class="str">"cmt">//-- set and moderate the lot size or volume
「手数夹逼与百次重试的下单防呆」
开仓前先对手数做三层约束:下限用 SYMBOL_VOLUME_MIN 兜住,上限用 SYMBOL_VOLUME_MAX 封顶,再用 SYMBOL_VOLUME_STEP 向下取整,避免 broker 拒单。这三行直接决定你的 volume 字段能不能过校验。 ResetLastError() 必须放在发单循环前,否则上一次残留的错误码会污染 ErrorAdvisor 的判断。循环上限设 100 次,意味着极端流动性下最多连发百回,直到 retcode 回到 10008(已成交)或 10009(已下单待成交)才 return true。 每次迭代都重写 tradeRequest.price = SYMBOL_ASK,并依据 tp/sl 点数重算止损止盈:tp*_Point 加在卖价上、sl*_Point 减在卖价上,再用 NormalizeDouble 按 _Digits 截断。外汇与贵金属杠杆高,滑点可能让实际成交价偏离,重算能降低无效挂单概率。 OrderSend 失败分支里调 ErrorAdvisor 做诊断,若返回 false 或 IsStopped() 为真就打印错误并退出。实盘跑这段代码前,建议先在策略测试器用任意品种验证 100 次循环是否真能收敛,而不是卡死在重试里。
lotSize = MathMax(lotSize, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN)); class=class="str">"cmt">//-- Verify that volume is not less than allowed minimum lotSize = MathMin(lotSize, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX)); class=class="str">"cmt">//-- Verify that volume is not more than allowed maximum lotSize = MathFloor(lotSize / SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP)) * SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); class=class="str">"cmt">//-- Round down to nearest volume step tradeRequest.volume = lotSize; class=class="str">"cmt">//--- Reset error cache so that we get an accurate runtime error code in the ErrorAdvisor function ResetLastError(); for(class="type">int loop = class="num">0; loop <= class="num">100; loop++) class=class="str">"cmt">//-- try opening the order untill it is successful(class="num">100 max tries) { class=class="str">"cmt">//--- Place the order request code to open a new buy position } class=class="str">"cmt">//--- update order opening price on each iteration tradeRequest.price = SymbolInfoDouble(symbol, SYMBOL_ASK); class=class="str">"cmt">//-- set the take profit and stop loss on each iteration if(tp > class="num">0) { tradeRequest.tp = NormalizeDouble(tradeRequest.price + (tp * _Point), _Digits); } if(sl > class="num">0) { tradeRequest.sl = NormalizeDouble(tradeRequest.price - (sl * _Point), _Digits); } class=class="str">"cmt">//--- send order to the trade server if(OrderSend(tradeRequest, tradeResult)) { class=class="str">"cmt">//-- Print the order details PrintOrderDetails("Sent OK"); class=class="str">"cmt">//-- Confirm order execution if(tradeResult.retcode == class="num">10008 || tradeResult.retcode == class="num">10009) { Print(__FUNCTION__, ": CONFIRMED: Successfully openend a ", symbol, " BUY POSITION #", tradeResult.order, ", Price: ", tradeResult.price); PrintFormat("retcode=%u deal=%I64u order=%I64u", tradeResult.retcode, tradeResult.deal, tradeResult.order); Print("_______________________________________________________________________________________"); class="kw">return(true); class=class="str">"cmt">//-- exit the function break; class=class="str">"cmt">//--- success - order placed ok. exit the for loop } } else class=class="str">"cmt">//-- Order request failed { class=class="str">"cmt">//-- Print the order details PrintOrderDetails("Sending Failed"); class=class="str">"cmt">//-- order not sent or critical error found if(!ErrorAdvisor(__FUNCTION__, symbol, tradeResult.retcode) || IsStopped()) { Print(__FUNCTION__, ": ", symbol, " ERROR opening a BUY POSITION at: ", tradeRequest.price, ", Lot\\Vol: ", tradeRequest.volume);
◍ 买单调仓的容错与手数校正
开多单函数先把交易开关拦一道:TradingIsAllowed() 返回 false 就直接退出,避免在 EA 自动交易被终端禁用时还去撞服务器。 手数这一步最容易出事。代码里用 MathMax 把手数顶到 SYMBOL_VOLUME_MIN 之上,MathMin 压到 SYMBOL_VOLUME_MAX 之下,再用 MathFloor(lotSize / SYMBOL_VOLUME_STEP) * SYMBOL_VOLUME_STEP 向下取整到合法步长。不这么处理,OrderSend 会直接报 4756(手数不合规范)。 deviation 设成 SYMBOL_SPREAD * 2,也就是允许两倍点差滑点;对黄金这类点差跳得快的符号,这个余量能显著降低 4751(价格过期)的报错概率。 下单放在 for(loop=0; loop<=100; loop++) 里反复试,每次都重读 SYMBOL_ASK 刷新 price,并按传入的 tp/sl 点数重算挂单价。最多 100 次重试,能消化瞬时流动性缺口,但不是无限死循环。外汇和贵金属波动剧烈,这种重试机制只是降低失败率,不保证成交。
class="type">bool OpenBuyPosition(class="type">ulong magicNumber, class="type">class="kw">string symbol, class="type">class="kw">double lotSize, class="type">int sl, class="type">int tp, class="type">class="kw">string positionComment) class="kw">export { if(!TradingIsAllowed()) { class="kw">return(false); } ZeroMemory(tradeRequest); ZeroMemory(tradeResult); tradeRequest.type = ORDER_TYPE_BUY; tradeRequest.action = TRADE_ACTION_DEAL; tradeRequest.magic = magicNumber; tradeRequest.symbol = symbol; tradeRequest.tp = class="num">0; tradeRequest.sl = class="num">0; tradeRequest.comment = positionComment; tradeRequest.deviation = SymbolInfoInteger(symbol, SYMBOL_SPREAD) * class="num">2; lotSize = MathMax(lotSize, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN)); lotSize = MathMin(lotSize, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX)); lotSize = MathFloor(lotSize / SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP)) * SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); tradeRequest.volume = lotSize; ResetLastError(); for(class="type">int loop = class="num">0; loop <= class="num">100; loop++) { tradeRequest.price = SymbolInfoDouble(symbol, SYMBOL_ASK); if(tp > class="num">0) { tradeRequest.tp = NormalizeDouble(tradeRequest.price + (tp * _Point), _Digits); } if(sl > class="num">0) { tradeRequest.sl = NormalizeDouble(tradeRequest.price - (sl * _Point), _Digits); } if(OrderSend(tradeRequest, tradeResult)) { PrintOrderDetails("Sent OK");
开仓回执的确认与卖出函数骨架
下单之后必须靠回执码判断真实成交状态,而不是假设 SendOrder 返回即成功。MQL5 里 retcode 10008 代表订单已在交易服务器确认,10009 代表成交已执行,这两值任一命中才可视为 BUY 仓位真正落地。 下面这段逻辑在确认分支里打印订单号、成交价与 deal 标识,随后 return(true) 退出函数;注意它后面还写了一句 break,实际因 return 已离开作用域,break 永不会执行,属于冗余死代码,复制时需清掉。 [CODE] //-- Confirm order execution
| if(tradeResult.retcode == 10008 | tradeResult.retcode == 10009) |
|---|
{ Print(__FUNCTION__, ": CONFIRMED: Successfully openend a ", symbol, " BUY POSITION #", tradeResult.order, ", Price: ", tradeResult.price); PrintFormat("retcode=%u deal=%I64u order=%I64u", tradeResult.retcode, tradeResult.deal, tradeResult.order); Print("_______________________________________________________________________________________”); return(true); //-- exit the function break; //--- success - order placed ok. exit the for loop } } else //-- Order request failed { //-- Print the order details PrintOrderDetails("Sending Failed"); //-- order not sent or critical error found
| if(!ErrorAdvisor(__FUNCTION__, symbol, tradeResult.retcode) | IsStopped()) |
|---|
{ Print(__FUNCTION__, ": ", symbol, " ERROR opening a BUY POSITION at: ", tradeRequest.price, ", Lot\\Vol: ", tradeRequest.volume); Print("_______________________________________________________________________________________”); return(false); //-- exit the function break; //-- exit the for loop } } } return(false); } //-------------------------------------------------------------------+ // OpenSellPosition(): Function to open a new sell entry order. | //+------------------------------------------------------------------+ bool OpenSellPosition(ulong magicNumber, string symbol, double lotSize, int sl, int tp, string positionComment) export { //-- first check if the ea is allowed to trade if(!TradingIsAllowed()) { return(false); //--- algo trading is disabled, exit function } //-- reset the the tradeRequest and tradeResult values by zeroing them ZeroMemory(tradeRequest); ZeroMemory(tradeResult); //-- initialize the parameters to open a sell position tradeRequest.type = ORDER_TYPE_SELL; tradeRequest.action = TRADE_ACTION_DEAL; tradeRequest.magic = magicNumber; tradeRequest.symbol = symbol; tradeRequest.tp = 0; tradeRequest.sl = 0; tradeRequest.comment = positionComment; tradeRequest.deviation = SymbolInfoInteger(symbol, SYMBOL_SPREAD) * 2; //-- set and moderate the lot size or volume lotSize = MathMax(lotSize, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN)); //-- Verify that volume is not less than allowed minimum [/CODE] 卖出函数 OpenSellPosition 开头先调 TradingIsAllowed 拦截自动交易开关,禁用即 return(false)。随后 ZeroMemory 清空 tradeRequest 与 tradeResult 结构体,避免上一笔残留字段污染新请求。 tradeRequest.deviation 被设为品种点差整数值乘 2,意味着允许滑点约为两倍 spread;lotSize 用 MathMax 兜底到 SYMBOL_VOLUME_MIN,防止手数低于券商最小限制而被拒。外汇与贵金属杠杆高,实测滑点放大时回执码可能偏离 10008/10009,需要在 MT5 策略测试器用真实点差重跑验证。
class=class="str">"cmt">//-- Confirm order execution if(tradeResult.retcode == class="num">10008 || tradeResult.retcode == class="num">10009) { Print(__FUNCTION__, ": CONFIRMED: Successfully openend a ", symbol, " BUY POSITION #", tradeResult.order, ", Price: ", tradeResult.price); PrintFormat("retcode=%u deal=%I64u order=%I64u", tradeResult.retcode, tradeResult.deal, tradeResult.order); Print("_______________________________________________________________________________________”); class="kw">return(true); class=class="str">"cmt">//-- exit the function break; class=class="str">"cmt">//--- success - order placed ok. exit the for loop } } else class=class="str">"cmt">//-- Order request failed { class=class="str">"cmt">//-- Print the order details PrintOrderDetails("Sending Failed"); class=class="str">"cmt">//-- order not sent or critical error found if(!ErrorAdvisor(__FUNCTION__, symbol, tradeResult.retcode) || IsStopped()) { Print(__FUNCTION__, ": ", symbol, " ERROR opening a BUY POSITION at: ", tradeRequest.price, ", Lot\\Vol: ", tradeRequest.volume); Print("_______________________________________________________________________________________”); class="kw">return(false); class=class="str">"cmt">//-- exit the function break; class=class="str">"cmt">//-- exit the for loop } } } class="kw">return(false); } class=class="str">"cmt">//-------------------------------------------------------------------+ class=class="str">"cmt">// OpenSellPosition(): Function to open a new sell entry order. | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool OpenSellPosition(class="type">ulong magicNumber, class="type">class="kw">string symbol, class="type">class="kw">double lotSize, class="type">int sl, class="type">int tp, class="type">class="kw">string positionComment) class="kw">export { class=class="str">"cmt">//-- first check if the ea is allowed to trade if(!TradingIsAllowed()) { class="kw">return(false); class=class="str">"cmt">//--- algo trading is disabled, exit function } class=class="str">"cmt">//-- reset the the tradeRequest and tradeResult values by zeroing them ZeroMemory(tradeRequest); ZeroMemory(tradeResult); class=class="str">"cmt">//-- initialize the parameters to open a sell position tradeRequest.type = ORDER_TYPE_SELL; tradeRequest.action = TRADE_ACTION_DEAL; tradeRequest.magic = magicNumber; tradeRequest.symbol = symbol; tradeRequest.tp = class="num">0; tradeRequest.sl = class="num">0; tradeRequest.comment = positionComment; tradeRequest.deviation = SymbolInfoInteger(symbol, SYMBOL_SPREAD) * class="num">2; class=class="str">"cmt">//-- set and moderate the lot size or volume lotSize = MathMax(lotSize, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN)); class=class="str">"cmt">//-- Verify that volume is not less than allowed minimum
「卖单发送的百次重试与风控截断」
手算出来的 lotSize 不能直接丢进交易请求。先把它夹在经纪商允许的最大成交量之内,再用品种的最小步长向下取整,避免因为 0.01 手精度问题被服务器拒单。这两步不做,后面 OrderSend 大概率返回 475(无效交易量)错误。 发送卖单前必须 ResetLastError,否则上一次残留的错误码会污染 ErrorAdvisor 的判断。循环上限设成 100 次迭代(loop 从 0 到 100 共 101 次),每次都重新抓取 SYMBOL_BID 做进场价,并依据 tp、sl 点数重算止损止盈——滑点环境下硬用旧价格,可能让实际风险偏离计划 2~3 个点以上。 成功判定只看 retcode 10008(已成交)或 10009(已放置),命中即打印订单号与成交价并 return(true) 退出。若 OrderSend 返回 false,则交给 ErrorAdvisor 决定要不要继续重试;一旦顾问函数说停或者 IsStopped() 为真,立刻打印错误并放弃。外汇与贵金属杠杆高,这类自动重试逻辑若不加熔断,可能在流动性真空时连续吃拒单。 别把百次重试当护身符 重试循环只是对抗瞬时报价过期,不是对抗经纪商规则。真遇到 10018(撮合队列满)或 10019(禁交易时段),硬跑 101 次也只是刷日志,该人工介入就得介入。
lotSize = MathMin(lotSize, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX)); class=class="str">"cmt">//-- Verify that volume is not more than allowed maximum lotSize = MathFloor(lotSize / SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP)) * SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); class=class="str">"cmt">//-- Round down to nearest volume step tradeRequest.volume = lotSize; ResetLastError(); class=class="str">"cmt">//--- reset error cache so that we get an accurate runtime error code in the ErrorAdvisor function for(class="type">int loop = class="num">0; loop <= class="num">100; loop++) class=class="str">"cmt">//-- try opening the order(class="num">101 max) times untill it is successful { class=class="str">"cmt">//--- update order opening price on each iteration tradeRequest.price = SymbolInfoDouble(symbol, SYMBOL_BID); class=class="str">"cmt">//-- set the take profit and stop loss on each iteration if(tp > class="num">0) { tradeRequest.tp = NormalizeDouble(tradeRequest.price - (tp * _Point), _Digits); } if(sl > class="num">0) { tradeRequest.sl = NormalizeDouble(tradeRequest.price + (sl * _Point), _Digits); } class=class="str">"cmt">//--- send order to the trade server if(OrderSend(tradeRequest, tradeResult)) { class=class="str">"cmt">//-- Print the order details PrintOrderDetails("Sent OK"); class=class="str">"cmt">//-- Confirm order execution if(tradeResult.retcode == class="num">10008 || tradeResult.retcode == class="num">10009) { Print("CONFIRMED: Successfully openend a ", symbol, " SELL POSITION #", tradeResult.order, ", Price: ", tradeResult.price); PrintFormat("retcode=%u deal=%I64u order=%I64u", tradeResult.retcode, tradeResult.deal, tradeResult.order); Print("_______________________________________________________________________________________"); class="kw">return(true); class=class="str">"cmt">//-- exit function break; class=class="str">"cmt">//--- success - order placed ok. exit for loop } } else class=class="str">"cmt">//-- Order request failed { class=class="str">"cmt">//-- Print the order details PrintOrderDetails("Sending Failed"); class=class="str">"cmt">//-- order not sent or critical error found if(!ErrorAdvisor(__FUNCTION__, symbol, tradeResult.retcode) || IsStopped()) { Print(symbol, " ERROR opening a SELL POSITION at: ", tradeRequest.price, ", Lot\Vol: ", tradeRequest.volume); Print("_______________________________________________________________________________________");
◍ 按持仓单号重设止损止盈的函数骨架
在 MT5 里用 EA 批量管理已有仓位,最稳的入口是按 ticket 精确选中持仓再改 SL/TP,而不是遍历所有持仓去猜。下面这段导出函数 SetSlTpByTicket 就是干这个的:先卡一道 TradingIsAllowed() 交易开关,再拿 PositionSelectByTicket 用单号锁仓。 选仓失败不要硬改,直接 Print 出 GetLastError() 并 return(false),否则会把错单塞给服务器。选中后立刻把 POSITION_SYMBOL、开仓价、成交量、现有 SL/TP、持仓类型全部用 PositionGet 系列捞进局部变量,后面算价才有依据。 算价前必须取 SYMBOL_DIGITS、SYMBOL_TRADE_STOPS_LEVEL、SYMBOL_POINT、SYMBOL_SPREAD,其中 stops level 是经纪商要求的止损最小距离(常以 point 倍数计),破规下单会被拒。买仓示例里 newSlPrice = entryPrice - (sl * symbolPoint) 就是把传入的整型点数 sl 折算成真实价格,tp 同理加在 entryPrice 上。 外汇与贵金属杠杆高、点差跳变频繁,SL/TP 距离若小于 symbolStopLevel 会直接报规范错误,实盘前务必在策略测试器用真实品种参数跑一遍校验。
class="type">bool SetSlTpByTicket(class="type">ulong positionTicket, class="type">int sl, class="type">int tp) class="kw">export { class=class="str">"cmt">//-- Function body } class=class="str">"cmt">//-- first check if the EA is allowed to trade if(!TradingIsAllowed()) { class="kw">return(false); class=class="str">"cmt">//--- algo trading is disabled, exit function } class=class="str">"cmt">//--- Confirm and select the position using the provided positionTicket ResetLastError(); class=class="str">"cmt">//--- Reset error cache incase of ticket selection errors if(PositionSelectByTicket(positionTicket)) { class=class="str">"cmt">//---Position selected Print("\r\n_______________________________________________________________________________________"); Print(__FUNCTION__, ": Position with ticket:", positionTicket, " selected and ready to set SLTP."); } else { Print("\r\n_______________________________________________________________________________________"); Print(__FUNCTION__, ": Selecting position with ticket:", positionTicket, " failed. ERROR: ", GetLastError()); class="kw">return(false); class=class="str">"cmt">//-- Exit the function } class=class="str">"cmt">//-- create variables to store the calculated tp and sl prices to send to the trade server class="type">class="kw">double tpPrice = class="num">0.0, slPrice = class="num">0.0; class="type">class="kw">double newTpPrice = class="num">0.0, newSlPrice = class="num">0.0; class=class="str">"cmt">//--- Position ticket selected, save the position properties class="type">class="kw">string positionSymbol = PositionGetString(POSITION_SYMBOL); class="type">class="kw">double entryPrice = PositionGetDouble(POSITION_PRICE_OPEN); class="type">class="kw">double volume = PositionGetDouble(POSITION_VOLUME); class="type">class="kw">double currentPositionSlPrice = PositionGetDouble(POSITION_SL); class="type">class="kw">double currentPositionTpPrice = PositionGetDouble(POSITION_TP); class="type">ENUM_POSITION_TYPE positionType = (class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); class=class="str">"cmt">//-- Get some information about the positions symbol class="type">int symbolDigits = (class="type">int)SymbolInfoInteger(positionSymbol, SYMBOL_DIGITS); class=class="str">"cmt">//-- Number of symbol decimal places class="type">int symbolStopLevel = (class="type">int)SymbolInfoInteger(positionSymbol, SYMBOL_TRADE_STOPS_LEVEL); class="type">class="kw">double symbolPoint = SymbolInfoDouble(positionSymbol, SYMBOL_POINT); class="type">class="kw">double positionPriceCurrent = PositionGetDouble(POSITION_PRICE_CURRENT); class="type">int spread = (class="type">int)SymbolInfoInteger(positionSymbol, SYMBOL_SPREAD); class=class="str">"cmt">//--Save the non-validated tp and sl prices if(positionType == POSITION_TYPE_BUY) class=class="str">"cmt">//-- Calculate and store the non-validated sl and tp prices { newSlPrice = entryPrice - (sl * symbolPoint); newTpPrice = entryPrice + (tp * symbolPoint);
修改挂单前先把 SL/TP 算准并打印出来
在真正调用 OrderSend 之前,代码先按持仓方向把止损止盈价算出来:SELL 仓位是 entryPrice + sl*point 作为新 SL,entryPrice - tp*point 作为新 TP;BUY 则反向加减。注意这里 sl、tp 是「点数」变量,乘的是 symbolPoint(每点价格),不是直接价格。 算完不算完,程序用 positionProperties 字符串把 ticket、volume、开仓价、当前 SL/TP、拟改 SL/TP、comment、magic 全部拼好再 Print 到日志。这样你在 MT5 专家日志里能直接看到「Current SL 1.09500 -> New Proposed SL 1.09300」这种对照,省得盲改。 随后有一段校验:若 sl==0 则 slPrice=0.0(代表不修改 SL),tp==0 同理。外汇和贵金属波动大、点差跳变频繁,0 值逻辑用错可能把保护垫直接撤掉,属于高风险操作,改之前先在策略测试器跑一遍。 BUY 仓位还会二次核验 TP 合法性:newTpPrice 必须高于 entryPrice + spread*point,且离 entryPrice 或当前价的距离不能小于 symbolStopLevel*point,否则 TP 不修改。经纪商最小止损距离(stop level)常藏在品种属性里,不读它就容易报『无效成交价』。
} else class=class="str">"cmt">//-- SELL POSITION { newSlPrice = entryPrice + (sl * symbolPoint); newTpPrice = entryPrice - (tp * symbolPoint); } class=class="str">"cmt">//-- Print position properties before modification class="type">class="kw">string positionProperties = "--> " + positionSymbol + " " + EnumToString(positionType) + " SLTP Modification Details" + " <--\r\n"; positionProperties += "------------------------------------------------------------\r\n"; positionProperties += "Ticket: " + (class="type">class="kw">string)positionTicket + "\r\n"; positionProperties += "Volume: " + StringFormat("%G", volume) + "\r\n"; positionProperties += "Price Open: " + StringFormat("%G", entryPrice) + "\r\n"; positionProperties += "Current SL: " + StringFormat("%G", currentPositionSlPrice) + " -> New Proposed SL: " + (class="type">class="kw">string)newSlPrice + "\r\n"; positionProperties += "Current TP: " + StringFormat("%G", currentPositionTpPrice) + " -> New Proposed TP: " + (class="type">class="kw">string)newTpPrice + "\r\n"; positionProperties += "Comment: " + PositionGetString(POSITION_COMMENT) + "\r\n"; positionProperties += "Magic Number: " + (class="type">class="kw">string)PositionGetInteger(POSITION_MAGIC) + "\r\n"; positionProperties += "---"; Print(positionProperties); class=class="str">"cmt">//-- validate the sl and tp to a proper class="type">class="kw">double that can be used in the OrderSend() function if(sl == class="num">0) { slPrice = class="num">0.0; } if(tp == class="num">0) { tpPrice = class="num">0.0; } class=class="str">"cmt">//--- Check if the sl and tp are valid in relation to the current price and set the tpPrice if(positionType == POSITION_TYPE_BUY) { class=class="str">"cmt">//-- calculate the new sl and tp prices newTpPrice = class="num">0.0; newSlPrice = class="num">0.0; if(tp > class="num">0) { newTpPrice = entryPrice + (tp * symbolPoint); } if(sl > class="num">0) { newSlPrice = entryPrice - (sl * symbolPoint); } class=class="str">"cmt">//-- save the new sl and tp prices incase they don&class="macro">#x27;t change afte validation below tpPrice = newTpPrice; slPrice = newSlPrice; if( class=class="str">"cmt">//-- Check if specified TP is valid tp > class="num">0 && ( newTpPrice <= entryPrice + (spread * symbolPoint) || newTpPrice <= positionPriceCurrent || ( newTpPrice - entryPrice < symbolStopLevel * symbolPoint || (positionPriceCurrent > entryPrice && newTpPrice - positionPriceCurrent < symbolStopLevel * symbolPoint) ) ) ) { class=class="str">"cmt">//-- Specified TP price is invalid, don&class="macro">#x27;t modify the TP
「卖单止盈止损的合法性拦截」
空单的 TP/SL 推算逻辑与多单镜像:TP 用 entryPrice 减去点数乘 symbolPoint,SL 用 entryPrice 加上同样距离。落单前必须再过一遍经纪商 stop level 与实时价位的冲突,否则修改请求会被服务端静默拒绝。 卖单 TP 的无效判定比多单更苛刻——除了不能高于 entryPrice 减点差、不能高于当前价,还要处理浮亏持仓时当前价已低于开仓价的情况:若 positionPriceCurrent < entryPrice 且两者到 TP 的距离小于 symbolStopLevel * symbolPoint,同样判为非法。 代码里先用 tpPrice = newTpPrice 把推算值存底,验证不通过才回退到 currentPositionTpPrice。这种「先赋值后否决」的写法能保证未触发修改时结构体里至少是合理默认值,不会留 0.0 误伤持仓。 外汇与贵金属杠杆高,stop level 随流动性跳动,实盘跑这段前建议在策略测试器用 2023 年 XAUUSD 的 M5 数据复算一次 symbolStopLevel 取值,确认点差放大时段不会批量误拒。
if(positionType == POSITION_TYPE_SELL) { class=class="str">"cmt">//-- calculate the new sl and tp prices newTpPrice = class="num">0.0; newSlPrice = class="num">0.0; if(tp > class="num">0) { newTpPrice = entryPrice - (tp * symbolPoint); } if(sl > class="num">0) { newSlPrice = entryPrice + (sl * symbolPoint); } class=class="str">"cmt">//-- save the new sl and tp prices incase they don&class="macro">#x27;t change afte validation below tpPrice = newTpPrice; slPrice = newSlPrice; if( class=class="str">"cmt">//-- Check if specified TP price is valid tp > class="num">0 && ( newTpPrice >= entryPrice - (spread * symbolPoint) || newTpPrice >= positionPriceCurrent || ( entryPrice - newTpPrice < symbolStopLevel * symbolPoint || (positionPriceCurrent < entryPrice && positionPriceCurrent - newTpPrice < symbolStopLevel * symbolPoint) ) ) ) { class=class="str">"cmt">//-- Specified TP price is invalid, don&class="macro">#x27;t modify the TP Print( "Specified proposed ", positionSymbol, " TP Price at ", newTpPrice,
◍ 修改挂单止损止盈前的校验与重发逻辑
这段逻辑在真正下发 SL/TP 修改前,先拦截不合规的价格。若新 TP 不大于当前价,或距离 entry / 当前价小于 symbolStopLevel * symbolPoint,就保留原 TP 并打印提示,避免服务器拒单。SL 同理:新 SL 必须高于当前价且留出最小止损距离,否则回退到 currentPositionSlPrice。 校验通过后,代码把确认后的开仓价、当前价、新旧 SL/TP 拼成字符串用 Print 输出,方便在 MT5 专家日志里逐单核对。随后 ZeroMemory 清空 tradeRequest 与 tradeResult,再填 TRADE_ACTION_SLTP 及持仓 ticket、品种、价格。 真正发单用了 for(loop=0; loop<=100; loop++) 的 101 次重试:OrderSend 成功后若 retcode 为 10008(已成交)或 10009(已受理)才视为成功。外汇与贵金属杠杆高,SL/TP 被经纪商最小止损位卡掉是常态,这种重试能降低瞬时报价冲突导致的修改失败概率。
" is invalid since current ", positionSymbol, " price is at ", positionPriceCurrent,
"\r\nCurrent TP at ", StringFormat("%G", currentPositionTpPrice), " will not be changed!"
);
tpPrice = currentPositionTpPrice;
}
if( class=class="str">"cmt">//-- Check if specified SL price is valid
sl > class="num">0 &&
(
newSlPrice <= positionPriceCurrent ||
newSlPrice - entryPrice < symbolStopLevel * symbolPoint ||
newSlPrice - positionPriceCurrent < symbolStopLevel * symbolPoint
)
)
{
class=class="str">"cmt">//-- Specified SL price is invalid, don&class="macro">#x27;t modify the SL
Print(
"Specified proposed ", positionSymbol,
" SL Price at ", newSlPrice,
" is invalid since current ", positionSymbol, " price is at ", positionPriceCurrent,
"\r\nCurrent SL at ", StringFormat("%G", currentPositionSlPrice), " will not be changed!"
);
slPrice = currentPositionSlPrice;
}
}
class=class="str">"cmt">//-- Print verified position properties before modification
positionProperties = "---\r\n";
positionProperties += "--> Validated and Confirmed SL and TP: <--\r\n";
positionProperties += "Price Open: " + StringFormat("%G", entryPrice) + ", Price Current: " + StringFormat("%G", positionPriceCurrent) + "\r\n";
positionProperties += "Current SL: " + StringFormat("%G", currentPositionSlPrice) + " -> New SL: " + (class="type">class="kw">string)slPrice + "\r\n";
positionProperties += "Current TP: " + StringFormat("%G", currentPositionTpPrice) + " -> New TP: " + (class="type">class="kw">string)tpPrice + "\r\n";
Print(positionProperties);
class=class="str">"cmt">//-- reset the the tradeRequest and tradeResult values by zeroing them
ZeroMemory(tradeRequest);
ZeroMemory(tradeResult);
class=class="str">"cmt">//-- initialize the parameters to set the sltp
tradeRequest.action = TRADE_ACTION_SLTP; class=class="str">"cmt">//-- Trade operation type for setting sl and tp
tradeRequest.position = positionTicket;
tradeRequest.symbol = positionSymbol;
tradeRequest.sl = slPrice;
tradeRequest.tp = tpPrice;
ResetLastError(); class=class="str">"cmt">//--- reset error cache so that we get an accurate runtime error code in the ErrorAdvisor function
for(class="type">int loop = class="num">0; loop <= class="num">100; loop++) class=class="str">"cmt">//-- try modifying the sl and tp class="num">101 times untill the request is successful
{
class=class="str">"cmt">//--- send order to the trade server
if(OrderSend(tradeRequest, tradeResult))
{
class=class="str">"cmt">//-- Confirm order execution
if(tradeResult.retcode == class="num">10008 || tradeResult.retcode == class="num">10009)
{用持仓单号改止损止盈的落地写法
在 MT5 里按 ticket 改 SL/TP,第一步不是直接发单,而是先确认自动交易开关。TradingIsAllowed() 返回 false 就直接 return,避免 EA 在禁交易时段瞎报错。 选中持仓用 PositionSelectByTicket(positionTicket),成功才继续;失败立刻打印 GetLastError() 并退出。很多人漏了 ResetLastError(),结果拿到的是上一次调用的残留错误码。 下面这段是修改成功后的回执打印与退出逻辑,注意 break 放在 return 之后其实永远不会执行,属于防御性写法。成功路径返回 true,失败路径经 ErrorAdvisor 判断后返回 false,调用方据此决定是否重试。 别把 return 后的 break 当真流程 代码里 return(true) 后面跟 break,编译器不会报错,但 break 永远跑不到。写的时候图个对称,真调试时别被它误导,以为循环还会继续。 外汇与贵金属杠杆高,改 SL/TP 的市价单请求可能因点差跳空被拒,实盘前务必在策略测试器用历史数据跑一遍错误分支。
PrintFormat("Successfully modified SLTP for #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType)); PrintFormat("retcode=%u runtime_code=%u", tradeResult.retcode, GetLastError()); Print("_______________________________________________________________________________________\r\n\r\n"); class="kw">return(true); class=class="str">"cmt">//-- exit function break; class=class="str">"cmt">//--- success - order placed ok. exit for loop } } else class=class="str">"cmt">//-- Order request failed { class=class="str">"cmt">//-- order not sent or critical error found if(!ErrorAdvisor(__FUNCTION__, positionSymbol, tradeResult.retcode) || IsStopped()) { PrintFormat("ERROR modified SLTP for #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType)); Print("_______________________________________________________________________________________\r\n\r\n"); class="kw">return(false); class=class="str">"cmt">//-- exit function break; class=class="str">"cmt">//-- exit for loop } } } class="type">bool SetSlTpByTicket(class="type">ulong positionTicket, class="type">int sl, class="type">int tp) class="kw">export { class=class="str">"cmt">//-- first check if the EA is allowed to trade if(!TradingIsAllowed()) { class="kw">return(false); class=class="str">"cmt">//--- algo trading is disabled, exit function } class=class="str">"cmt">//--- Confirm and select the position using the provided positionTicket ResetLastError(); class=class="str">"cmt">//--- Reset error cache incase of ticket selection errors if(PositionSelectByTicket(positionTicket)) { class=class="str">"cmt">//---Position selected Print("\r\n_______________________________________________________________________________________"); Print(__FUNCTION__, ": Position with ticket:", positionTicket, " selected and ready to set SLTP."); } else { Print("\r\n_______________________________________________________________________________________"); Print(__FUNCTION__, ": Selecting position with ticket:", positionTicket, " failed. ERROR: ", GetLastError()); class="kw">return(false); class=class="str">"cmt">//-- Exit the function } class=class="str">"cmt">//-- create variables to store the calculated tp and sl prices to send to the trade server class="type">class="kw">double tpPrice = class="num">0.0, slPrice = class="num">0.0; class="type">class="kw">double newTpPrice = class="num">0.0, newSlPrice = class="num">0.0; class=class="str">"cmt">//--- Position ticket selected, save the position properties class="type">class="kw">string positionSymbol = PositionGetString(POSITION_SYMBOL); class="type">class="kw">double entryPrice = PositionGetDouble(POSITION_PRICE_OPEN);
「持仓属性抓取与止损止盈预演」
改仓前先把当前持仓的关键字段读出来,是避免乱改 SL/TP 的前提。下面这段从持仓池里取 volume、SL、TP、类型,再补抓交易品种的 digits、stops level、point、实时价与 spread,外汇与贵金属点差跳动大,symbolStopLevel 和 symbolPoint 直接决定你算出来的挂单价是否会被服务器拒。 double volume = PositionGetDouble(POSITION_VOLUME); // 持仓手数 double currentPositionSlPrice = PositionGetDouble(POSITION_SL); // 当前止损价 double currentPositionTpPrice = PositionGetDouble(POSITION_TP); // 当前止盈价 ENUM_POSITION_TYPE positionType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); // 持仓方向买或卖 int symbolDigits = (int)SymbolInfoInteger(positionSymbol, SYMBOL_DIGITS); // 品种小数位 int symbolStopLevel = (int)SymbolInfoInteger(positionSymbol, SYMBOL_TRADE_STOPS_LEVEL); // 最小止损距离点数 按方向套用偏移量生成候选 SL/TP:买仓用 entryPrice - sl*point 做止损、+ tp*point 做止盈;卖仓反过来。注意这里 newSlPrice 只是「未校验价」,距离现价若小于 symbolStopLevel 点,后续 OrderSend 可能直接返回无效。 if(positionType == POSITION_TYPE_BUY) { newSlPrice = entryPrice - (sl * symbolPoint); newTpPrice = entryPrice + (tp * symbolPoint); } else { newSlPrice = entryPrice + (sl * symbolPoint); newTpPrice = entryPrice - (tp * symbolPoint); } 打印明细用 StringFormat("%G", ...) 压缩浮点显示,能把 1.05000000 折成 1.05,日志可读性高很多。sl 或 tp 入参为 0 时直接把对应价格置 0.0,代表不修改该端,这一步不清零容易把原有止损误覆盖。外汇贵金属属高风险品种,任何改仓操作前都建议在策略测试器用历史数据跑一遍拒单概率。
class="type">class="kw">double volume = PositionGetDouble(POSITION_VOLUME); class="type">class="kw">double currentPositionSlPrice = PositionGetDouble(POSITION_SL); class="type">class="kw">double currentPositionTpPrice = PositionGetDouble(POSITION_TP); class="type">ENUM_POSITION_TYPE positionType = (class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); class=class="str">"cmt">//-- Get some information about the positions symbol class="type">int symbolDigits = (class="type">int)SymbolInfoInteger(positionSymbol, SYMBOL_DIGITS); class=class="str">"cmt">//-- Number of symbol decimal places class="type">int symbolStopLevel = (class="type">int)SymbolInfoInteger(positionSymbol, SYMBOL_TRADE_STOPS_LEVEL); class="type">class="kw">double symbolPoint = SymbolInfoDouble(positionSymbol, SYMBOL_POINT); class="type">class="kw">double positionPriceCurrent = PositionGetDouble(POSITION_PRICE_CURRENT); class="type">int spread = (class="type">int)SymbolInfoInteger(positionSymbol, SYMBOL_SPREAD); class=class="str">"cmt">//--Save the non-validated tp and sl prices if(positionType == POSITION_TYPE_BUY) class=class="str">"cmt">//-- Calculate and store the non-validated sl and tp prices { newSlPrice = entryPrice - (sl * symbolPoint); newTpPrice = entryPrice + (tp * symbolPoint); } else class=class="str">"cmt">//-- SELL POSITION { newSlPrice = entryPrice + (sl * symbolPoint); newTpPrice = entryPrice - (tp * symbolPoint); } class=class="str">"cmt">//-- Print position properties before modification class="type">class="kw">string positionProperties = "--> " + positionSymbol + " " + EnumToString(positionType) + " SLTP Modification Details" + " <--\r\n"; positionProperties += "------------------------------------------------------------\r\n"; positionProperties += "Ticket: " + (class="type">class="kw">string)positionTicket + "\r\n"; positionProperties += "Volume: " + StringFormat("%G", volume) + "\r\n"; positionProperties += "Price Open: " + StringFormat("%G", entryPrice) + "\r\n"; positionProperties += "Current SL: " + StringFormat("%G", currentPositionSlPrice) + " -> New Proposed SL: " + (class="type">class="kw">string)newSlPrice + "\r\n"; positionProperties += "Current TP: " + StringFormat("%G", currentPositionTpPrice) + " -> New Proposed TP: " + (class="type">class="kw">string)newTpPrice + "\r\n"; positionProperties += "Comment: " + PositionGetString(POSITION_COMMENT) + "\r\n"; positionProperties += "Magic Number: " + (class="type">class="kw">string)PositionGetInteger(POSITION_MAGIC) + "\r\n"; positionProperties += "---"; Print(positionProperties); class=class="str">"cmt">//-- validate the sl and tp to a proper class="type">class="kw">double that can be used in the OrderSend() function if(sl == class="num">0) { slPrice = class="num">0.0; } if(tp == class="num">0) { tpPrice = class="num">0.0; } class=class="str">"cmt">//--- Check if the sl and tp are valid in relation to the current price and set the tpPrice if(positionType == POSITION_TYPE_BUY) { class=class="str">"cmt">//-- calculate the new sl and tp prices newTpPrice = class="num">0.0;
◍ 挂单前先卡死止损止盈的合法性
修改持仓的 TP/SL 不是填个数就完事。MT5 里经纪商有 StopLevel 限制,点差也会吃掉紧贴市价的挂单,代码里必须先在本地算好 newTpPrice / newSlPrice,再做一轮有效性拦截。 以多单为例,TP 距离要用 symbolPoint 折算:newTpPrice = entryPrice + tp * symbolPoint。若 newTpPrice 小于等于「入场价 + 点差折算」或小于等于当前价,又或者与入场价、与当前价的间距小于 symbolStopLevel * symbolPoint,这个 TP 就会被判定非法,直接回退到 currentPositionTpPrice,不改动原持仓。 空单镜像处理:newTpPrice = entryPrice - tp * symbolPoint,SL 则要求 newSlPrice 必须严格低于当前价 positionPriceCurrent,且距入场价、距当前价都不能小于 StopLevel。任何一条不满足,Print 打出原因后 slPrice 沿用旧值。 外汇与贵金属杠杆高、点差跳变频繁,这类校验能避免「看似改单成功、实际 broker 拒单」的暗亏。开 MT5 把这段塞进 EA 的修改持仓分支,用 EURUSD 实测点差 2~3 点时 TP 贴边会被拦下来。
newSlPrice = class="num">0.0; if(tp > class="num">0) { newTpPrice = entryPrice + (tp * symbolPoint); } if(sl > class="num">0) { newSlPrice = entryPrice - (sl * symbolPoint); } class=class="str">"cmt">//-- save the new sl and tp prices incase they don&class="macro">#x27;t change afte validation below tpPrice = newTpPrice; slPrice = newSlPrice; if( class=class="str">"cmt">//-- Check if specified TP is valid tp > class="num">0 && ( newTpPrice <= entryPrice + (spread * symbolPoint) || newTpPrice <= positionPriceCurrent || ( newTpPrice - entryPrice < symbolStopLevel * symbolPoint || (positionPriceCurrent > entryPrice && newTpPrice - positionPriceCurrent < symbolStopLevel * symbolPoint) ) ) ) { class=class="str">"cmt">//-- Specified TP price is invalid, don&class="macro">#x27;t modify the TP Print( "Specified proposed ", positionSymbol, " TP Price at ", newTpPrice, " is invalid since current ", positionSymbol, " price is at ", positionPriceCurrent, "\r\nCurrent TP at ", StringFormat("%G", currentPositionTpPrice), " will not be changed!" ); tpPrice = currentPositionTpPrice; } if( class=class="str">"cmt">//-- Check if specified SL price is valid sl > class="num">0 && ( newSlPrice >= positionPriceCurrent || entryPrice - newSlPrice < symbolStopLevel * symbolPoint || positionPriceCurrent - newSlPrice < symbolStopLevel * symbolPoint ) ) { class=class="str">"cmt">//-- Specified SL price is invalid, don&class="macro">#x27;t modify the SL Print( "Specified proposed ", positionSymbol, " SL Price at ", newSlPrice, " is invalid since current ", positionSymbol, " price is at ", positionPriceCurrent, "\r\nCurrent SL at ", StringFormat("%G", currentPositionSlPrice), " will not be changed!" ); slPrice = currentPositionSlPrice; } } if(positionType == POSITION_TYPE_SELL) { class=class="str">"cmt">//-- calculate the new sl and tp prices newTpPrice = class="num">0.0; newSlPrice = class="num">0.0; if(tp > class="num">0) { newTpPrice = entryPrice - (tp * symbolPoint); }
挂单前先卡死止损止盈的合规边界
在 MT5 里用 EA 改仓,最容易被忽略的是经纪商对 SL/TP 与当前价的最小距离限制,也就是 symbolStopLevel 个点。下面这段逻辑就是在写入前先做校验:SL 距离入场价或当前价小于 stop level,TP 距离不满足正向缓冲,就直接沿用原值不改动。 代码先用 sl>0 算出 newSlPrice = entryPrice + sl*_Point,再把暂存价赋给 slPrice/tpPrice。随后两个 if 分别检查 TP 和 SL 的合法性:TP 若低于入场减 spread 点、或低于当前价、或距离不足 stop level,就 Print 提示并回退到 currentPositionTpPrice;SL 若大于等于当前价或距离不足 stop level,同样回退。 实盘里外汇和贵金属杠杆高、滑点频繁,这类校验能避免‘修改订单被拒’的静默失败。把 symbolStopLevel 和 spread 打进日志,你能在策略测试器里直接看到哪些参数组合会被经纪商挡掉。
if(sl > class="num">0) { newSlPrice = entryPrice + (sl * symbolPoint); } class=class="str">"cmt">//-- save the new sl and tp prices incase they don&class="macro">#x27;t change afte validation below tpPrice = newTpPrice; slPrice = newSlPrice; if( class=class="str">"cmt">//-- Check if specified TP price is valid tp > class="num">0 && ( newTpPrice >= entryPrice - (spread * symbolPoint) || newTpPrice >= positionPriceCurrent || ( entryPrice - newTpPrice < symbolStopLevel * symbolPoint || (positionPriceCurrent < entryPrice && positionPriceCurrent - newTpPrice < symbolStopLevel * symbolPoint) ) ) ) { class=class="str">"cmt">//-- Specified TP price is invalid, don&class="macro">#x27;t modify the TP Print( "Specified proposed ", positionSymbol, " TP Price at ", newTpPrice, " is invalid since current ", positionSymbol, " price is at ", positionPriceCurrent, "\r\nCurrent TP at ", StringFormat("%G", currentPositionTpPrice), " will not be changed!" ); tpPrice = currentPositionTpPrice; } if( class=class="str">"cmt">//-- Check if specified SL price is valid sl > class="num">0 && ( newSlPrice <= positionPriceCurrent || newSlPrice - entryPrice < symbolStopLevel * symbolPoint || newSlPrice - positionPriceCurrent < symbolStopLevel * symbolPoint ) ) { class=class="str">"cmt">//-- Specified SL price is invalid, don&class="macro">#x27;t modify the SL Print( "Specified proposed ", positionSymbol, " SL Price at ", newSlPrice, " is invalid since current ", positionSymbol, " price is at ", positionPriceCurrent, "\r\nCurrent SL at ", StringFormat("%G", currentPositionSlPrice), " will not be changed!" ); slPrice = currentPositionSlPrice; } } class=class="str">"cmt">//-- Print verified position properties before modification positionProperties = "---\r\n"; positionProperties += "--> Validated and Confirmed SL and TP: <--\r\n"; positionProperties += "Price Open: " + StringFormat("%G", entryPrice) + ", Price Current: " + StringFormat("%G", positionPriceCurrent) + "\r\n"; positionProperties += "Current SL: " + StringFormat("%G", currentPositionSlPrice) + " -> New SL: " + (class="type">class="kw">string)slPrice + "\r\n";
「用重试循环硬改持仓止盈止损」
修改持仓的 SL/TP 不是一次 OrderSend 就完事。经纪商返回 10008(TRADE_RETCODE_DONE)或 10009(TRADE_RETCODE_DONE_PARTIAL)才算改单成功,其余情况可能只是临时拒绝或网络抖动,所以代码里套了一个上限 101 次的 for 循环反复提交 tradeRequest。 循环前先用 ZeroMemory 把 tradeRequest 和 tradeResult 清零,再填 TRADE_ACTION_SLTP、position、symbol、sl、tp 字段;每次发单前 ResetLastError,避免上一次错误码污染本次诊断。若 OrderSend 返回 true 且 retcode 为 10008/10009,打印成功日志并直接 return(true) 退出函数。 失败分支里调用 ErrorAdvisor 做错误归类,只有当它返回 false(不可恢复错误)或 IsStopped() 为真时才退出;否则循环会继续跑到 loop=100。外汇与贵金属杠杆高,SL/TP 修改失败可能让浮亏越过预期边界,实盘前应在 MT5 策略测试器用点差放大环境验证这套重试逻辑。 下面这段是改单核心循环,注意 break 写在 return 之后实际不可达,属冗余写法,复制时可删掉。
positionProperties += "Current TP: " + StringFormat("%G", currentPositionTpPrice) + " -> New TP: " + (class="type">class="kw">string)tpPrice + "\r\n"; Print(positionProperties); class=class="str">"cmt">//-- reset the the tradeRequest and tradeResult values by zeroing them ZeroMemory(tradeRequest); ZeroMemory(tradeResult); class=class="str">"cmt">//-- initialize the parameters to set the sltp tradeRequest.action = TRADE_ACTION_SLTP; class=class="str">"cmt">//-- Trade operation type for setting sl and tp tradeRequest.position = positionTicket; tradeRequest.symbol = positionSymbol; tradeRequest.sl = slPrice; tradeRequest.tp = tpPrice; ResetLastError(); class=class="str">"cmt">//--- reset error cache so that we get an accurate runtime error code in the ErrorAdvisor function for(class="type">int loop = class="num">0; loop <= class="num">100; loop++) class=class="str">"cmt">//-- try modifying the sl and tp class="num">101 times untill the request is successful { class=class="str">"cmt">//--- send order to the trade server if(OrderSend(tradeRequest, tradeResult)) { class=class="str">"cmt">//-- Confirm order execution if(tradeResult.retcode == class="num">10008 || tradeResult.retcode == class="num">10009) { PrintFormat("Successfully modified SLTP for #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType)); PrintFormat("retcode=%u runtime_code=%u", tradeResult.retcode, GetLastError()); Print("_______________________________________________________________________________________\r\n\r\n"); class="kw">return(true); class=class="str">"cmt">//-- exit function break; class=class="str">"cmt">//--- success - order placed ok. exit for loop } } else class=class="str">"cmt">//-- Order request failed { class=class="str">"cmt">//-- order not sent or critical error found if(!ErrorAdvisor(__FUNCTION__, positionSymbol, tradeResult.retcode) || IsStopped()) { PrintFormat("ERROR modified SLTP for #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType)); Print("_______________________________________________________________________________________\r\n\r\n"); class="kw">return(false); class=class="str">"cmt">//-- exit function break; class=class="str">"cmt">//-- exit for loop } } } class="kw">return(false); } class="type">bool ClosePositionByTicket(class="type">ulong positionTicket) class="kw">export { class=class="str">"cmt">//--- Function body } class=class="str">"cmt">//-- first check if the EA is allowed to trade if(!TradingIsAllowed()) { class="kw">return(false); class=class="str">"cmt">//--- algo trading is disabled, exit function } class=class="str">"cmt">//--- Confirm and select the position using the provided positionTicket ResetLastError(); class=class="str">"cmt">//--- Reset error cache incase of ticket selection errors
◍ 按持仓单号回收仓位的代码骨架
在 EA 里平掉某一笔特定仓位,第一步不是直接发单,而是用 PositionSelectByTicket 按单号把持仓激活到当前上下文。选不中就立刻 GetLastError 打印错误并 return(false),否则后续读取的属性全是脏值。 选中后要把 symbol、volume、type 三个字段用 PositionGetString / PositionGetDouble / PositionGetInteger 落盘保存。下面这段把持仓关键属性拼成可读字符串再 Print,方便你在 MT5 专家日志里一眼核对 ticket、开仓价、SL、TP、magic。 tradeRequest 每次复用前必须 ZeroMemory 清结构体,否则上一次平仓残留的字段可能污染新请求。deviation 设为 SYMBOL_SPREAD * 2,意味着允许滑点约等于两倍点差——在黄金这种点差跳变快的品种上,过小的值会提高废单概率,过大则增加滑点成本,建议你在回测里按品种实测。 平仓方向由 positionType 决定:买仓用 SYMBOL_BID 配 ORDER_TYPE_SELL,卖仓反之。代码里只写了买仓分支的收尾,卖仓分支需对称补全。
if(PositionSelectByTicket(positionTicket)) { class=class="str">"cmt">//---Position selected Print("..........................................................................................."); Print(__FUNCTION__, ": Position with ticket:", positionTicket, " selected and ready to be closed."); } else { Print("..........................................................................................."); Print(__FUNCTION__, ": Selecting position with ticket:", positionTicket, " failed. ERROR: ", GetLastError()); class="kw">return(false); class=class="str">"cmt">//-- Exit the function } class=class="str">"cmt">//--- Position ticket selected, save the position properties class="type">class="kw">string positionSymbol = PositionGetString(POSITION_SYMBOL); class="type">class="kw">double positionVolume = PositionGetDouble(POSITION_VOLUME); class="type">ENUM_POSITION_TYPE positionType = (class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); class=class="str">"cmt">//-- Print position properties before closing it class="type">class="kw">string positionProperties; positionProperties += "-- " + positionSymbol + " " + EnumToString(positionType) + " Details" + " -------------------------------------------------------------\r\n"; positionProperties += "Ticket: " + (class="type">class="kw">string)positionTicket + "\r\n"; positionProperties += "Volume: " + StringFormat("%G", PositionGetDouble(POSITION_VOLUME)) + "\r\n"; positionProperties += "Price Open: " + StringFormat("%G", PositionGetDouble(POSITION_PRICE_OPEN)) + "\r\n"; positionProperties += "SL: " + StringFormat("%G", PositionGetDouble(POSITION_SL)) + "\r\n"; positionProperties += "TP: " + StringFormat("%G", PositionGetDouble(POSITION_TP)) + "\r\n"; positionProperties += "Comment: " + PositionGetString(POSITION_COMMENT) + "\r\n"; positionProperties += "Magic Number: " + (class="type">class="kw">string)PositionGetInteger(POSITION_MAGIC) + "\r\n"; positionProperties += "_______________________________________________________________________________________"; Print(positionProperties); class=class="str">"cmt">//-- reset the the tradeRequest and tradeResult values by zeroing them ZeroMemory(tradeRequest); ZeroMemory(tradeResult); class=class="str">"cmt">//-- initialize the trade reqiest parameters to close the position tradeRequest.action = TRADE_ACTION_DEAL; class=class="str">"cmt">//-- Trade operation type for closing a position tradeRequest.position = positionTicket; tradeRequest.symbol = positionSymbol; tradeRequest.volume = positionVolume; tradeRequest.deviation = SymbolInfoInteger(positionSymbol, SYMBOL_SPREAD) * class="num">2; class=class="str">"cmt">//--- Set the price and order type of the position being closed if(positionType == POSITION_TYPE_BUY) { tradeRequest.price = SymbolInfoDouble(positionSymbol, SYMBOL_BID); tradeRequest.type = ORDER_TYPE_SELL; } elseclass=class="str">"cmt">//--- For sell type positions {
用重试循环堵住平仓失败漏洞
MT5 发单到交易服务器不是百分百即时成功的,尤其在高波动的贵金属或外汇行情里,retcode 非 10008/10009 的概率明显上升。这段逻辑用 for 循环最多跑 101 次去平掉指定 ticket 的仓位,直到 OrderSend 返回成功且成交回码为 TRADE_RETCODE_DONE(10008) 或 TRADE_RETCODE_PLACED(10009) 才退出函数。 循环前先 ResetLastError() 清掉错误缓存,否则 ErrorAdvisor 里拿到的 runtime error 可能是上一笔残留码,误导排查。每次失败走 else 分支调用 ErrorAdvisor 做分类处理,若顾问判定不可恢复或脚本被停止(IsStopped),直接 return false 并 break,不再空耗。 ClosePositionByTicket 入口先卡 TradingIsAllowed(),自动交易被终端禁用时立刻返回 false,避免在禁交易状态下反复发请求。外汇与贵金属杠杆高,这类重试机制能降低滑点拒单造成的仓位失控,但无法消除极端流动性下的穿价风险,参数 100 次上限可按经纪商延迟自行下调。
tradeRequest.price = SymbolInfoDouble(positionSymbol, SYMBOL_ASK); tradeRequest.type = ORDER_TYPE_BUY; } ResetLastError(); class=class="str">"cmt">//--- reset error cache so that we get an accurate runtime error code in the ErrorAdvisor function for(class="type">int loop = class="num">0; loop <= class="num">100; loop++) class=class="str">"cmt">//-- try closing the position class="num">101 times untill the request is successful { class=class="str">"cmt">//--- send order to the trade server if(OrderSend(tradeRequest, tradeResult)) { class=class="str">"cmt">//-- Confirm order execution if(tradeResult.retcode == class="num">10008 || tradeResult.retcode == class="num">10009) { Print(__FUNCTION__, "_________________________________________________________________________"); PrintFormat("Successfully closed position #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType)); PrintFormat("retcode=%u runtime_code=%u", tradeResult.retcode, GetLastError()); Print("_______________________________________________________________________________________"); class="kw">return(true); class=class="str">"cmt">//-- exit function break; class=class="str">"cmt">//--- success - order placed ok. exit for loop } } else class=class="str">"cmt">//-- position closing request failed { class=class="str">"cmt">//-- order not sent or critical error found if(!ErrorAdvisor(__FUNCTION__, positionSymbol, tradeResult.retcode) || IsStopped()) { Print(__FUNCTION__, "_________________________________________________________________________"); PrintFormat("ERROR closing position #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType)); Print("_______________________________________________________________________________________"); class="kw">return(false); class=class="str">"cmt">//-- exit function break; class=class="str">"cmt">//-- exit for loop } } } class="type">bool ClosePositionByTicket(class="type">ulong positionTicket) class="kw">export { class=class="str">"cmt">//-- first check if the EA is allowed to trade if(!TradingIsAllowed()) { class="kw">return(false); class=class="str">"cmt">//--- algo trading is disabled, exit function } class=class="str">"cmt">//--- Confirm and select the position using the provided positionTicket ResetLastError(); class=class="str">"cmt">//--- Reset error cache incase of ticket selection errors if(PositionSelectByTicket(positionTicket)) { class=class="str">"cmt">//---Position selected Print("...........................................................................................");
「平仓前先把持仓属性打印出来」
在 MT5 的持仓管理函数里,选中持仓后第一件事不是直接发单,而是把关键属性抓出来留痕。下面这段逻辑先用 PositionGetString / PositionGetDouble / PositionGetInteger 把品种、手数、方向、开仓价、SL、TP、注释和魔术码读出来,拼成一段可读文本用 Print 打到日志。 注意 deviation 的设定:用 SymbolInfoInteger(positionSymbol, SYMBOL_SPREAD) * 2 取当前点差的两倍作为允许偏差。这意味着当市场流动性薄、点差从常态 1~2 点跳到 5 点以上时,平仓请求容忍的滑点也同步放大,避免因为微小偏差被券商拒绝。 平仓方向的配对是硬规则:BUY 持仓只能以 SYMBOL_BID 挂 ORDER_TYPE_SELL 平,SELL 持仓以 SYMBOL_ASK 挂 ORDER_TYPE_BUY。写错一边,OrderSend 会直接返回非法请求错误。外汇与贵金属杠杆高,实盘跑这段前建议在策略测试器用历史数据验证滑点容忍是否合乎你的风控。
Print(__FUNCTION__, ": Position with ticket:", positionTicket, " selected and ready to be closed."); } else { Print("..........................................................................................."); Print(__FUNCTION__, ": Selecting position with ticket:", positionTicket, " failed. ERROR: ", GetLastError()); class="kw">return(false); class=class="str">"cmt">//-- Exit the function } class=class="str">"cmt">//--- Position ticket selected, save the position properties class="type">class="kw">string positionSymbol = PositionGetString(POSITION_SYMBOL); class="type">class="kw">double positionVolume = PositionGetDouble(POSITION_VOLUME); class="type">ENUM_POSITION_TYPE positionType = (class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); class=class="str">"cmt">//-- Print position properties before closing it class="type">class="kw">string positionProperties; positionProperties += "-- " + positionSymbol + " " + EnumToString(positionType) + " Details" + " -------------------------------------------------------------\r\n"; positionProperties += "Ticket: " + (class="type">class="kw">string)positionTicket + "\r\n"; positionProperties += "Volume: " + StringFormat("%G", PositionGetDouble(POSITION_VOLUME)) + "\r\n"; positionProperties += "Price Open: " + StringFormat("%G", PositionGetDouble(POSITION_PRICE_OPEN)) + "\r\n"; positionProperties += "SL: " + StringFormat("%G", PositionGetDouble(POSITION_SL)) + "\r\n"; positionProperties += "TP: " + StringFormat("%G", PositionGetDouble(POSITION_TP)) + "\r\n"; positionProperties += "Comment: " + PositionGetString(POSITION_COMMENT) + "\r\n"; positionProperties += "Magic Number: " + (class="type">class="kw">string)PositionGetInteger(POSITION_MAGIC) + "\r\n"; positionProperties += "_______________________________________________________________________________________"; Print(positionProperties); class=class="str">"cmt">//-- reset the the tradeRequest and tradeResult values by zeroing them ZeroMemory(tradeRequest); ZeroMemory(tradeResult); class=class="str">"cmt">//-- initialize the trade reqiest parameters to close the position tradeRequest.action = TRADE_ACTION_DEAL; class=class="str">"cmt">//-- Trade operation type for closing a position tradeRequest.position = positionTicket; tradeRequest.symbol = positionSymbol; tradeRequest.volume = positionVolume; tradeRequest.deviation = SymbolInfoInteger(positionSymbol, SYMBOL_SPREAD) * class="num">2; class=class="str">"cmt">//--- Set the price and order type of the position being closed if(positionType == POSITION_TYPE_BUY) { tradeRequest.price = SymbolInfoDouble(positionSymbol, SYMBOL_BID); tradeRequest.type = ORDER_TYPE_SELL; } elseclass=class="str">"cmt">//--- For sell type positions { tradeRequest.price = SymbolInfoDouble(positionSymbol, SYMBOL_ASK); tradeRequest.type = ORDER_TYPE_BUY; }
◍ 平仓请求的重复重试机制
在 MT5 里发平仓单,网络抖动或报价瞬变都可能让 OrderSend 返回失败。与其一次失败就放弃,更稳妥的做法是在函数开头先 ResetLastError(),把上一次运行残留的错误码清掉,否则后面取 GetLastError() 会拿到脏数据。 下面这段逻辑用 for 循环最多跑 101 次(loop 从 0 到 100)去试关仓。只要某次 OrderSend 成功且 tradeResult.retcode 等于 10008(TRADE_RETCODE_DONE)或 10009(TRADE_RETCODE_DONE_PARTIAL),就打印成功信息并直接 return(true) 退出。 如果发送失败,则进 else 分支调用 ErrorAdvisor 做错误分类。若 ErrorAdvisor 返回 false(不可恢复错误)或 IsStopped() 为真(脚本被用户终止),立刻打印错误并 return(false)。否则循环继续,等下一轮再试。 外汇与贵金属交易存在高风险,这种重试只能缓解瞬时故障,无法消除滑点或报价跳空带来的成交偏差,实际效果请在策略测试器或模拟盘验证。
ResetLastError(); class=class="str">"cmt">//--- reset error cache so that we get an accurate runtime error code in the ErrorAdvisor function for(class="type">int loop = class="num">0; loop <= class="num">100; loop++) class=class="str">"cmt">//-- try closing the position class="num">101 times untill the request is successful { class=class="str">"cmt">//--- send order to the trade server if(OrderSend(tradeRequest, tradeResult)) { class=class="str">"cmt">//-- Confirm order execution if(tradeResult.retcode == class="num">10008 || tradeResult.retcode == class="num">10009) { Print(__FUNCTION__, "_________________________________________________________________________"); PrintFormat("Successfully closed position #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType)); PrintFormat("retcode=%u runtime_code=%u", tradeResult.retcode, GetLastError()); Print("_______________________________________________________________________________________"); class="kw">return(true); class=class="str">"cmt">//-- exit function break; class=class="str">"cmt">//--- success - order placed ok. exit for loop } } else class=class="str">"cmt">//-- position closing request failed { class=class="str">"cmt">//-- order not sent or critical error found if(!ErrorAdvisor(__FUNCTION__, positionSymbol, tradeResult.retcode) || IsStopped()) { Print(__FUNCTION__, "_________________________________________________________________________"); PrintFormat("ERROR closing position #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType)); Print("_______________________________________________________________________________________"); class="kw">return(false); class=class="str">"cmt">//-- exit function break; class=class="str">"cmt">//-- exit for loop } } } class="kw">return(false); }
把这条线请下神坛
PositionsManager.mq5 这个 31.46 KB 的源码包已经把前面几节攒下的仓位管理函数都收进去了,在 MT5 里直接编译就能挂到自己的 EA 里跑。 别把它当成什么圣杯级框架,它只是把开平仓、查持仓这些脏活封装成了 ex5 库,真要稳还得你自己接风控和品种波动过滤。 下一篇若去扩这个库,建议先拿黄金 5 分钟线试调用,外汇和贵金属杠杆高、滑点跳空频繁,参数没调顺之前小仓位验证最实际。