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

MQL4(32):订单管理和统计

在开发 EA 时,经常需要知道当前由本EA管理的、符合特定条件的订单数量。例如,统计总共有多少订单、有多少买单、多少卖单、多少挂单等。这对于执行某些策略逻辑(如限制最大持仓数、判断是否已有同向订单)或进行信息展示都非常有用。我们可以创建一系列订单管理函数来实现这些功能。

统计所有符合条件的订单总数 (CountTotalOrders)

这个函数用于统计当前订单池中,由指定魔术数字 (argMagicNumber) 放置在指定交易品种 (argSymbol) 上的所有类型(包括持仓和挂单)的订单总数。

/**
 * @brief 计算符合指定品种和魔术数字的当前活动订单总数 (含挂单)。
 * @param argSymbol       要统计的交易品种名称 (e.g., Symbol())。
 * @param argMagicNumber  要统计的魔术数字。
 * @return int            符合条件的订单数量。
 */
int CountTotalOrders(string argSymbol, int argMagicNumber)
{
    int orderCount = 0; // 1. 初始化计数器为 0
    int totalOrdersInPool = OrdersTotal(); // 2. 获取当前订单池中的总订单数
    // 3. 遍历订单池 (从 0 到 总数-1)
    // 注意:如果后续需要在循环中删除订单,应从后往前遍历
    for (int i = totalOrdersInPool - 1; i >= 0; i--)
    {
        // 4. 按位置索引选择订单
        if (OrderSelect(i, SELECT_BY_POS) == true) // 确保成功选中
        {
            // 5. 核心过滤条件:检查订单的品种和魔术数字是否匹配
            if (OrderSymbol() == argSymbol && OrderMagicNumber() == argMagicNumber)
            {
                orderCount++; // 6. 如果匹配,计数器加 1
            }
        }
    }
    // 7. 返回最终统计的数量
    return(orderCount);
}
  • 函数解释:
    • 函数接收 argSymbolargMagicNumber 作为过滤条件。
    • 初始化计数器 orderCount 为 0。
    • 使用 for 循环遍历订单池(从索引 0 到 OrdersTotal() - 1)。
    • 在循环内部,使用 OrderSelect(i, SELECT_BY_POS) 选中每个订单。
    • 核心过滤: 通过 if (OrderSymbol() == argSymbol && OrderMagicNumber() == argMagicNumber) 来判断当前选中的订单是否是我们关心的(由本 EA 在指定品种上下的单)。
      • 为何需要过滤? 因为 OrdersTotal() 返回的是所有活动订单,可能包含手动交易、其他 EA 的订单。必须通过交易品种和魔术数字进行双重过滤,才能准确统计属于当前 EA 实例在目标品种上的订单。
      • 魔术数字的重要性: 强烈建议为您的每个 EA(或者同一 EA 在不同图表/策略设置下)使用唯一的魔术数字,这是区分和管理各自订单的基础。避免在同一账户的同一品种上运行使用相同魔术数字的多个 EA。
    • 如果订单符合过滤条件,计数器 orderCount 加 1。
    • 循环结束后,返回 orderCount 的最终值。

使用示例:

extern int MagicNumber = 123; // EA 的魔术数字
extern bool CloseAllSwitch = false; // 假设有一个外部开关控制是否平仓

// ... 在 start() 函数中 ...
int myEaOrderCount = CountTotalOrders(Symbol(), MagicNumber); // 调用函数获取本 EA 订单数
Print("当前由本 EA 管理的订单总数: ", myEaOrderCount);

if (myEaOrderCount > 0 && CloseAllSwitch == true) // 如果有订单且开关打开
{
    // ... 在这里执行批量平仓所有属于本 EA 订单的逻辑 ...
    // 例如,再次循环并调用 CloseBuyOrder / CloseSellOrder 函数
}

统计特定类型的订单数量 (以买单为例: CountBuyMarketOrders)

如果我们只想统计特定类型的订单(例如,只统计买入市价单),可以在 CountTotalOrders 函数的基础上,增加一个对 OrderType() 的检查。

/**
 * @brief 计算符合指定品种和魔术数字的当前【买入市价单】(OP_BUY) 的数量。
 * @param argSymbol       要统计的交易品种名称。
 * @param argMagicNumber  要统计的魔术数字。
 * @return int            符合条件的买入市价单数量。
 */
