Entry Latency Is a Solved Problem. Position State After a Restart Is Not.

Entry Latency Is a Solved Problem. Position State After a Restart Is Not.

5 August 2026, 15:01
Boris Armenteros
0
32

There is a conversation that repeats often enough to be worth writing down. A trader has traded a setup manually for months, decides to automate it, and frames the project as "turn my rules into an EA." The entry logic gets written in an afternoon. Then the EA runs for three weeks and does something nobody expected at 03:00 on a Tuesday.

The entry logic was never the hard part.

What automation actually removes

Worth being precise, because it gets oversold: automating a strategy does not improve the strategy. If the rules have no edge, executing them faster produces losses faster.

What automation removes is execution variance the trader never chose — the entry missed during a meeting, the one hesitated on after two consecutive losses, the one taken four candles late because the London open happened while they were asleep. Those are attendance problems, and code does not get tired. That is a real and measurable gain, and it is also where most people stop thinking about the project.

The part the Strategy Tester will not generate

The Strategy Tester runs your EA in a world where the terminal never closes, fills are complete, and the broker's stop level never moves. Live trading provides none of those guarantees.

The most common production failure we see in inherited code is state. An EA holds its position context in global variables — a ticket, an entry price, a flag saying "I am long" — and the terminal restarts mid-position. OnInit()  fires, the globals are back to their initial values, and the EA now believes it is flat while the account holds an open position. The next signal opens a second one.

The fix is not clever, it is just rarely written: never trust in-memory state on init. Rebuild it from the terminal.

//+------------------------------------------------------------------+
//| Rebuild position context from the terminal, not from memory.     |
//| Called from OnInit() so a mid-position restart cannot leave the   |
//| EA believing it is flat while the account holds a position.      |
//+------------------------------------------------------------------+
bool RecoverPositionState(const long magic, const string symbol, PositionCtx &ctx)
{
   ctx.ticket = 0;
   ctx.isOpen = false;

   const int total = PositionsTotal();
   for(int i = total - 1; i >= 0; i--)
   {
      const ulong ticket = PositionGetTicket(i);
      if(ticket == 0)
      {
         printf("%s: PositionGetTicket failed at index %d, err=%d",
                __FUNCTION__, i, GetLastError());
         continue;
      }
      if(PositionGetInteger(POSITION_MAGIC) != magic)   { continue; }
      if(PositionGetString(POSITION_SYMBOL) != symbol)  { continue; }

      ctx.ticket     = ticket;
      ctx.entryPrice = PositionGetDouble(POSITION_PRICE_OPEN);
      ctx.volume     = PositionGetDouble(POSITION_VOLUME);
      ctx.direction  = (int)PositionGetInteger(POSITION_TYPE);
      ctx.isOpen     = true;

      printf("%s: Recovered position | Ticket=%I64u | Entry=%.5f | Vol=%.2f",
             __FUNCTION__, ticket, ctx.entryPrice, ctx.volume);
      return true;
   }

   printf("%s: No open position for magic=%I64d on %s — starting flat",
          __FUNCTION__, magic, symbol);
   return false;
}

Three details matter more than the loop itself. Filtering on both magic number and symbol prevents an EA from adopting a position another EA opened — a portfolio of EAs on one account will otherwise collide. Reading POSITION_VOLUME  back rather than assuming the requested lot size is what makes the recovery survive a partial fill; the position may be smaller than the order that created it. And logging the recovery unconditionally, not behind a debug flag, means the journal can answer "what did the EA think it owned?" weeks later, which is the question you will actually be asked.

The same discipline applies to stop levels

A related failure: an EA computes a stop from an indicator, sends it, and the broker clamps it to the minimum stop distance. The EA's internal record and the server's record now disagree, and every subsequent trailing calculation is built on a number that was never accepted.

Compute the effective value before sending, then validate and log that:

const double stopsLevel = (double)SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL) * _Point;
const double effectiveSL = MathMin(indicatorSL, SymbolInfoDouble(symbol, SYMBOL_BID) - stopsLevel);
printf("%s: SL %.5f (requested %.5f, stops level %.1f pts)",
       __FUNCTION__, effectiveSL, indicatorSL, stopsLevel / _Point);

The practical takeaway

If you are automating a strategy you already trust, budget your effort by where the failures actually are. Entry logic is a known quantity. The work that decides whether the EA survives an unattended night is state recovery, partial-fill handling, and reconciling what you asked the broker for with what the broker accepted — none of which a clean backtest will ever exercise.

Write the recovery path first. It is the part you will be glad exists at 03:00 on a Tuesday.