Summary: 一个完整的MQL4 EA,在指定每日时间自动平仓所有持仓,支持经纪商时区偏差调整、部分平仓比例、基于盈亏的跳过条件。适合日内交易者每日结算。




收盘自动平仓EA – 带智能覆盖的每日结算工具



说个场景。晚上11:55,你半睡半醒,突然想起欧美的单子还挂在那里,说好今天收盘前平的。手忙脚乱翻手机、开MT4、手动平仓……这套动作我重复了不下几十次。

后来实在受不了,就写了个简单粗暴的EA,只干一件事——到点全平。没有花哨的指标,不判断趋势,纯粹的风险管理。但奇怪的是,网上翻了一圈,居然没找到一个能干净处理经纪商时区差异的版本。大部分免费源码要么直接用TimeCurrent()不做任何偏移,要么写死小时数,夏令时一换就全乱套。

这个EA就是来填这个坑的。

为什么需要这个EA



每日收盘结算(EOD)在很多机构交易台是标配。国际清算银行(BIS)2024年6月季报显示,超过38%的即期外汇交易现在采用当日或次日结算惯例,很多自营交易公司也强制每日P&L核算风控。但在MT4零售端,这类自动化工具很少见。

这个EA做三件事:
  • 在用户设定的时间,平掉所有持仓(含挂单)。

  • <strong>自动适配经纪商时区</strong>——你只需要填经纪商服务器时间,不用换算本地时间。

  • 支持<strong>部分平仓</strong>比例,比如只平60%,留一部分继续跑。


  • 独创的点在这里:我加了一个利润覆盖条件。如果所有持仓的浮动盈利总额超过了账户净值的某个百分比(比如+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,原创内容,未经授权禁止转载。
    ``