int CountBuyMarketOrders(string argSymbol, int argMagicNumber)
{
    int orderCount = 0; // 初始化
    int totalOrdersInPool = OrdersTotal();

    for (int i = totalOrdersInPool - 1; i >= 0; i--)
    {
        if (OrderSelect(i, SELECT_BY_POS) == true)
        {
            // 过滤条件:品种、魔术数字,【并且】订单类型必须是 OP_BUY
            if (OrderSymbol() == argSymbol &&
                OrderMagicNumber() == argMagicNumber &&
                OrderType() == OP_BUY) // 增加了对订单类型的判断
            {
                orderCount++;
            }
        }
    }
    return(orderCount);
}
  • 主要变化: 在 if 判断条件中,增加了 && OrderType() == OP_BUYOP_BUY 是 MQL 中代表买入市价单的预定义常量。
  • 扩展: 您可以通过类似的方式创建统计其他类型订单的函数:
    • 统计卖出市价单:将 OP_BUY 改为 OP_SELL,函数名改为 CountSellMarketOrders
    • 统计所有买单 (含挂单):检查 OrderType() == OP_BUY || OrderType() == OP_BUYSTOP || OrderType() == OP_BUYLIMIT
    • 统计特定挂单类型:例如,只统计卖出限价单,使用 OrderType() == OP_SELLLIMIT
    • …依此类推。

建议根据您的EA策略需求,创建一套完整的订单计数函数,并将它们放入您的包含文件 (.mqh) 中,方便调用。

通常情况下,我们可能需要一次性关闭满足特定条件(例如,同一品种、同一魔术数字、同一类型)的所有订单,而不是只关闭单个订单。这可以通过组合订单遍历循环和订单关闭/删除逻辑来实现。

关闭所有符合条件的市价买单 (CloseAllBuyOrders)

这个函数演示了如何遍历订单池,并关闭所有由本 EA(通过 argMagicNumber 识别)在指定品种 (argSymbol) 上开设的市价买单 (OP_BUY)

/**
 * @brief 关闭所有符合指定品种和魔术数字的【市价买单】。
 * @param argSymbol         要操作的交易品种名称。
 * @param argMagicNumber    要操作的订单的魔术数字。
 * @param argSlippagePoints 允许的滑点 (points)。
 */
void CloseAllBuyOrders(string argSymbol, int argMagicNumber, int argSlippagePoints)
{
    int total = OrdersTotal(); // 在循环开始前获取一次总数
    for (int i = total-1; i>=0 ; i--)
    {
        // 按位置选择订单
        if (OrderSelect(i, SELECT_BY_POS) == true)
        {
            // 检查是否是目标订单:品种、魔术数字、类型为 OP_BUY
            if (OrderSymbol() == argSymbol &&
                OrderMagicNumber() == argMagicNumber &&
                OrderType() == OP_BUY)
            {
                // --- 准备并执行平仓 ---
                int ticketToClose = OrderTicket();
                double lotsToClose = OrderLots();

                if (!WaitForTradeContext()) { // 等待上下文 (假设已封装)
                   Print("等待交易上下文超时,跳过订单 #", ticketToClose);
                   continue; // 跳过本次循环,处理下一个
                }
                RefreshRates();
                double closePrice = Bid; // 平买单用 Bid 价

                bool closed = OrderClose(ticketToClose, lotsToClose, closePrice, argSlippagePoints, Red);

                // --- 处理平仓结果 ---
                if (!closed) { // 如果平仓失败
                    HandleTradeError(StringFormat("CloseAllBuyOrders 平仓 #%d", ticketToClose), GetLastError()); // 调用错误处理函数
                    // 失败时不改变 i 或 total,允许下次循环可能重试或被跳过(取决于错误类型)
                } else { // 如果平仓成功
                    Print("成功发送平仓指令 for 买单 #", ticketToClose);
                }
            } // end if 匹配订单
        } // end if OrderSelect 成功
    } // end for
} // end CloseAllBuyOrders
  • 函数签名: 返回类型为 void,表示该函数执行一个操作,不返回具体计算结果。

  • 遍历与筛选: 使用 for 循环遍历订单池,通过 OrderSelect 选中订单后,使用 OrderSymbol(), OrderMagicNumber(), OrderType() 筛选出目标市价买单。

  • 平仓操作: 对符合条件的订单,执行与 CloseBuyOrder 函数内部类似的平仓逻辑(等待上下文、刷新价格、获取手数和 Bid 价、调用 OrderClose)。

  • 错误处理: 对 OrderClose 的失败情况进行处理(建议封装)。

  • i-- 与索引重排: 这是理解此段代码的关键。如前所述,当一个订单被关闭后,MQL 会自动重新排列订单池索引。如果在从前往后遍历时关闭了订单 i,那么原来在 i+1 的订单会移动到 i。如果在成功平仓后不执行i--,循环末尾的 i++ 会导致下一次迭代从新的 i+1 开始,从而跳过了那个刚移动到 i 位置的订单。执行 i-- 正好抵消了 i++,使得下一次迭代仍然从当前的索引i 开始,确保所有订单都被检查到。同时,因为订单总数减少了,也需要 total-- 来配合循环条件 i < total

  • FIFO 规则与遍历方向: 原文解释称,采用这种从索引 0 开始(从最旧到最新)的遍历方式是为了遵守美国的 FIFO(先进先出) 规则,该规则要求同一交易品种的订单必须按开仓时间顺序进行平仓。

    • 重要提示: 虽然“从前往后 + i--”可以实现 FIFO 平仓并处理索引问题,但它逻辑上更复杂且可能有潜在的边界问题。在不严格要求代码层面实现 FIFO(例如,FIFO 由经纪商强制执行)或者可以接受非 FIFO 平仓的情况下,强烈建议使用从后往前遍历 (for (int i = OrdersTotal() - 1; i >= 0; i--)) 的方式来进行批量平仓或删除,这种方式代码更简洁、逻辑更清晰、不易出错,因为它天然避免了索引重排的影响。
  • 关闭卖单: 关闭所有市价卖单的函数 (CloseAllSellOrders) 逻辑完全相同,只需将类型检查改为 OP_SELL,平仓价格改为 Ask 即可。完整代码见原文附录 D。

