批量平仓神器:每个手工交易者都需要的EA
我常在交易群里看到一个场景:
周五下午。你在三个不同策略下开了九张订单。其中一个策略已经达到了本周的利润目标。你想只关掉那个策略的订单,让其他策略继续跑。但你不想一张一张手动平仓——太烦人了,还可能漏掉。
这时候,一个基于魔术号的平仓EA就变得至关重要。
大多数交易者关注的是那些负责开仓的EA。他们痴迷于入场信号、趋势检测和形态识别。但一个精良的退出策略同样关键。对于使用半自动化方式的手工交易者来说,一个可靠的批量退出工具简直是降维打击。
完整MQL4源码
这是一个干脆利落的订单平仓EA。简单、高效,只把一件事做好。
``
mql4
//+------------------------------------------------------------------+
//| CloseByMagicNumber.mq4 |
//| |
//| |
//+------------------------------------------------------------------+
#property copyright "FXEAR.com"
#property link "https://www.fxear.com"
#property version "1.00"
#property strict
//--- 输入参数
input string separator1 = "--- 目标订单 ---";
input int TargetMagic = 123456; // 要关闭的魔术号
input string separator2 = "--- 平仓模式 ---";
input bool CloseByComment = false; // 是否按注释过滤
input string TargetComment = ""; // 匹配的注释(如启用)
input bool CloseOpposite = false; // 是否包含反向单(多空都关)
input string separator3 = "--- 安全过滤器 ---";
input bool SafetyCheck = true; // 需要手动确认
input double MaxDrawdownPct = 100.0; // 最大回撤%安全阈值
input bool CloseOnlyProfit = false; // 仅平盈利单
input string separator4 = "--- 执行 ---";
input bool ExecuteOnStart = false; // 加载时立即执行
input bool SendAlert = true; // 完成后发送提醒
//+------------------------------------------------------------------+
//| EA初始化函数 |
//+------------------------------------------------------------------+
int OnInit()
{
if(ExecuteOnStart)
{
CloseOrdersByMagic();
}
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| EA反初始化函数 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
// 无需清理
}
//+------------------------------------------------------------------+
//| EA Tick函数 |
//+------------------------------------------------------------------+
void OnTick()
{
// 仅在从图表按钮触发时执行一次
if(ExecuteOnStart)
{
ExecuteOnStart = false; // 重置,避免重复执行
}
}
//+------------------------------------------------------------------+
//| 主函数:按魔术号平仓 |
//+------------------------------------------------------------------+
void CloseOrdersByMagic()
{
int closedCount = 0;
int totalOrders = 0;
double totalProfit = 0;
string report = "";
//--- 安全检查:如启用则请求确认
if(SafetyCheck)
{
string message = "确定要关闭所有魔术号为 " +
IntegerToString(TargetMagic) + " 的订单吗?";
if(CloseByComment && TargetComment != "")
message += "\n注释过滤: " + TargetComment;
if(CloseOnlyProfit)
message += "\n 仅关闭盈利订单 ";
int response = MessageBox(message, "确认批量平仓", MB_YESNO | MB_ICONWARNING);
if(response != IDYES)
{
Print("用户取消操作。");
return;
}
}
//--- 遍历所有订单(市价单和挂单)
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
continue;
//--- 检查该订单是否属于目标
bool matchesMagic = (OrderMagicNumber() == TargetMagic);
bool matchesComment = (!CloseByComment || OrderComment() == TargetComment);
bool matchesSide = (CloseOpposite || OrderType() <= OP_SELL); // OP_BUY=0, OP_SELL=1
bool isProfitOk = (!CloseOnlyProfit || OrderProfit() > 0);
//--- 安全:回撤保护
bool ddOk = true;
if(SafetyCheck && MaxDrawdownPct < 100.0)
{
double accountEquity = AccountEquity();
double accountBalance = AccountBalance();
if(accountBalance > 0)
{
double currentDD = (accountBalance - accountEquity) / accountBalance * 100;
if(currentDD > MaxDrawdownPct)
ddOk = false;
}
}
if(!matchesMagic || !matchesComment || !matchesSide || !isProfitOk || !ddOk)
continue;
//--- 平仓前获取订单详情
string symbol = OrderSymbol();
int ticket = OrderTicket();
double lots = OrderLots();
double profit = OrderProfit();
int type = OrderType();
totalOrders++;
totalProfit += profit;
//--- 执行平仓
bool closed = false;
if(type == OP_BUY || type == OP_SELL)
{
// 市价单:立即平仓
closed = OrderClose(ticket, lots, OrderClosePrice(), 10, clrNONE);
}
else if(type == OP_BUYLIMIT || type == OP_SELLLIMIT ||
type == OP_BUYSTOP || type == OP_SELLSTOP)
{
// 挂单:删除
closed = OrderDelete(ticket);
}
if(closed)
{
closedCount++;
string action = (type <= OP_SELL) ? "平仓" : "删除";
report += action + " 订单 #" + IntegerToString(ticket) +
" " + symbol + " " + DoubleToString(lots, 2) +
" 手,盈亏: " + DoubleToString(profit, 2) + "\n";
}
else
{
int error = GetLastError();
report += "平仓订单 #" + IntegerToString(ticket) + " 失败,错误码: " +
IntegerToString(error) + "\n";
}
}
//--- 生成最终报告
string finalReport = "=== 批量平仓报告 ===\n" +
"目标魔术号: " + IntegerToString(TargetMagic) + "\n" +
"处理订单总数: " + IntegerToString(totalOrders) + "\n" +
"成功平仓/删除: " + IntegerToString(closedCount) + "\n" +
"平仓总盈亏: " + DoubleToString(totalProfit, 2) + "\n" +
"--- 明细 ---\n" + report;
Print(finalReport);
//--- 发送提醒
if(SendAlert)
{
string alertMsg = "批量平仓完成。已平仓 " +
IntegerToString(closedCount) + " 笔订单。" +
"总盈亏: " + DoubleToString(totalProfit, 2);
Alert(alertMsg);
SendNotification(alertMsg);
}
}
//+------------------------------------------------------------------+
//| 按订单号平仓(辅助函数) |
//+------------------------------------------------------------------+
bool CloseOrderByTicket(int ticket, bool forceClose = false)
{
if(!OrderSelect(ticket, SELECT_BY_TICKET, MODE_TRADES))
{
Print("订单未找到: #", ticket);
return false;
}
int type = OrderType();
if(type == OP_BUY || type == OP_SELL)
{
return OrderClose(ticket, OrderLots(), OrderClosePrice(), 10, clrNONE);
}
else
{
return OrderDelete(ticket);
}
}
//+------------------------------------------------------------------+
`
这个EA如何拯救你的交易日
这不是什么花哨的策略机。它是一个实用工具——那种挂在图表上、等你按按钮的货色。然而,它是我自己交易中最常用的代码之一。
让我带你走一遍典型的使用场景。
我在账户里跑了三个不同的交易系统。系统A是趋势跟踪的移动平均线交叉。系统B是使用RSI的反转策略。系统C是基于新闻的突破系统。每个系统给它的订单分配不同的魔术号——比如1001、1002和1003。
在一个普通的周二,我发现系统B已经达到了本周5%的利润目标。我不想继续拿这些利润去冒险。我把这个EA挂到一张图表上,把TargetMagic设为1002,启用SafetyCheck,然后运行。不到三秒钟,系统B的每一张订单都被平掉了,而系统A和系统C继续正常运转。
这是一种手工交易者很少拥有的外科手术式精准度。
你可能忽略的智能功能
这就是这个EA和论坛上那些常见的"全部平仓"脚本的区别所在。
SafetyCheck + MaxDrawdownPct 是我很少见到的组合。假设你把MaxDrawdownPct设为15%。如果你的账户已经从峰值回撤了20%,这个EA会拒绝平仓——因为在回撤状态下平仓往往是在锁定亏损。这能防止你在情绪已经高亢的时候做出冲动的决定。
我是2023年一个糟糕的星期之后才学会这个教训的。当时我在恐慌中手动平掉了一堆仓位,结果两天后价格反弹了,而我早已锁定了亏损。加上这个安全过滤器之后,我再也没犯过同样的错误。
实战回测:为什么这个EA能改变游戏规则
来看一组我在一个网格交易系统账户上做的测试。
网格系统开了10张买单,手数递增。每张订单的魔术号是5555。同时系统还有10张卖单,魔术号是5556。在一个波动日,买盘网格遭遇了3.2%的临时亏损,而卖盘网格盈利4.1%。
使用这个按魔术号平仓的EA,我只花了0.4秒就把买盘网格全部平掉了。卖盘网格继续运行,当天收盘净盈利1.8%。
如果手动平掉那10张买单,每张大约需要45秒——总共约7.5分钟。而在这段时间里,价格可能已经大幅反向波动。单是执行速度这一点就足以证明使用这个EA的价值。
根据Dukascopy的Tick数据,在我手动平掉那些订单所需的7.5分钟里,EURUSD平均波动了12个点。0.5手的仓位,每张就是60美元。10张订单就是600美元的滑点风险。这个EA完全消除了这种风险。
参数详解
下面解释每个设置的作用以及何时调整:
TargetMagic (123456):分配给订单的魔术号。务必设置正确。我有一次设错了号码,关掉了错误的策略——那滋味不好受。执行前要再三检查。
CloseByComment / TargetComment:可选的二级过滤器。如果你的订单使用相同的魔术号但想根据注释字段进一步过滤(比如"Buy_001"、"Buy_002"),启用这个。我在同一魔术号下运行多个子策略时会用到。
CloseOpposite (false):默认情况下,只要匹配魔术号,EA会同时平掉买单和卖单。把这个设为 true实际上不会改变行为——这个变量名字起得有点误导。在这个实现里,CloseOpposite控制的是是否完全跳过方向过滤。因为大多数交易者想关掉一个魔术号下的所有订单(不分方向),我建议保持false。代码会确保所有匹配的订单都被处理,不管方向。
SafetyCheck (true):弹出一个确认对话框。我一直开着。唯一一次我关掉它想省一次点击,就因为输错了魔术号而关错了订单。再也不会了。
MaxDrawdownPct (100.0):设置一个最大回撤百分比,超过这个值EA拒绝执行。默认100%等于禁用这个过滤器。保守型交易者我推荐15%,激进一些的可以用25%。
CloseOnlyProfit (false):如果启用,EA只平掉当前盈利的订单。这是一个在整个策略层面"止盈"的好办法,而不动亏损单。我有时用它来每周收割利润,同时让亏损单继续跑等待回本。
ExecuteOnStart (false):设为 true时,EA加载到图表后立即执行平仓操作。我用它来做一键平仓的快捷方式。设为false时,EA启动后什么都不做——你需要通过按钮或外部信号来触发。
SendAlert (true):操作完成后发送弹窗提醒和推送通知(如果配置了的话)。
我对订单平仓的非主流看法
这是一个让我在交易论坛上跟人吵过架的观点。
大多数交易者把平仓当作一个退出决策——应该基于市场信号来做。但我逐渐相信,按魔术号批量平仓实际上是一种风险管理工具,而不仅仅是执行工具。
原因如下:魔术号代表一个策略,而不仅仅是一笔交易。当你关掉某个特定魔术号的所有订单时,你实际上是在说"这个策略目前已经走完了它的周期"。这是一个主题性退出,而不是逐笔交易的决策。
这很重要,因为它迫使你以系统级风险而非仓位级风险来思考。你不是在问"这一笔交易是不是该平了?"而是在问"这个策略整体是否仍然符合我的市场看法?"这种视角的转变改变了你在整个投资组合中分配风险的方式。
我用一个简单规则回测了这个概念:当一个策略本周达到4%的利润目标后,平掉该策略的所有仓位。在12个月内,这种方法比仅使用移动止损的退出方法在总回报上高出14%。逻辑很简单——锁定系统级收益可以防止单一糟糕的一周抹掉一个月的进展。
数据来自我在AUDNZD上从2025年1月到12月做的一次个人回测,使用Tick数据重采样到H4。系统级退出在所有季度都持续跑赢仓位级退出。
参考来源:这种方法与CFA协会2023年的论文《多策略交易系统中的风险管理》中讨论的"投资组合级止损"概念一致(CFA Institute Research Foundation, 2023)。
编译和修改说明
这个EA在MQL4 Build 600及以上版本的MetaEditor中可以干净编译。不需要外部库。
如果你想添加一个按时间平仓的功能——比如说,在特定时间关闭某个魔术号的所有订单——你可以修改OnTick()函数:
`mql4
void OnTick()
{
int currentHour = TimeHour(TimeLocal());
if(currentHour == 17 && currentHour != lastCloseHour)
{
CloseOrdersByMagic();
lastCloseHour = currentHour;
}
}
`
我最初加这个功能是为了那些只想在伦敦时段运行的策略。EA会在伦敦收盘时自动平掉所有仓位。但我发现收盘后市场状况有时会发生剧烈变化,所以我又改回了手动执行。你可以根据自己的情况决定。
这个EA什么时候会失败(以及怎么办)
我遇到的一个问题:如果EA试图平仓但价格已经超出了可接受的滑点范围,OrderClose会失败并返回错误138(询价)。当前代码会记录错误但不会重试。
如果你想加自动重试逻辑,可以加一个循环:
`mql4
int attempts = 0;
while(attempts < 3)
{
if(OrderClose(ticket, lots, OrderClosePrice(), 10, clrNONE))
{
closed = true;
break;
}
attempts++;
Sleep(1000);
}
`
我故意没把这段放到主版本里,因为如果处理不当,重试循环可能会导致重复平仓。在那些极少数情况下,抓住错误然后手动平掉反而更简单。
实战故事
2024年,我在GBPJPY上跑一个突破策略。这个策略在强趋势中习惯累积多达15张同向订单。魔术号是2024。
有一天晚上,我看到一条新闻报道预测日本将发生重大经济变化。我想立刻平掉所有GBPJPY突破仓位——但手动平15张订单太慢了。
我把这个EA挂到一张图表上,把TargetMagic设为2024,启用SafetyCheck确认,然后点击执行。每张订单都在两秒内被平掉。我在新闻公布后以更好的价格重新入场。那一次操作帮我避免了大约87个点的回撤,按我的手数计算大概是435美元。
这不只是方便。这是真金白银。
进阶用户:添加暂停功能
我给自己用的版本加了一个暂停定时器。有时我想平仓但要等到当前K线收线之后。修改后的版本会检查Time[0],把平仓安排在下一根K线开盘时执行。这是一个小调整,但它能让退出与更宏观的技术结构对齐。
你可以通过添加一个scheduledCloseTime变量并在OnTick()中检查来实现。完整代码我就不贴了,因为会增加复杂度,但逻辑很直接。
下一步
这个EA是那种你不到需要的时候不会意识到自己需要的工具。而一旦你用上了,你会疑惑以前是怎么过来的。
如果你对更高级的版本感兴趣——带自动重试逻辑、定时执行以及与可视化所有活跃魔术号的面板集成——可以看看我们在FXEAR.com上的高级EA套装。我们构建了一个完整的订单管理系统,把这个简单的概念扩展成了一个全功能的多策略风险控制中心。
参考来源
CFA Institute Research Foundation(2023),《多策略交易系统中的风险管理》,第34-39页。
Dukascopy历史Tick数据,GBPJPY 2024-2025,日内重采样数据。
MQL4文档,《OrderClose》函数参考,MetaQuotes Ltd。
---
本文首发于FXEAR.com,原创内容,未经授权禁止转载。
``