在前面已经接触过 OrderSelect()
函数。本节我们将探讨如何结合使用 OrderSelect()
函数与 MQL 中的循环运算符(for
循环和 while
循环),来实现对订单池(即当前所有持仓订单和挂单的集合)的遍历操作,以便检索和管理订单信息。掌握这种遍历方法是实现许多高级订单管理功能的基础,例如批量平仓、追踪止损、统计持仓订单数量等等。
for
循环运算符
for
循环通常用于执行已知或预定次数的代码块重复。其基本结构包含三个部分,用分号隔开,并写在一对圆括号内:for (初始化表达式; 循环条件; 迭代表达式)
。
- 初始化表达式: 在循环开始前仅执行一次,通常用于声明并初始化一个循环计数器变量。
- 循环条件 : 每次循环迭代开始前进行判断。如果条件为
true
,则执行循环体{}
内的代码;如果为false
,则跳出for
循环,执行循环后面的代码。 - 迭代表达式: 在每次循环体执行完毕后执行一次,通常用于修改计数器变量的值(递增或递减),为下一次条件判断做准备。
// 这个 for 循环将执行 3 次
for (int counter = 1; counter <= 3; counter++) // 初始化 counter=1; 条件 counter<=3; 每次循环后 counter+1
{
// 这里是需要重复执行的代码块
Print("For 循环次数: ", counter);
}
// 循环结束后,程序继续执行这里的代码
int counter = 1
: 声明并初始化计数器counter
为 1。counter <= 3
: 循环条件。只要counter
小于或等于 3,就继续循环。counter++
: 每次循环体执行结束后,将counter
的值加 1。(counter--
表示减 1,counter = counter + 2
表示加 2,等等)。- 注意:
for
语句圆括号内的三个表达式之间用分号分隔,但第三个表达式(迭代表达式)后面没有分号。
while
循环运算符
while
循环提供了一种基于条件进行循环的更简单直接的方式。它尤其适用于不确定循环需要执行多少次,只知道当某个条件满足时就应继续循环的情况。其基本结构是 while (循环条件)
。
循环条件: 在每次循环迭代开始前进行判断。如果条件为 true
,则执行循环体 {}
内的代码;如果为 false
,则跳出 while
循环。
bool needToContinue = true; // 用一个布尔变量控制循环
int loopCounter = 0;
while (needToContinue == true) // 当 needToContinue 为 true 时,就一直循环
{
// 循环体代码
Print("While 循环执行中...");
loopCounter++;
// !!! 关键:循环体内必须包含能最终改变条件的逻辑 !!!
if (loopCounter >= 5) // 例如,当循环执行了 5 次后
{
needToContinue = false; // 将条件变量置为 false,下一次循环判断时就会终止
}
}
- 如果
while
循环的条件永远为true
(例如,在上面的例子中如果忘记了if (loopCounter >= 5)
这个判断和needToContinue = false;
这行代码),程序将陷入无限循环,这会导致 EA 停止响应或耗尽系统资源。因此,务必确保循环体内部存在能最终使循环条件变为false
的出口逻辑。
while
循环也可以用来模拟 for
循环的行为(虽然不太常见),只需在循环外部初始化计数器,并在循环体内部手动更新计数器即可:
int counter = 1; // 在循环外初始化计数器
while (counter <= 3) // 设置循环条件
{
// 循环体代码...
Print("While 模拟 For 循环次数: ", counter);
counter++; // 在循环体内部手动递增计数器
}
这段 while
循环代码的执行效果与前面介绍的 for
循环完全相同,也会执行三次。
当循环次数已知或容易计算时,通常使用 for
循环更方便、结构更清晰。当循环的持续依赖于某个动态变化的条件,而事先不确定具体会执行多少次时,使用 while
循环更符合逻辑。在下一节,我们将看到如何使用 for
循环来遍历订单池。
订单遍历循环
现在我们将学习如何使用 for
循环来遍历(即逐个访问)当前账户中所有未平仓的订单和挂单(统称为订单池)。这是进行批量订单管理(如批量平仓、修改止损、统计信息等)的基础。
以下是遍历订单池的标准代码结构:
// 使用 for 循环遍历所有当前订单 (包括持仓和挂单)
int totalOrders = OrdersTotal(); // 先获取当前订单总数
// 从第一个订单 (索引 0) 开始,到最后一个订单 (索引 totalOrders - 1) 结束
// 注意:循环条件是 i < totalOrders (等价于 i <= totalOrders - 1)
for (int i = 0; i < totalOrders; i++)
{
// 按位置索引 i 选择订单 (默认在 MODE_TRADES 池中选择)
if (OrderSelect(i, SELECT_BY_POS) == true) // 必须检查 OrderSelect 是否成功选中
{
// --- 订单成功选中 ---
// 在这里可以调用 Order*() 函数获取该订单的信息
// 例如:
// int ticket = OrderTicket();
// string symbol = OrderSymbol();
// int type = OrderType();
// ...
// 根据需要评估条件并处理该订单 (例如打印信息、判断是否关闭等)
// if (OrderSymbol() == "EURUSD" && OrderMagicNumber() == 12345) { ... }
}
// else { Print("OrderSelect 按位置 ", i, " 失败!"); } // 可选:处理选择失败的情况
}
// 循环结束,已遍历完当前时刻的所有订单
代码解释:
-
for (int i = 0; i < OrdersTotal(); i++)
:int i = 0
: 在循环开始前,声明并初始化一个整数计数器i
为 0。这个i
将用作订单在池中的位置索引 (position index)。i < OrdersTotal()
: 这是循环得以继续执行的条件。OrdersTotal()
是一个 MQL 内置函数,它返回当前活动订单池中订单的总数量。只要计数器i
的值严格小于订单总数,循环就会执行。i++
: 在每次循环体{}
内的代码执行完毕之后,计数器i
的值会自动加 1。
-
订单池与 0 索引:
- 订单池可以看作是一个包含当前所有活动订单(持仓单+挂单)的列表。
- 这个列表的索引是从 0 开始的。列表中的第一个订单(通常是下单时间最早的那个)索引为 0,第二个订单索引为 1,以此类推,最后一个(最新下的)订单索引为
OrdersTotal() - 1
。 - 理解循环条件: 如果
OrdersTotal()
返回 3(表示有 3 个订单),它们的索引分别是 0, 1, 2。循环需要覆盖这三个索引。当i
从 0 增加到 2 时,i < 3
条件都为真,循环执行。当i
增加到 3 时,3 < 3
条件变为假,循环终止。因此i < OrdersTotal()
或i <= OrdersTotal() - 1
都是正确的循环条件。
-
OrderSelect(i, SELECT_BY_POS)
:- 这是循环体内的关键操作。它尝试选中订单池中位置索引为
i
的那个订单。 - 第二个参数
SELECT_BY_POS
表明我们是根据位置而非订单号来选择。 - 返回值检查:
OrderSelect()
可能因各种原因(如索引越界,虽然在此循环中理论上不会发生)而失败并返回false
。因此,必须检查其返回值,只有在返回true
时,后续的Order*()
函数才能获取到正确的订单信息。
- 这是循环体内的关键操作。它尝试选中订单池中位置索引为
-
循环流程: 这个
for
循环会依次将i
设为 0, 1, 2, …, 直到OrdersTotal() - 1
,并在每次迭代中尝试选中对应位置的订单,让您有机会在循环体内对每个订单进行检查和处理。
关于订单关闭与索引序列
- 当未结订单池中的一个订单被关闭时,池中任何较新的订单(即索引比被关闭订单大的订单)的订单索引将被递减(自动向前移动填补空位)。
- 示例: 如果有订单 0, 1, 2。当订单 0 被关闭后,原来的订单 1 会变成新的索引 0,原来的订单 2 会变成新的索引 1。
- 潜在问题: 如果您在从 0 开始递增(从前往后)遍历的过程中,关闭或删除了某个订单(例如,在
i=1
时关闭了当前选中的订单),那么原本在i=2
位置的订单会立刻移动到i=1
的位置。但此时循环的下一次迭代i
会变成 2,您就会错过处理那个刚刚移动到i=1
位置的订单,导致漏单。 - 为了避免这个问题,当需要在循环内部执行关闭或删除订单的操作时,最安全可靠的做法是从后往前遍历订单池。即,循环应写为:
for (int i = OrdersTotal() - 1; i >= 0; i--) // 从最后一个订单往前遍历到索引 0 { if (OrderSelect(i, SELECT_BY_POS)) { // 在这里关闭或删除订单是安全的 // 因为即使删除了 i,也不会影响接下来要处理的 i-1, i-2 ... 这些订单的索引 } }
我们将在后续讨论批量平仓等场景时详细应用这种倒序遍历的方法。目前,只需理解递增遍历在内部删单时存在的问题即可。