在MQL4中为市价单设置止损(Stop Loss) 和止盈 (Take Profit) ,传统方法是直接在调用 OrderSend()
函数时就将 SL/TP 价格作为参数传入。虽然这种方式对于大多数经纪商运行良好,但是一些账户不支持在提交市价单 (OP_BUY
/OP_SELL
) 的同时设置止损和止盈。如果在这些平台上强行这样做,OrderSend()
可能会失败并返回错误。
因此,为了确保EA具有更好的兼容性,特别是能够适应各种环境,推荐采用两步法来为市价单设置 SL/TP:
- 先调用
OrderSend()
发送一个不带SL/TP(即将 SL/TP 参数设为 0)的市价单。 - 在确认订单成功开立后,再调用
OrderModify()
函数来添加或修改该订单的SL 和TP。
注意:这个问题通常仅限于市价单。对于挂单通常可以在调用 OrderSend()
时就直接设置好止损和止盈价格。
OrderModify() – 订单修改函数
在订单成功提交(无论是市价单还是挂单)之后,您可以使用 OrderModify()
函数来更改该订单的某些属性,主要包括:
- 止损价格
- 止盈价格
- 挂单的触发价格
- 挂单的到期时间
要使用 OrderModify()
,必须先知道要修改订单的订单号。
其函数语法如下:
bool OrderModify(
int ticket, // 要修改的订单的订单号
double price, // 【仅对挂单有效】新的挂单触发价格
double stoploss, // 新的止损价格
double takeprofit, // 新的止盈价格
datetime expiration, // 【仅对挂单有效】新的到期时间
color arrow_color=clrNONE // 可选,标记修改位置的箭头颜色
);
返回值 (bool
类型):
true
: 修改指令已成功发送。false
: 修改指令发送失败。调用GetLastError()
获取原因。
使用OrderModify()的注意事项:
- 订单状态: 必须确保您要修改的订单仍然是活动状态(未平仓的市价单或未触发/未过期的挂单)。不能修改已平仓或已取消的订单。
- 挂单价格修改: 只能修改尚未被触发的挂单的触发价格 (
price
参数)。修改后的价格同样需要满足与当前市价的最小距离要求 (MODE_STOPLEVEL
)。 - SL/TP 有效性: 新设置的止损和止盈价格必须是有效的。例如,买单的止损价必须低于当前 Ask 价(或挂单价),止盈价必须高于当前 Ask 价(或挂单价),并且都需要满足
MODE_STOPLEVEL
的最小距离要求。 - 保持未修改参数的原始值: 这是极其重要的一点。当您只想修改订单的某一个或几个属性(例如,只想添加止损),对于不打算修改的参数,您必须传入它们在订单中当前的值。这意味着在调用
OrderModify()
之前,您通常需要先用OrderSelect()
选中订单,然后调用相应的Order*()
函数(如OrderOpenPrice()
,OrderTakeProfit()
,OrderExpiration()
等)来获取这些未改变参数的当前值,并将它们作为参数传给OrderModify()
。 - 错误 1 (无结果): 如果您调用
OrderModify()
时,传入的所有参数值(Price, StopLoss, TakeProfit, Expiration)都与订单当前的实际值完全相同(即没有进行任何有效修改),函数会调用失败并返回错误代码 1 (“no result” 或 “无效参数”)。虽然这个错误本身通常无害,但它表明您的代码逻辑可能存在问题。可以有条件地调用OrderModify
,仅在计算出的新值与当前值不同时才执行修改。
为现有市价单添加止损和止盈
- 下单: 调用
OrderSend()
发送市价单,并将stoploss
和takeprofit
参数设置为 0。同时,获取返回的订单号并存入变量(例如buyTicket
)。 - 检查下单结果: 确认
buyTicket > 0
,表示订单已成功发送。 - 选中订单: 使用
OrderSelect(buyTicket, SELECT_BY_TICKET)
选中刚刚创建的订单。 - 获取开仓价: 调用
OrderOpenPrice()
获取该订单的实际开仓价格。 - 计算 SL/TP: 根据获取到的开仓价和用户设置的 SL/TP 点数 (pips) 以及
g_onePipValue
(之前计算好的代表1 pip的值),计算出实际的止损价和止盈价。 - 检查是否需要修改: 确认计算出的
newSL
或newTP
大于 0 (即用户确实设置了 SL/TP) 并且与订单当前的 SL/TP 值不同。 - 调用 OrderModify: 调用
OrderModify()
函数,传入buyTicket
,OrderOpenPrice()
(保持开仓价不变),计算出的newSL
和newTP
,以及 0 (保持到期时间不变)。
代码示例 (为买单添加 SL/TP):
// 假设 LotSize, Ask, g_slippagePoints, MagicNumber, StopLoss, TakeProfit, g_onePipValue 已定义且有效
// 1. 发送不带 SL/TP 的买单
int buyTicket = OrderSend(Symbol(), OP_BUY, LotSize, Ask, g_slippagePoints, 0, 0,
"Buy Order (ECN)", MagicNumber, 0, Green);
// 2. 检查下单是否成功发送
if (buyTicket > 0)
{
// 3. 尝试选中刚下的订单
if (OrderSelect(buyTicket, SELECT_BY_TICKET))
{
// 4. 获取实际开仓价和当前 SL/TP (应为 0)
double openPrice = OrderOpenPrice();
double currentSL = OrderStopLoss();
double currentTP = OrderTakeProfit();
// 5. 计算新的 SL/TP 价格
double newSL = 0;
double newTP = 0;
if (StopLoss > 0) newSL = NormalizeDouble(openPrice - (StopLoss * g_onePipValue), Digits);
if (TakeProfit > 0) newTP = NormalizeDouble(openPrice + (TakeProfit * g_onePipValue), Digits);
// 6. 检查是否有必要修改 (新值有效且与当前值不同)
bool needModify = false;
if (newSL > 0 && MathAbs(newSL - currentSL) > Point * 0.1) needModify = true; // 允许微小误差比较
if (newTP > 0 && MathAbs(newTP - currentTP) > Point * 0.1) needModify = true;
// 7. 如果需要修改,则调用 OrderModify
if (needModify)
{
// 准备参数:保持开仓价和到期时间不变
RefreshRates(); // (可选但推荐) 确保价格信息最新
bool modifyResult = OrderModify(buyTicket, OrderOpenPrice(), newSL, newTP, 0, clrNONE); // 不显示修改箭头
if (modifyResult)
{
Print("成功发送修改指令 (添加 SL/TP) for #", buyTicket);
}
else
{
Print("发送修改指令失败 for #", buyTicket, " Error:", GetLastError());
// 可能需要错误处理逻辑,例如稍后重试
}
} // end if (needModify)
}
else
{
Print("OrderSelect 失败 for ticket #", buyTicket, " Error:", GetLastError());
// 无法选中刚开的单?可能状态已改变,需要处理
}
}
else
{
Print("OrderSend 开买单失败, Error:", GetLastError());
}
结论与建议: 尽管为市价单设置 SL/TP 采用这种“先下单,后修改”的两步法会增加几行代码,但强烈推荐您在开发 EA 时使用此方法。主要原因确保您的 EA 在包括 ECN/STP 在内的各类经纪商平台上都能稳定运行。这种方法基于订单实际成交后的开仓价 (OrderOpenPrice()
) 来计算 SL/TP,避免了 OrderSend()
时可能因滑点导致 SL/TP 相对位置不准确的问题。
修改挂单价格
OrderModify()
函数不仅可以用来修改止损和止盈,还可以用来调整尚未被触发的挂单 (Pending Order) 的入场价格 (price
参数)。但请注意,一旦挂单被市场价格触及并成功执行(即“成交”或“filled”),它就转变成了市价单,其开仓价格就不能再被修改了。
假设我们已经计算出了一个新的、期望的挂单价格,并存储在变量 newPendingPrice
中,同时确保了这个新价格是有效的(例如,满足最小距离要求)。以下是如何使用 OrderModify()
来修改一个现有挂单的入场价格,同时保持其止损、止盈和到期时间不变:
// int ticketToModify = ...; // 要修改的挂单订单号
// double newPendingPrice = ...; // 计算好的新挂单价格
// 0. 定义变量
int ticket = ticketToModify;
double newPrice = NormalizeDouble(newPendingPrice, _Digits); // 规范化新价格
string sym = OrderSymbol(); // 获取订单的交易品种
int orderType = -1;
double currentAsk = SymbolInfoDouble(sym, SYMBOL_ASK);
double currentBid = SymbolInfoDouble(sym, SYMBOL_BID);
int slippage = 3; // 允许的滑点,对于挂单修改价格不是必须,但保持良好习惯
int stopLevel = (int)SymbolInfoInteger(sym, SYMBOL_TRADE_STOPS_LEVEL);
double point = SymbolInfoDouble(sym, SYMBOL_POINT);
// 1. 选中目标挂单
if (OrderSelect(ticket, SELECT_BY_TICKET))
{
orderType = OrderType();
// 2. 检查是否是未成交的挂单
if (OrderCloseTime() == 0 && (orderType >= OP_BUYSTOP && orderType <= OP_SELLLIMIT))
{
// 3. 检查新价格是否与当前价格确实不同
if (MathAbs(newPrice - NormalizeDouble(OrderOpenPrice(), _Digits)) > point * 0.1) // 允许微小误差比较
{
// 3.1 【重要】检查新价格是否满足 StopLevel 要求
bool priceOk = false;
RefreshRates(); // 尝试获取最新的服务器价格,以便进行更准确的StopLevel判断
currentAsk = SymbolInfoDouble(sym, SYMBOL_ASK); // 再次获取,因为RefreshRates是异步的
currentBid = SymbolInfoDouble(sym, SYMBOL_BID);
switch(orderType)
{
case OP_BUYLIMIT:
if (newPrice < currentBid - stopLevel * point) priceOk = true;
break;
case OP_BUYSTOP:
if (newPrice > currentAsk + stopLevel * point) priceOk = true;
break;
case OP_SELLLIMIT:
if (newPrice > currentAsk + stopLevel * point) priceOk = true;
break;
case OP_SELLSTOP:
if (newPrice < currentBid - stopLevel * point) priceOk = true;
break;
}
if (!priceOk)
{
Print("订单 #", ticket, " 新挂单价格 ", DoubleToString(newPrice, _Digits),
" 不满足StopLevel要求 (StopLevel: ", stopLevel, " points). ",
"Current Ask: ", DoubleToString(currentAsk, _Digits),
", Current Bid: ", DoubleToString(currentBid, _Digits));
}
else
{
// 4. 调用 OrderModify 修改价格
// 注意:需要传入当前的 SL, TP, Expiration 值以保持不变
// RefreshRates(); // 已经在上面为StopLevel检查调用过了,这里可以省略,服务器会用它自己的价格验证
bool result = OrderModify(
ticket, // 订单号
newPrice, // 新的挂单价格 (已规范化)
OrderStopLoss(), // 保持当前止损不变
OrderTakeProfit(), // 保持当前止盈不变
OrderExpiration(), // 保持当前到期时间不变
clrNONE // 不绘制修改箭头
);
// 5. 处理修改结果
if (result) {
Print("成功发送修改挂单价格指令 for #", ticket, " to ", DoubleToString(newPrice, _Digits));
} else {
Print("修改挂单价格失败 for #", ticket, ", Error: ", GetLastError());
// 可以进一步分析常见的错误,如130 (ERR_INVALID_STOPS), 1 (ERR_NO_RESULT), 129 (ERR_PRICE_CHANGED)
}
}
} else {
Print("新价格 ", DoubleToString(newPrice, _Digits), " 与当前价格 ", DoubleToString(OrderOpenPrice(),_Digits), " 几乎相同,无需修改 for #", ticket);
}
} else {
Print("订单 #", ticket, " 不是一个可修改的挂单 (Type: ", orderType, ", CloseTime: ", TimeToString(OrderCloseTime()), ").");
}
} else {
Print("无法选中订单 #", ticket, ", 错误代码: ", GetLastError());
}