删除所有符合条件的挂单 (CloseAllBuyStopOrders)

这个函数演示了如何关闭(在本例中是删除)所有符合条件的买入止损挂单 (OP_BUYSTOP)。

/**
 * @brief 关闭(删除)所有符合指定品种和魔术数字的【买入止损挂单】。
 * @param argSymbol         交易品种名称。
 * @param argMagicNumber    魔术数字。
 * @param argSlippagePoints (此参数对于 OrderDelete 无效,可移除)
 */
void CloseAllBuyStopOrders(string argSymbol, int argMagicNumber, int argSlippagePoints) // argSlippagePoints 未使用
{
    // 同样,从后往前遍历更优,此处遵循原文逻辑展示:
    int total = OrdersTotal();
    for (int i = total-1; i>=0; i--)
    {
        if (OrderSelect(i, SELECT_BY_POS) == true)
        {
            // 检查品种、魔术数字和类型 (OP_BUYSTOP)
            if (OrderSymbol() == argSymbol &&
                OrderMagicNumber() == argMagicNumber &&
                OrderType() == OP_BUYSTOP)
            {
                // --- 准备并执行删除 ---
                int ticketToDelete = OrderTicket();
                if (!WaitForTradeContext()) continue;

                bool deleted = OrderDelete(ticketToDelete, Red); // 调用 OrderDelete 删除

                // --- 处理删除结果 ---
                if (!deleted) { // 删除失败
                    HandleTradeError(StringFormat("CloseAllBuyStopOrders 删除 #%d", ticketToDelete), GetLastError());
                } else { // 删除成功
                    Print("成功发送删除指令 for 挂单 #", ticketToDelete);
                }
            } // end if 匹配订单
        } // end if OrderSelect 成功
    } // end for
} // end CloseAllBuyStopOrders
  • 主要区别:
    • 筛选条件中的订单类型检查变为 OrderType() == OP_BUYSTOP
    • 使用 OrderDelete() 函数来删除挂单。OrderDelete 不需要价格、手数和滑点参数。因此,函数签名中的 argSlippagePoints 参数实际上是未使用的。
    • 错误处理针对 OrderDelete 的失败情况。
    • 成功删除后,同样需要执行 i--total-- 来处理索引重排。
  • 通用性: 这段代码的逻辑可以轻松修改,用于删除任何类型的挂单(OP_SELLSTOP, OP_BUYLIMIT, OP_SELLLIMIT),只需更改 if 条件中检查的 OrderType 常量即可。

通过封装这些批量操作函数,可以极大地简化 EA 中需要对多个订单进行统一处理时的代码逻辑。

赞(0)
未经允许不得转载:图道交易 » MQL4(32):订单管理和统计
分享到