An EA that has stopped working usually looks exactly like an EA that is waiting for a signal. No trades, no errors in front of you, just a quiet chart. The terminal may have lost its connection, the VPS may have restarted, AutoTrading may have been switched off, or the EA may have been removed by a profile change. You find out days later.

A heartbeat file fixes that. Every minute the EA writes the current time and a few status values to a small file. Anything outside the terminal (a script, a cron job, a monitoring tool) reads the file and raises an alarm when the timestamp gets old.

What to write

Keep it to one line so the reader never has to parse much:

  1. Local time of the write, as a Unix timestamp.
  2. Whether the terminal is connected to the trade server.
  3. Whether trading is allowed for the terminal and for this EA.
  4. Account equity.
  5. Number of open positions with this EA's magic number.

The code

input long InpMagic = 20261112; int OnInit() { if(!EventSetTimer(60)) { Print("EventSetTimer failed: ", GetLastError()); return INIT_FAILED; } WriteHeartbeat(); return INIT_SUCCEEDED; } void OnDeinit(const int reason) { EventKillTimer(); WriteHeartbeat(reason); } void OnTimer() { WriteHeartbeat(); } int CountMyPositions() { int n = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { if(PositionGetTicket(i) == 0) continue; if(PositionGetInteger(POSITION_MAGIC) == InpMagic) n++; } return n; } void WriteHeartbeat(const int deinitReason = -1) { string name = "heartbeat_" + _Symbol + "_" + (string)InpMagic + ".txt"; int h = FileOpen(name, FILE_WRITE | FILE_TXT | FILE_COMMON | FILE_ANSI); if(h == INVALID_HANDLE) { Print("Heartbeat open failed: ", GetLastError()); return; } string line = StringFormat("%I64d;%d;%d;%d;%.2f;%d;%d", (long)TimeLocal(), (int)TerminalInfoInteger(TERMINAL_CONNECTED), (int)TerminalInfoInteger(TERMINAL_TRADE_ALLOWED), (int)MQLInfoInteger(MQL_TRADE_ALLOWED), AccountInfoDouble(ACCOUNT_EQUITY), CountMyPositions(), deinitReason); FileWriteString(h, line + "\r\n"); FileClose(h); } 

A few details that matter:

  1. FILE_COMMON puts the file in the shared Common\Files folder, so the path does not depend on which terminal instance is running. That makes the external checker simpler.
  2. The file is opened with FILE_WRITE each time, which truncates it. There is always exactly one current line, not a growing log.
  3. FileClose flushes the write. Without closing (or FileFlush) the line can sit in a buffer.
  4. The deinit reason is written on the way out. If the last line shows reason 1 (REASON_REMOVE), someone removed the EA. A stale file with reason -1 means the terminal itself stopped.
  5. TimeLocal is used on purpose. TimeCurrent only moves when quotes arrive, so on a quiet market or a weekend it stands still and the heartbeat would look stale for the wrong reason.

The checker

The reader only needs to compare the timestamp with the clock. In Python:

import time, pathlib path = pathlib.Path(r"C:\Users\YOU\AppData\Roaming\MetaQuotes\Terminal\Common\Files\heartbeat_EURUSD_20261112.txt") fields = path.read_text().strip().split(";") age = time.time() - int(fields[0]) connected, terminal_ok, ea_ok = fields[1], fields[2], fields[3] if age > 180: print("ALERT: heartbeat is", int(age), "seconds old") elif connected != "1" or terminal_ok != "1" or ea_ok != "1": print("ALERT: EA alive but cannot trade", fields) else: print("OK", fields) 

Note that TimeLocal and Python's time.time both come from the same machine clock when the checker runs on the same VPS, so there is no time zone math. Run the checker on another machine and you will need to account for the clock offset.

Three minutes is a good threshold

With a 60 second timer, allow at least two missed writes before alarming. A timer event is not queued if one is already waiting, so a slow OnTimer can skip a beat now and then. Alerting on one missed beat produces false alarms, and false alarms get ignored.

The heartbeat does not tell you the strategy is good. It tells you the strategy is actually running, which is the question you want answered first.