在开发 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);
}
- 函数解释:
- 函数接收
argSymbol
和argMagicNumber
作为过滤条件。 - 初始化计数器
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_BUY
。OP_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 中需要对多个订单进行统一处理时的代码逻辑。