Stale Quotes on MT5: My Gold EA Ran a Full Session on a Frozen Bid and Ask, and My Monitor Said Normal

15 October 2026, 01:33
Yuki Nakayama
0
21

This is the ninth entry in the incubation diary for my gold breakout EA. The EA trades XAUUSD on M15: during 10:00 to 13:59 UTC it keeps a buy stop at the highest high and a sell stop at the lowest low of the last 128 closed bars, with an initial stop of 0.5 ATR, a trailing exit and a 480-minute time stop. It runs on a demo account at XM. Fills dated 7 October or later are still sealed for the pre-registered slippage test described in the seventh entry, so this entry contains no trade results at all. It is about plumbing: for one full trading session this week the EA made every decision against a bid and ask that had stopped moving at 14:03 UTC the day before, and the monitor I built to catch silent failures reported that everything was normal.

What the logs show

Every time a new M15 bar opens inside the session, the EA writes a line with the channel levels, the ATR, the current spread and the number of open positions. Here are the spread readings from two consecutive sessions. Each session has sixteen bars.

SessionBars loggedSpread readingsDistinct values
Mon 12 Oct, 10:00 to 13:45 UTC160.21 to 0.29, three of them 0.278
Tue 13 Oct, 10:00 to 13:45 UTC160.27 every time1

The last reading on Monday, at 13:45 UTC, was 0.27. On Tuesday the spread never read anything else. One flat session on its own could be a quiet market. What makes it more than that is a second EA on the same terminal, a USDJPY research EA that enters at a fixed clock time and logs the price it sees before sending the order. On Monday at 14:45 UTC and again on Tuesday at 14:45 UTC it logged the same entry price to the last digit, 158.656, and computed the same stop, 158.906. The broker filled the Tuesday order at 158.366. Monday's order, sent 42 minutes after the first synchronization, had already filled 1.5 pips away from its logged price, at 158.671. The EA believed it was selling 29 pips higher than it actually was.

That second EA is where the damage is easiest to see. Its stop is meant to sit 25 pips from entry, and its lot size is computed from that distance. Because the stop was anchored to the frozen price rather than the real fill, the actual distance was 54 pips, a little over twice the risk the position was sized for. The position closed at its time exit half a pip against it, so the account did not feel it. That was the market's doing, not the code's.

What happened on the broker side

The terminal journal has the start of it. At 14:03 UTC on Monday the terminal lost its connection to the trade server, reconnected within seconds, and then wrote a line I had not seen before: Symbols: perform full synchronization. The same disconnection and the same line appeared within seconds on three other terminals on this machine that connect to the same XM server. A second full synchronization followed on the gold EA's terminal at 20:03 UTC the same day.

To check how unusual that is, I searched every journal this terminal has written in 2026: 188 daily log files since 2 January. They contain 127 ordinary disconnections before Monday and not a single full synchronization. Ordinary reconnections have never left the quotes stale in any way I could detect. This one did.

The odd part is what kept working. The M15 bars kept updating, so the channel levels and the ATR in Tuesday's log lines moved normally; the upper level drifted from 4217.15 down to 4200.43 as old highs left the window. The EA kept receiving its new-bar events. Orders went to the server and were accepted. Only the bid and ask that a program reads back from the symbol stayed fixed. On one of the other terminals, a separate script that reads prices through the Python API saw fresh ticks while the program on the chart did not. I cannot tell from the logs what changed inside the terminal, and I am not going to guess. The working description is: after the full synchronization, programs that were already running on charts were reading a symbol state that no longer updated.

It did not fix itself. It survived the daily one-hour gold break and the second disconnection and full synchronization at 20:03 UTC. The quotes came back only after the PC shut down at about 00:14 UTC on Wednesday and came back nine minutes later, a restart I had not scheduled. From the first full synchronization to that reboot is about 34 hours. The second EA's Tuesday log proves the quotes were still frozen at least 24 hours in; I have no evidence they recovered before the reboot.

Why the gold EA got through it, and why that was luck

While it trades, the gold EA uses the bid and ask for five decisions (a sixth read is a lot-size estimate printed once at start-up). Going through them one by one is the most useful thing I can do with this incident, because the answer to "was it affected?" is different for each.

1. The spread filter. The EA does not place orders if the spread is above a ceiling. With the spread frozen at 0.27, that filter was effectively switched off for a day. Had there been a real spread spike, the EA would not have seen it.

2. Deciding whether a level has already been crossed. Before placing the buy stop, the EA compares the ask with the channel high. If the ask is already above it, the EA either chases with a market order (up to a small fraction of the ATR) or skips. With a frozen ask, that comparison uses Monday's price. If Monday's price had been above Tuesday's channel high, the EA would have sent market orders against a price that no longer existed.

