保持敬畏之心
交易是一场持久战

​MQL4(21):EA交易环境与并发控制

MetaTrader平台为所有运行在上面的EA提供了一个单一的交易执行线程。这意味着,在同一时刻,只有一个EA能够成功地执行交易相关的操作(如下单、平仓、修改订单等),无论您在终端中同时运行了多少个 EA 实例。这种机制是为了保证订单处理的顺序性和一致性。

因此,在您的 EA 代码中,每次尝试调用任何交易函数(如 OrderSend(), OrderClose(), OrderDelete(), OrderModify())之前,都必须先检查当前的交易环境是否正被其他EA操作占用。

MQL4提供了 IsTradeContextBusy() 函数来完成这个检查:

  • IsTradeContextBusy() 返回 true: 表示交易线程当前正忙,您的EA此时不能执行交易操作,需要等待。
  • IsTradeContextBusy() 返回 false: 表示交易线程当前空闲,您的 EA 可以尝试执行交易操作。

推荐的处理方式: 使用一个 while 循环结合 Sleep() 来等待交易环境变为可用:

// --- 在执行任何交易操作前检查并等待交易 ---
int waitCounter = 0; // (可选) 增加一个等待计数器,避免无限等待
int maxWaitLoops = 500; // (可选) 例如最多等待 500 * 10ms = 5 秒

while (IsTradeContextBusy()) // 循环检查交易是否繁忙
{
    if (waitCounter >= maxWaitLoops) { // (可选) 如果等待时间过长,则放弃本次操作
        Print("等待交易超时!");
        return; // 或进行其他错误处理,例如返回特定错误码
    }
    Sleep(10); // 短暂休眠 10 毫秒,将CPU让给其他任务,避免空转
    waitCounter++; // (可选) 更新等待计数
}

// --- 跳出循环,表示交易环境现在空闲 ---
RefreshRates(); // (强烈推荐) 在执行交易前刷新价格变量
// 现在可以安全地调用交易函数了
int ticket = OrderSend(Symbol(), OP_BUY, LotSize, Ask, g_slippagePoints, /*...*/);
// ... 后续操作 ...

  • while(IsTradeContextBusy()): 只要交易环境繁忙,循环就持续进行。
  • Sleep(10): 在循环中短暂休眠,是 MQL 中处理等待的标准做法,可以有效降低 CPU 占用率,并给其他程序(包括释放交易上下文的程序)运行的机会。
  • 后果: 如果您的 EA 在未检查或检查后交易又变为繁忙的情况下,强行调用交易函数,该函数会调用失败,并通常导致错误代码 146 (ERR_TRADE_CONTEXT_BUSY)。
  • 局限性: 尽管上述方法是标准实践,但在极高并发(多个 EA 同时抢占资源)的情况下,它并不能完全保证 100% 避免冲突。更高级的错误处理可能涉及在捕获到 146 错误后进行有限次数的重试。本书后续章节会探讨更复杂的错误处理和重试机制。

刷新预定义变量

MQL4提供了一些非常方便的预定义变量来直接访问当前的市场状态,例如 Bid (当前买价)、Ask (当前卖价)、Time[0] (当前 K 线开盘时间)、Volume[0] (当前 K 线成交量) 等。

需要特别注意的是:这些预定义变量的值,是由平台在每个新的报价 (tick) 到达,并触发 EA 的OnTick() 函数开始执行时,自动更新一次的。

这意味着,在 OnTick() 函数的执行过程中,如果您代码运行需要一定时间,或者中间调用了可能产生延迟的操作(如 Sleep 或交易函数),那么当您稍后再次读取 BidAsk 时,它们的值可能已经不再是最新的市场报价了。在高波动市场或考虑交易服务器延迟的情况下,这种滞后尤为关键。

为了确保您的决策和操作始终基于最新的市场数据,MQL4提供了 RefreshRates() 函数。

RefreshRates() 函数:

  • 作用: 调用此函数会强制 MQL4客户端立即向服务器请求最新的行情数据,并用这些最新数据更新所有相关的预定义变量,如 Bid, Ask, Time 等。

    • 在调用任何交易函数 (OrderSend, OrderClose 等) 之前,因为这些函数需要准确的价格和滑点计算。
    • 在执行了任何可能耗时较长或导致程序暂停的操作(如 Sleep, 文件读写,网络请求等)之后。

      何时调用: 强烈建议在您的代码中,每次需要使用 Bid, Ask 或其他可能变化的预定义变量之前,特别是:

  • // 示例:下单前确保价格最新
    RefreshRates(); // 获取最新报价
    double currentBid = Bid;
    double currentAsk = Ask;
    // 使用 currentBid, currentAsk 进行计算或下单...
    OrderSend(..., currentAsk, ...); // 使用刷新后的 Ask 价
    
  • MarketInfo() 的关系: 如果您选择使用 MarketInfo() 函数(例如 MarketInfo(Symbol(), MODE_BID))来获取价格信息,那么不需要在调用 MarketInfo() 之前调用 RefreshRates()。因为 MarketInfo() 函数本身的设计就是去直接查询当前的实时市场数据。我们将在后续章节讨论自定义函数时,更多地采用 MarketInfo() 来获取价格,这样可以减少对预定义变量状态的依赖。

  • 方便性考量: 尽管 MarketInfo() 更为直接,但在 OnTick() 函数的主体逻辑中,直接使用预定义变量 BidAsk 仍然不失为一种简洁、方便地引用当前图表价格的方式,前提是您能养成在使用它们之前习惯性地调用RefreshRates() 的良好编程习惯。

