Twenty-Five Fills, None in My Favour: Measuring Entry Slippage Without a Backtest
This is the fourth entry in a public incubation diary for a gold expert advisor that is not for sale and may never be, depending on what the verification says. I publish the checks as they happen, including the ones I fail and the ones I get wrong, because the ability to audit a system matters more than the numbers its author chooses to show you.
This week the reconciliation between my Python research code and the live EA failed for the third time running. Last week I wrote that the failure was probably an artefact of my reproduction rather than a real cost. This week I went to the primary record and found I had been half right in a way that was more expensive than being simply wrong.
The reconciliation, and why it keeps failing
The EA runs on a demo account and prints its decision inputs on every M15 bar. A Python script re-derives those inputs from the same broker's minute data and reproduces where each pending order would have sat and when it would have been hit. Stage one of that comparison — do the Donchian levels and the ATR agree? — has passed since 21 August and passed again this week across 252 bars, with a difference of zero on the levels and an ATR difference no larger than half the print rounding.
Stage two is the execution: given that the order sat at that price, when and at what price did it fill? The acceptance criteria were fixed before any of this ran — at least 90% of live trades matched to a reproduced trade within 15 minutes and $0.30, no more than a 10% difference in trade count, zero live trades with no counterpart, and a total-R difference within 15%.
| Criterion | Threshold | Result (n=25) |
|---|---|---|
| Trade correspondence | ≥ 90% | 17/25 = 68.0% fail |
| Trade count difference | ≤ 10% | 0.0% (25 live, 25 reproduced) pass |
| Live trades with no counterpart within tolerance | 0 | 8 fail |
| Total R difference | ≤ 15% | +2.963R live vs +3.129 to +3.286R reproduced pass (matched trades only) |
The last row uses 14 trades, not 17: three of the matched trades opened and closed inside a single minute, and a one-minute bar holds no evidence about what happened inside itself, so they are excluded rather than guessed at.
The strange part is that criteria one and three fail while criterion two passes perfectly. The reproduction is not inventing trades or missing them. Every one of the eight unmatched trades has a reproduced counterpart in the same direction with the same timestamp to the minute. The only thing outside tolerance is the price.
A measurement that needs no model at all
Last week I attributed the price gaps to the limits of minute data: a one-minute bar records a high and a low, not the path between them, so a reproduction cannot know what happened inside the minute. That is true. It is also a comfortable answer, and comfortable answers deserve a second look.
The second look was the terminal journal, which records the price at which an order is accepted and the price at which the resulting deal executes, tagged with the same ticket number. Four consecutive lines from 3 September, local time, unedited except for trimming the account number and the source column:
21:30:00.516 buy stop 0.01 GOLD# at 4450.22 sl: 4446.08 21:30:00.662 accepted buy stop 0.01 GOLD# at 4450.22 sl: 4446.08 21:30:00.662 order #981466147 buy stop 0.01 / 0.01 GOLD# at 4450.22 done in 146.013 ms 21:30:02.747 deal #964976104 buy 0.01 GOLD# at 4457.67 done (based on order #981466147)
The order was accepted at 4450.22 and the deal printed 2.085 seconds later at 4457.67. That is $7.45 away from where I asked to be filled, and observing it required no Python, no minute bars and no reproduction. It is the terminal's record of the broker's execution.
So my reproduction was right about where the order sat and wrong about what happens when a stop order triggers. It fills at the level, always, by construction. Reality does not.
I rebuilt the measurement from the journal alone. All 25 closed trades in the ledger matched a journal order, and in every case the journal's deal price agreed with the ledger's entry price to the cent, which is the sanity check that tells me I am reading the right lines.
Twenty-five entries, none of them in my favour
| Adverse gap, order price to fill price | As a fraction of the initial stop width | |
|---|---|---|
| n | 25 | 25 |
| adverse / neutral / favourable | 25 / 0 / 0 | — |
| minimum | $0.010 | 0.18% |
| lower quartile | — | 1.19% |
| median | $0.190 | 4.32% |
| upper quartile | — | 11.17% |
| mean | $0.742 | 17.80% |
| maximum | $7.450 | 179.78% |
Twenty-five out of twenty-five adverse looks like a striking result. It is not, and the reason matters, because this is the kind of number that ends up in bold on a product page.
A buy stop triggers when the ask reaches the level and then executes at market, so the fill lands at or above the level in all but the rare case where price ticks back in the milliseconds before execution. A sell stop is the mirror. The direction of the gap is a property of the order type, not an observation about my broker, my strategy or the market, and anyone using stop entries on any instrument should expect nearly every fill to be adverse. Quoting the direction as evidence of honest execution would be quoting a tautology.
What carries information is the magnitude, and one number does not summarise it. The median is $0.190. The mean is nearly four times that, at $0.742, and most of the difference comes from two trades; without them the mean is $0.277 and the maximum falls from $7.450 to $1.370. This is a distribution with a thin body and a fat tail, and quoting only the mean or only the median would misrepresent it in opposite directions.
Where it actually hurts: the stop is anchored to the order, not the fill
The EA computes its initial stop from the order level, one line above the order itself:
double lvl = NormalizeDouble(up, digits); double sl = NormalizeDouble(lvl - InitialStopATR * atr, digits);
Nothing moves that stop towards the fill afterwards. The trail only ever moves the stop in the profitable direction, and only after the trade is half an ATR in front. So when an entry slips, the stop stays where it was and the real distance from entry to stop is the intended width plus the slippage.
That has an exact consequence. For a long, entry is at level plus slippage and the stop is at level minus the stop width, so a stop-out returns −(1 + slippage ÷ stop width). The floor is set by the slippage, not by the spread. Across all twelve losing trades:
| Losing trades (n=12) | Value |
|---|---|
| median distance below the slippage-anchored floor | −0.0184R (−$0.083) |
| worst | −0.0624R (−$0.294) |
| best | −0.0012R (−$0.007) |
| finishing below the floor | 12 of 12 |
Every loss lands just under the predicted floor. That residual is one-sided, as it has to be — the stop-loss is itself a stop order executed at market, so it fills a tick or more past its own level — and it is small: under a cent to thirty cents of price, median eight cents. Against an entry slippage whose median is nineteen cents and whose worst case is $7.45, the exit side is both smaller and has no tail at all. Exit execution, unlike entry execution, is unremarkable.
The entry is where the damage sits. Because the stop does not follow the fill, slippage inflates the risk actually carried rather than merely shaving the edge. Measured against the nominal stop width the R denominator uses, realised initial risk was a median of 1.043 times nominal and a maximum of 2.798 times. On the 3 September trade the nominal stop width was $4.144 — the journal rounds it to $4.14 when it prints the order — and the true distance from fill to stop was $11.59.
Which also means the ledger flatters that trade. It records +0.094R against the nominal width; against the risk actually taken it was closer to +0.034R. Every R in a stop-entry system carries this assumption, and I had not been checking it.
The tail has a timestamp
The two outliers are $4.75 and $7.45, and both fills landed at exactly 12:30 UTC.
| Fill time (UTC) | Side | Order price | Fill price | Gap | Order to fill | Result |
|---|---|---|---|---|---|---|
| 2026-09-03 12:30 | long | 4450.22 | 4457.67 | $7.45 | 2 s | +0.094R |
| 2026-09-04 12:30 | short | 4418.76 | 4414.01 | $4.75 | 15 s | +4.513R |
| 2026-08-26 13:16 | short | 4605.26 | 4603.89 | $1.37 | 61 s | +0.657R |
| 2026-09-01 12:32 | short | 4364.21 | 4363.25 | $0.96 | 154 s | +1.268R |
| 2026-08-27 10:02 | short | 4578.19 | 4577.49 | $0.70 | 144 s | +1.335R |
Three of those five orders — the first, second and fourth — were placed on a 12:30 decision bar. 12:30 UTC is 08:30 in New York under US daylight saving time, the standard slot for American macro releases. In a broker calendar I keep locally, covering 29 June 2017 to 24 July 2026, 12:30 is the modal release time for high-importance events by a wide margin: 1,347 of 3,808, or 35.4%, against 650 for the next most common slot.
That is a base rate and not a fact about these two days, and the calendar file stops in July, so I checked the days themselves against public sources. 4 September was the August employment report at 08:30 Eastern; payrolls came in at 162,000 against a consensus near 58,000, a large upside surprise. 3 September was the weekly jobless claims release at the same time, printing 206,000 against a forecast of 205,000, essentially in line.
Note that the larger gap fell on the smaller surprise. With two observations that is noise, but it is the kind of noise that should stop anyone writing "slippage scales with surprise" as though it were established.
There is a second signature, and this one does not depend on the fills at all. The EA prints the current spread on every decision bar. Across 276 such bars from 14 August to 9 September the spread at bar open has a median of $0.250 and a maximum of $0.410. There are five bars at that maximum, and all five are at 12:30. Every other time slot in the sample tops out at $0.290.
| Slot | Decision bars | Median spread | Mean spread | Maximum spread |
|---|---|---|---|---|
| 12:30 UTC | 18 | $0.270 | $0.297 | $0.410 |
| all other | 258 | $0.250 | $0.248 | $0.290 |
The medians differ by two cents. The entire difference lives in the tail, in the same place the fill gaps live.
What I am not going to do with this
The obvious move is a filter that stops the EA placing orders at 12:30. I am not going to add one, and the reason is the discipline this diary is supposed to demonstrate.
What I have measured is the accuracy of a reproduction, not the profitability of a rule. Those are different questions and the fill gaps do not answer the second. All four trades from 12:30 bars finished positive, and the largest winner in the ledger is one of them. Refusing to trade a minute because it is hard to model is optimising the model rather than the system.
Beyond that, the fill-based case for a time-of-day rule currently lives in a tail holding two observations, and a filter drawn from where two observations happened to fall is a filter fitted to two data points. I have written before in this diary about deriving rules from a mechanism rather than from an outcome, and this is the same trap wearing a new hat.
So I am registering the test in advance instead. There are four fills so far from the eighteen 12:30 decision bars; I will not judge the slot until there are at least ten. The test will be a two-sided rank-sum comparison of the absolute price gap at 12:30 against every other slot, at the 5% level, and I additionally require the 12:30 median to be at least twice the median elsewhere before I treat it as actionable. If it does not clear both, the outliers were coincidence and I will say so here.
The $0.30 tolerance also stays where it is. Widening a threshold because the data will not clear it is how a reconciliation stops being a reconciliation. What changes is the reproduction: it has to model the fill instead of assuming it.
Two corrections to my own notes
First, the floor test above. I had been computing that floor from the entry spread rather than the entry slippage, which is simply the wrong quantity — the stop is anchored to the order price, so the gap that matters is the one between order and fill. Under the wrong model the residuals looked two-sided and inconclusive, with six of the twelve trades finishing under the bound and the rest over it. Under the right one they are all on the same side, small, and explained. A test that produces a muddle is often testing the wrong thing.
Second, and larger: last week I filed this whole effect under "limitation of my reproduction". The reproduction is genuinely limited, but that framing let me shelve a real, recurring execution cost for a week. It was sitting in the journal the entire time in plain text, and I did not look, because I had already explained it to myself.
What this ledger is and is not
Twenty-five closed trades on a demo account between 18 August and 4 September, all at 0.01 lots, every one closed by a stop — the initial stop or the trail, which the ledger records identically. Thirteen finished positive and twelve negative. The median holding time is one minute and the longest is eleven.
The sum is +6.384R, and a single trade accounts for +4.513R of that, or 70.7%. I report that ratio specifically so that nobody, including me, mistakes this for evidence that the system works. Twenty-five trades with one observation carrying 70% of the total is not a result. It is a sample that has barely started, and its use right now is exactly what I have used it for here: measuring execution, which becomes informative at a sample size where profitability is still meaningless.
Next
The next job is putting a fill model into the reproduction so criterion one tests what it was meant to test, and deciding whether the EA should re-anchor its stop to the fill price — which is a change to the strategy, not to the measurement, and therefore not one I will make casually. After that, the walk-forward and permutation tests this EA has still not been through, which I will publish whatever they say.
If you want to see the same kind of self-auditing built into things that are actually finished, my free tools are on my seller page.