3. The broker's minimum stop distance. The EA skips a level that is too close to the current price to accept a stop order. Same problem: the distance was measured from the wrong price.

4. The retry after a rejected order. I added a retry in version 1.25 for the case where a stop is rejected because the price moved through the level while the order was in flight. It reads a fresh tick and re-applies the chase rule. With the quotes frozen, the "fresh" tick is the same stale one, so the retry would conclude that the price had not reached the level yet and do nothing.

5. The trailing stop. The trail measures the best open profit from the current bid or ask. Against a frozen price, the trail would either never arm, or try to move the stop to a level computed from yesterday's price, which the server could reject.

On Tuesday none of these paths produced a bad trade, for two reasons that have nothing to do with the code. Monday's frozen price sat inside Tuesday's channel, so the EA kept placing ordinary stop orders, which is the behaviour it would have had anyway. And the real price never touched either level during the session, so nothing filled and there was no open position for the trail to mishandle. The pending orders themselves were fine: they live on the server, and the server fills them against real prices whatever the EA believes. The one path that would still have worked is the time stop: it uses server time, and server time kept advancing even while the quotes did not.

Put plainly: the EA had no protection against this, and the session happened to be one where that did not matter.

What did have protection

Two execution-measurement probes run on a different demo terminal. One of them, started on 28 September, checks the age of the last tick against server time before it places anything. If the tick is more than five seconds old, it writes a skip line and waits five minutes. From 14:15 UTC on Monday to 00:12 UTC on Wednesday it skipped 345 cycles in a row. Its sample is clean: no measurement was taken against a frozen price.

The older probe on the same terminal has no such check. It kept placing orders the whole time, with levels computed from the frozen price, and four of its stop-loss requests were rejected by the server as invalid. Its data from that window will have to be excluded or flagged when it is read out. I do not yet know how many rows that will be in the final count, and I am not looking at its results before then.

The first probe is also the experiment I promised to report on in the comments of the seventh entry: server-side stop orders against stops managed by the EA itself. It closes on 16 October. The skipped cycles cost it sample size, not validity, and the result will be its own entry.

The monitor said everything was normal

This is the part I find most uncomfortable. Since August I have run a watchdog next to the EA. Every 15 minutes it reads the EA log, counts new-bar lines and order rejections, and sends a push notification if the EA goes quiet inside its session window. It also sends one positive message per session, so that silence from the watchdog itself means something. Rejections are on its list because of the first entry, where 78 rejected orders went unnoticed for more than two days.

On Tuesday at 10:15 UTC it sent that positive message: the measurement is running. For the rest of the session, every 15 minutes, it logged the same verdict: inside the window, new-bar lines present, zero rejections, normal. All of that was true. The EA was alive, it was writing its lines, and the server was accepting its orders. The watchdog checks that the EA is running. It does not check whether the EA is seeing the market.

The lesson I take from the first entry and from this one together is that every check I have built tests the failure I had last time. Rejections were the last failure, so the watchdog counts rejections. A frozen quote produces no rejections, no errors and no gaps in the log. It produces normal-looking lines with a number in them that does not change.

What I am changing, and what I am not

Nothing has been changed yet. The EA running now is the same build that ran through the incident. The plan, in order:

First, the monitor. The new-bar line will carry the age of the last tick in seconds, measured against server time, and the watchdog will alert if that age is above a threshold inside the session, or if the bid and ask have not changed across several consecutive bars. This needs one extra field in a log line and no change to how the EA trades, so it can go in without disturbing the forward test.

Second, a guard in the EA itself, the same rule the clean probe uses: if the last tick is older than a few seconds when the EA is about to place, chase, retry or trail, it does none of those things and logs why. This changes behaviour only in a state where every decision the EA would make is already wrong, so I do not consider it a change to the strategy. It is still a code change in the middle of a forward test, so it will go in as a new version with the date written down, and the ledger will show which trades came before and after it.

Third, the procedure. If the journal shows a full symbol synchronization, I restart the terminal, the same way I would after a crash. That would have shortened this incident from 34 hours to whatever my reaction time is, and it needs no code at all.

What I am not doing is reading anything into Tuesday about the strategy. No trade was made against the frozen price, and whatever the EA has done since the reboot is sealed until the pre-registered test is read out.

A note for anyone running EAs on MT5

If your EA uses the current bid or ask for anything beyond sending an order, and nearly every EA does, it is worth checking what happens when that price stops updating while everything else keeps working. Bars that keep updating, a log full of normal lines and a broker that accepts your orders do not tell you the price is fresh. In my case the checks were simple: is the last tick's timestamp recent compared with server time, and has the spread been exactly the same for an implausibly long stretch. Neither was in the EA I am forward testing. The first was in the probe I wrote at the end of September, which is a fair summary of how this kind of knowledge accumulates: one incident at a time, and usually in the newest code first.