错误处理

在执行下单 (OrderSend)、修改订单 (OrderModify) 或平仓 (OrderClose/OrderDelete) 等交易操作时,可能会因为各种原因遇到执行失败的情况,例如:

  • 交易参数无效(如价格、手数、SL/TP 不符合规则)
  • 市场重新报价
  • 交易服务器问题或网络连接中断
  • 保证金不足
  • 交易环境繁忙

尽管我们在之前的步骤中已经尽力验证了参数的有效性以避免常见错误,但总有些意外情况可能发生。因此,在 MQL4编程中,实现健壮的错误处理机制至关重要。当交易操作失败时,我们应该能够:

  1. 检测到错误的发生。
  2. 获取具体的错误信息。
  3. (可选) 提醒用户发生了错误。
  4. 记录详细的相关信息到日志,以便后续分析和排查问题。

检测错误: 我们通过检查交易函数的返回值来判断操作是否成功:

  • OrderSend(): 如果失败,返回 -1。成功则返回订单号 (正整数)。
  • OrderModify(), OrderClose(), OrderDelete(): 如果失败,返回 false。成功则返回 true

错误处理流程 (以 OrderSend 失败为例): 下面是一个当 OrderSend() 返回 -1 时,执行错误处理的基本流程和代码示例:

  1. 获取错误代码: 使用内置函数 GetLastError() 获取导致失败的具体错误代码(一个整数)。 极其重要: GetLastError() 在被调用一次后,其内部存储的错误代码会被自动清除(重置为 0)。因此,必须在检测到函数失败后立即调用 GetLastError() 并将其返回值存储到一个变量中,否则错误信息会丢失。通常建议为此定义一个全局变量。

    // 建议在文件顶部定义全局变量
    int g_lastErrorCode = 0; // 用于存储最近一次的错误代码
    
  2. 获取错误描述: 为了便于理解错误代码的含义,可以使用标准库 stdlib.mqh 中提供的 ErrorDescription() 函数,将错误代码转换为对应的文本描述。

    • 前提: 必须在源文件开头包含 stdlib.mqh 文件 (#include <stdlib.mqh>)。
    • 注意: ErrorDescription() 返回的描述通常比较简略(例如只返回错误代码数字),但标准库中可能还有其他更详细的错误描述函数或自定义实现。

     

    #include <stdlib.mqh> // 必须包含此文件才能使用 ErrorDescription()
    
  3. 提醒用户: 使用内置的 Alert() 函数可以在 MetaTrader 终端弹出一个包含错误信息的警告窗口。这对于需要用户立即关注的严重错误很有用。Alert() 的内容也会自动记录到“专家”日志中。

    • 使用 StringConcatenate()StringFormat() 函数构建包含错误码、描述以及当前操作上下文的提示信息字符串。
  4. 记录详细日志: 使用 Print() 函数将更详细的信息输出到“专家”日志 (Experts Log)。这些信息应足够详细,以便开发者或用户在事后分析问题原因。建议至少包含:

    • 错误代码和描述。
    • 尝试执行的操作类型(如下单、平仓等)。
    • 相关的市场价格(如 Bid, Ask)。
    • 尝试使用的交易参数(如手数、目标价格、SL/TP 等)。

代码示例 (处理 OrderSend 失败):

// --- 文件顶部 ---
#include <stdlib.mqh> // 包含标准库以使用 ErrorDescription()

// --- 全局变量 ---
int g_lastErrorCode = 0; // 用于存储最近一次的错误代码

// --- 在 start() 函数或相关交易函数中 ---
// 假设 LotSize, PendingPrice, g_slippagePoints, MagicNumber 已定义
int ticket = OrderSend(Symbol(), OP_BUYSTOP, LotSize, PendingPrice, g_slippagePoints, 0, 0,
                      "Buy Stop Order", MagicNumber, 0, Green);

// 检查 OrderSend 是否失败 (返回 -1)
if (ticket == -1)
{
    // ====> 错误处理代码块开始 <====
    // 1. 立刻获取并保存错误代码
    g_lastErrorCode = GetLastError();

    // 2. 获取错误描述
    string errorDescription = ErrorDescription(g_lastErrorCode);

    // 3. 构造 Alert 弹窗信息 (使用 StringFormat 更灵活)
    string alertMessage = StringFormat(
                              "下单失败 (Buy Stop): Error %d: %s",
                              g_lastErrorCode, // 显示错误码
                              errorDescription // 显示错误描述
                          );
    // 4. 显示 Alert 弹窗 (信息也会进日志)
    Alert(alertMessage);

    // 5. 构造更详细的 Print 日志信息 (包含价格和参数)
    RefreshRates(); // (可选) 刷新价格以记录当前状态
    string logMessage = StringFormat(
                            "OrderSend(Buy Stop) Failed. Error %d: %s. Details: Symbol=%s, Lots=%.2f, Price=%.5f, SL=%.5f, TP=%.5f, Magic=%d. Market: Bid=%.5f, Ask=%.5f",
                            g_lastErrorCode,
                            errorDescription,
                            Symbol(),
                            LotSize,
                            PendingPrice, // 记录尝试使用的挂单价
                            0.0, // 记录尝试使用的 SL
                            0.0, // 记录尝试使用的 TP
                            MagicNumber,
                            Bid,
                            Ask
                        );
    // 6. 打印详细日志到 "专家" 标签页
    Print(logMessage);

    // 7. (可选) 根据错误类型决定后续操作,例如:
    // if (g_lastErrorCode == ERR_TRADE_CONTEXT_BUSY) { /* 稍后重试? */ }
    // return(0); // 终止当前 tick 的执行

    // ====> 错误处理代码块结束 <====
}
else
{
    // 下单指令发送成功
    Print("成功发送订单 #", ticket);
    // ... 可以进行后续操作,例如使用 OrderModify 添加 SL/TP ...
}

关于字符串拼接:

  • StringConcatenate(参数1, 参数2, ...): MQL4内置函数,用于将多个参数(可以是字符串、数字等)按顺序拼接成一个字符串。参数间用逗号分隔。对于拼接多个或包含非字符串变量的情况,代码通常更简洁。
  • + 运算符: 也可以用加号 + 来连接字符串。但如果操作数包含数字类型(如 double 型的 Ask),必须先使用 DoubleToStr() 等转换函数将其显式转换为字符串后才能进行拼接,否则可能导致编译错误或意外结果。

     

    // 使用 + 拼接,注意类型转换
    string askStr = DoubleToStr(Ask, Digits); // Ask 是 double, 需转为 string
    string message = "当前 Ask 价格是: " + askStr;
    

Alert() Print() 的区别:

  • Alert(): 主要用于实时提醒用户。它会弹出一个包含指定信息的模态对话框(用户需要点击确定才能继续操作),并且其内容也会同时输出到“专家”日志。适用于需要用户立即知晓的关键错误或重要状态变化。
  • Print(): 主要用于记录详细信息供后续查阅。它将信息直接输出到 MetaTrader 终端窗口的**“专家”(Experts)** 标签页日志中。如果在策略测试器 (Strategy Tester) 中运行,则输出到测试器窗口的“日志”标签页。适用于记录程序的运行状态、调试信息、交易详情、错误细节等,不打断用户操作。

通过结合使用这些错误检测和处理机制,您可以大大提高 EA 的健壮性、可靠性和可维护性。

以下是日志的内容。第一条记录来自Alert()函数,提示尝试开立买入止损单时发生错误131:无效的交易量。第二条记录来自Print()函数,显示出价为1.5046,要价为1.5048,但手数为0。这表明问题在于设定的手数无效。

16:47:54 Profit Buster EURUSD,H1: 警报:尝试开立买入止损单时出现错误131:无效的交易量
16:47:54 Profit Buster EURUSD,H1: 出价:1.5046,要价:1.5048,手数:0

对于这种情况,我们同样可以为OrderModify()和OrderClose()等其他函数编写类似的错误处理逻辑。此外,还能设计更为细致的错误处理机制,依据不同的错误代码给出具体的错误信息或者采取相应的措施。例如,如果遇到错误代码130——表示“无效的止损价位”,则可以显示“止损或止盈价位设置不正确”的消息。下面是一个示例代码,展示了如何实现这种功能:

// 获取最近一次发生的错误代码
ErrorCode = GetLastError();
// 定义一个字符串变量用于存储错误描述
string ErrDesc;

// 根据错误代码设置对应的错误描述
if(ErrorCode == 129) ErrDesc = "订单开仓价位无效!";
if(ErrorCode == 130) ErrDesc = "止损或止盈价位无效!";
if(ErrorCode == 131) ErrDesc = "手数设定无效!";

// 组合错误信息并弹出警告框
string ErrAlert = StringConcatenate("尝试开立买单时遇到错误",ErrorCode,":",ErrDesc);
Alert(ErrAlert); // 显示错误信息

 

赞(0)
未经允许不得转载:图道交易 » ​MQL4(21):EA交易环境与并发控制
分享到