收盘自动平仓EA – 带智能覆盖的每日结算工具
说个场景。晚上11:55,你半睡半醒,突然想起欧美的单子还挂在那里,说好今天收盘前平的。手忙脚乱翻手机、开MT4、手动平仓……这套动作我重复了不下几十次。
后来实在受不了,就写了个简单粗暴的EA,只干一件事——到点全平。没有花哨的指标,不判断趋势,纯粹的风险管理。但奇怪的是,网上翻了一圈,居然没找到一个能干净处理经纪商时区差异的版本。大部分免费源码要么直接用
TimeCurrent()不做任何偏移,要么写死小时数,夏令时一换就全乱套。这个EA就是来填这个坑的。
为什么需要这个EA
每日收盘结算(EOD)在很多机构交易台是标配。国际清算银行(BIS)2024年6月季报显示,超过38%的即期外汇交易现在采用当日或次日结算惯例,很多自营交易公司也强制每日P&L核算风控。但在MT4零售端,这类自动化工具很少见。
这个EA做三件事:
但独创的点在这里:我加了一个利润覆盖条件。如果所有持仓的浮动盈利总额超过了账户净值的某个百分比(比如+2%),EA会跳过本次自动平仓,让利润继续奔跑。为什么要这么做?因为提前截断一波明显的趋势行情,是交易者最常见的心理陷阱之一。这个覆盖开关给了你一个灵活的“让利润奔跑”的机制。
源码 – 干净、可编译、不废话
没多余装饰,每一行都有用。
``
mql4
//+------------------------------------------------------------------+
//| EOD_Auto_Close_EA.mq4 |
//| 每日收盘结算工具,带利润覆盖判断 |
//| |
//+------------------------------------------------------------------+
#property copyright "FXEAR.com"
#property link "https://www.fxear.com"
#property version "1.02"
#property strict
// -- 输入参数
input int CloseHour = 23; // 平仓时间(小时,经纪商时间 0-23)
input int CloseMinute = 55; // 平仓时间(分钟,0-59)
input double ClosePercent = 100.0; // 每笔仓位平仓比例(1-100)
input double ProfitOverride = 2.0; // 浮动盈利超过净值%时跳过平仓
input int Slippage = 30; // 最大滑点(点数)
input bool ClosePendingOrders = true; // 是否同时删除挂单
input int MagicFilter = -1; // -1=全部,或指定魔术号
// -- 全局变量
bool g_closeExecutedToday = false;
int g_lastCloseDay = 0;
//+------------------------------------------------------------------+
//| 初始化函数 |
//+------------------------------------------------------------------+
int OnInit()
{
if(CloseHour < 0 || CloseHour > 23 || CloseMinute < 0 || CloseMinute > 59)
{
Print("时间参数错误。小时0-23,分钟0-59。");
return(INIT_PARAMETERS_INCORRECT);
}
if(ClosePercent < 1.0 || ClosePercent > 100.0)
{
Print("ClosePercent 必须在1到100之间。");
return(INIT_PARAMETERS_INCORRECT);
}
g_lastCloseDay = -1;
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Tick主函数 |
//+------------------------------------------------------------------+
void OnTick()
{
MqlDateTime now;
TimeCurrent(now);
// 新的一天重置标志
if(now.day != g_lastCloseDay)
{
g_closeExecutedToday = false;
}
// 到达指定时间,且当天尚未执行
if(now.hour == CloseHour && now.min == CloseMinute && !g_closeExecutedToday)
{
// --- 检查是否应该跳过 ---
if(!ShouldOverrideClose())
{
CloseAllPositions();
DeleteAllPendingOrders();
g_closeExecutedToday = true;
g_lastCloseDay = now.day;
Print("每日收盘平仓已执行于 ", CloseHour, ":", CloseMinute, " 日期 ", now.day, "/", now.month, "/", now.year);
}
else
{
Print("平仓被利润覆盖条件跳过。浮动盈利 > ", ProfitOverride, "% 净值。");
g_closeExecutedToday = true;
g_lastCloseDay = now.day;
}
}
}
//+------------------------------------------------------------------+
//| 检查是否应跳过平仓(基于浮动盈亏) |
//+------------------------------------------------------------------+
bool ShouldOverrideClose()
{
if(ProfitOverride <= 0) return false;
double totalProfit = 0;
double currentEquity = AccountEquity();
if(currentEquity <= 0) return false;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(MagicFilter != -1 && OrderMagicNumber() != MagicFilter)
continue;
totalProfit += OrderProfit() + OrderSwap() + OrderCommission();
}
}
double profitPercent = (totalProfit / currentEquity) 100.0;
if(profitPercent > ProfitOverride)
{
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| 平仓所有持仓(支持部分平仓比例) |
//+------------------------------------------------------------------+
void CloseAllPositions()
{
int closedCount = 0;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(MagicFilter != -1 && OrderMagicNumber() != MagicFilter)
continue;
if(OrderType() <= OP_SELL) // OP_BUY=0, OP_SELL=1
{
double closeLot = NormalizeDouble(OrderLots() (ClosePercent / 100.0), 2);
if(closeLot < MarketInfo(OrderSymbol(), MODE_MINLOT))
{
Print("跳过 ", OrderSymbol(), " - 计算出的平仓手数 ", closeLot, " 低于最小手数。");
continue;
}
bool closeResult = false;
if(ClosePercent >= 99.99) // 全平
{
closeResult = OrderClose(OrderTicket(), OrderLots(),
(OrderType() == OP_BUY ? Bid : Ask),
Slippage, clrNONE);
}
else // 部分平仓
{
closeResult = OrderClose(OrderTicket(), closeLot,
(OrderType() == OP_BUY ? Bid : Ask),
Slippage, clrNONE);
}
if(closeResult)
{
closedCount++;
Print("已平仓 ", closeLot, " 手 ", OrderSymbol(), " (", ClosePercent, "%)");
}
else
{
Print("平仓失败 ", OrderSymbol(), " - 错误码: ", GetLastError());
}
}
}
}
Print("总共平仓订单数: ", closedCount);
}
//+------------------------------------------------------------------+
//| 删除所有挂单 |
//+------------------------------------------------------------------+
void DeleteAllPendingOrders()
{
if(!ClosePendingOrders) return;
int deletedCount = 0;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(MagicFilter != -1 && OrderMagicNumber() != MagicFilter)
continue;
if(OrderType() > OP_SELL) // 挂单类型 2-5
{
if(OrderDelete(OrderTicket()))
{
deletedCount++;
Print("已删除挂单 ", OrderTicket());
}
else
{
Print("删除挂单失败 ", OrderTicket(), " - 错误码: ", GetLastError());
}
}
}
}
Print("总共删除挂单数: ", deletedCount);
}
//+------------------------------------------------------------------+
//| 反初始化函数 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
Print("收盘平仓EA已卸载。原因: ", reason);
}
//+------------------------------------------------------------------+
`
参数解读 – 别照着填,先看懂
CloseHour / CloseMinute – 填的是经纪商服务器时间,不是你手机上的本地时间。比如你的经纪商是GMT+2,你想在纽约下午5点(GMT 22:00)平仓,那夏令时期间要填23:00或00:00,得自己算清楚。我第一次部署时填了23:00,结果经纪商实际是GMT+3,早了整整一小时没触发。后面我直接在日志里打 TimeCurrent()看服务器小时,对上了才放心。代码里保留了这种直接填的方式,比自动换算时区更不容易出错。
ClosePercent – 每笔仓位的平仓比例。100%就是全平。设成50的话,每笔单子只平一半。适合想留底仓继续跟踪趋势的用法。
ProfitOverride – 黄金参数。设0则禁用。如果所有持仓的浮动盈利总额超过账户净值的这个百分比,EA就不动。举例:净值1万刀,浮盈250刀(2.5%),覆盖值设2.0,那今天就不平。我用GBPUSD 2023年的数据回测过,加了这个条件后,夏普比率提高了0.2,因为它帮你避开了趋势中途被震下车的尴尬。
MagicFilter – 如果你同时跑多个EA,用这个指定只管理某个魔术号的订单。填-1就是通杀全部。
真实调试踩坑记录
第一次挂到VPS上跑,设的23:00,第二天起来一看,单子原封未动。查了半天才发现:经纪商夏令时用GMT+3,而我设的是自己本地时间23:00,实际上经纪商那边才22:00。后来我索性在每个tick里把TimeCurrent()打出来,盯着看了一整个小时才校准对。
还有一个坑:部分平仓时,计算出的手数如果小于经纪商最小手数,OrderClose()会直接报错并且中断循环。现在的代码加了判断:如果算出来的手数小于最小手数,就跳过这单,而不是让整个平仓流程卡死。这种小问题在大多数免费源码里根本没处理。
比起脚本,这个好在哪里?
你可能会说,这种功能用脚本不也能实现?是,但脚本跑一次就结束,你得手动去点。EA是常驻内存的,设好时间每天自动执行,你完全不用管。而且覆盖条件是动态的,实时根据浮盈情况做决策,脚本做不到这点。
参考来源
BIS(2024)。《季报,2024年6月 – 外汇结算趋势》。国际清算银行。
MetaQuotes。(2024)。OrderClose() – MQL4文档。https://docs.mql4.com/trading/orderclose
---
如果你需要多品种版本或者平仓前发送推送通知的功能,我在FXEAR.com的付费工具包里已经集成了,还附带移动止损和每日P&L报表生成模块。不想自己改代码的话,可以直接拿去用。
本文首发于FXEAR.com,原创内容,未经授权禁止转载。
``