Live trading is messier than the Strategy Tester. Connections drop. Prices jump. The trade server rejects an order that looked fine a tick ago. If your EA assumes every OrderSend succeeds, you will get stuck positions and duplicate risk.
What usually breaks
- The terminal loses the trade server for a few seconds. Your EA keeps thinking an order is pending when it never landed, or the opposite.
- A requote or trade context busy error returns. A naive retry loop hammers the server and can open two tickets for one signal.
- After a restart, OnInit runs again. Without a recovery path, the EA forgets which magic numbers it already owns.
Patterns that hold up
Track intent separately from confirmation
When you decide to enter, write a local state: signal id, side, volume, stop, take profit, timestamp. Only clear that intent after you see a matching deal in history or an open position with your magic and comment. If OrderSend fails, leave the intent and decide whether to retry under clear rules.
Bound your retries
Retry on transient codes such as trade context busy. Do not retry forever on invalid stops, no money or market closed. Sleep a short, increasing delay between attempts, and give up after a small number of tries for that signal.
Rehydrate state on startup
In OnInit, scan open positions and recent history for your magic number. Rebuild any trailing or breakeven state from what is actually on the account. Never assume memory from before the crash still matches the book.
One signal, one ticket
Store the bar time or a unique signal key for the last entry. Refuse a second entry for the same key even if the chart redraws or OnTick fires again after a reconnect.
Log failures with the retcode
Print or write the trade retcode, bid, ask, and relative spread when an order fails. When a client says "it randomly skipped trades," those logs are the difference between a guess and a fix.
A minimal checklist before you go live
- Kill the terminal mid trade on demo and confirm the EA recovers cleanly.
- Change the internet path (or use airplane mode for a minute) and watch how pending intents resolve.
- Verify magic number filters so a manual trade on the same chart is never managed by accident.
None of this replaces a real edge. It stops infrastructure noise from looking like strategy failure.
I write about MT5 risk control and custom bots at Custom Trading Bots.
Top comments (6)
I would add one recovery test: disconnect just after the server accepts the request, before the EA receives confirmation. OrderSend() returning true does not prove a fill, so check result.retcode and reconcile orders and deals before retrying. The expected result is one entry for the signal, even after restart. A timeout should leave that intent unresolved until reconciliation, rather than automatically triggering another send.
That is the test that catches the most expensive bug, so I agree it belongs in the list. The way I handle it is to store the intent before sending: a signal ID, the symbol and the direction, saved to a file or global variable. If the send times out or the terminal restarts, the EA treats that intent as unresolved and checks open positions and recent deals with its magic number and the signal ID in the comment before doing anything else. Only when nothing matching is found after a short wait does it allow a new send. It is a few more lines of code, but it turns a possible double entry into one line in the log.
The short wait is the part I would test next. An empty position/deal scan is not conclusive if a pending order is still active or history is not ready. I would keep the intent unresolved, include active orders in reconciliation, and test a late first fill arriving after the retry window. The expected result is still one entry for that signal after a restart. What confirms that reconciliation is complete before your code allows the second send?
Honest answer: nothing in MQL5 proves a negative, so I stopped treating an empty scan as permission. The window expiring never unlocks a resend on its own. The intent stays unresolved, and a second send needs positive evidence on every source at once.
Only when all of that passes twice in a row with the connection steady in between does the intent move to failed and allow a new send. If anything is inconclusive, the EA alerts and waits rather than guessing.
The late fill case you describe is handled in OnTradeTransaction. Every new deal is matched back to its signal ID, and if a signal ever ends up with two entries, the newer one is closed and the event is logged loudly. Your test sequence is exactly the one I would add: timeout, restart, empty first scan, then the original fill arriving after the retry window, with the expected end state of one position for that signal.
Thanks, the order-ticket check makes the policy clearer. I would add a netting-account version of that test too. Two same-direction fills can increase one position, so an end state of one position could still pass with twice the intended exposure. I would assert the final volume and deal IDs for the signal as well as the position count.
For a 0.10-lot intent, the delayed original fill plus a 0.10-lot retry should never leave 0.20 lots unnoticed. Does your recovery test cover that case separately from hedging accounts?
Reference: mql5.com/en/docs/trading/positiong...
Yes, and you are right that position count is the wrong assertion there. On a netting account the duplicate does not create a second position, it just grows the one that exists, so the hedging test would pass while the exposure is doubled.
The netting version of the test asserts on deals instead:
When the excess is detected live, the fix is different too. There is no second ticket to close, so the EA sends an opposite deal for exactly the excess volume, tagged with the same signal ID, and logs both deal tickets so the sum can be audited afterwards.
So the expected end state for your sequence on netting is one position, entry volume for that signal equal to 0.10, and either no excess deal or one correcting deal that brings it back to 0.10.