A trading system that quotes a price is not the same as a trading system that executes at that price. Between the quote and the execution lies a gap — measured in time, measured in pips, measured in rejected orders — that engineering choices directly affect.

Understanding slippage and rejection from the systems perspective is not just finance knowledge. It shapes how you design price feeds, order routing, and latency measurement.

The Three Costs

Spread: the difference between the price to buy and the price to sell at any given moment. If EUR/USD is quoted 1.28443/1.28453, the spread is 1 pip (0.00010). To buy at 1.28453 and immediately sell at 1.28443 is an instant 1-pip loss. The spread is the market maker’s revenue and the taker’s cost of immediacy.

Slippage: the difference between the price you intended to trade at and the price you actually traded at. You saw 1.28453 and wanted to buy; by the time your order arrived, the market had moved to 1.28461. You paid 0.8 pips more than expected.

Rejection: the venue refused to fill your order at all. Either the price moved beyond an acceptable tolerance, the counterparty is not willing to trade with you at this moment, or you exceeded a credit limit.

Quote received:    EUR/USD 1.28443 / 1.28453
Order sent:        BUY 10M at 1.28453
                   (15ms later)
Execution result:  ???
  → Filled at 1.28453: zero slippage (good)
  → Filled at 1.28461: +0.8 pip slippage (acceptable)
  → Filled at 1.28472: +1.9 pip slippage (significant)
  → Rejected:          try again at new price (latency cost)

What Causes Slippage

Time: the primary cause. Between quote and execution, the market moves. If your pipeline has 50ms of latency, prices can move significantly during that window. The volatility of EUR/USD is roughly 5 pips per minute during active sessions — that’s 0.25 pips per 3 seconds, or 0.004 pips per 50ms average. Irrelevant most of the time; significant during news events.

Quote staleness: your price feed lags the market. If your normalised best-bid-offer updates every 50ms but the underlying interbank market has moved 3 pips in that window, you’re trading on a stale price.

Venue selection: some venues have wider spreads but lower rejection rates. Others have tighter spreads but reject on stale quotes. The routing decision isn’t purely “cheapest price” — it’s “expected total cost including rejection probability.”

Measuring Slippage

Every executed order should be tagged with:

OrderId:            ORD-2014-06512
Symbol:             EUR/USD
QuoteTime:          14:08:23.441712  (when we saw the price)
QuotePrice:         1.28453
OrderSentTime:      14:08:23.442218  (506µs after quote)
ExecutionTime:      14:08:23.449831  (9.1ms after order)
ExecutionPrice:     1.28458
Slippage:           0.5 pip  (1.28458 - 1.28453)
VenueId:            CURRENEX

Tracking this allows you to build a statistical model of slippage per venue, per time of day, per market conditions. The questions that model answers:

  • Is one venue consistently worse than others? (Routing problem)
  • Does our slippage spike during specific market events? (Feed staleness problem)
  • Does slippage correlate with our own pipeline latency? (Infrastructure problem)

The last question is the engineering one. If slippage increases as our order-send latency increases, latency reduction has direct P&L value.

Engineering Rejection Handling

Rejections are expensive in two ways: you lose the trade opportunity, and you have to retry from a stale position. The latency of finding a new price and re-submitting is typically 10–50ms. During that time, the market can move further against you.

A rejection-aware order router:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class OrderRouter {
    private final Map<String, VenueStats> venueStats;

    OrderResult send(Order order, Price quote) {
        for (Venue venue : rankVenues(order.symbol, quote)) {
            OrderResult result = venue.send(order, quote.forVenue(venue.id));
            venueStats.get(venue.id).record(result);

            if (result.filled()) return result;

            if (result.rejectedBecause() == PRICE_MOVED) {
                // Stale quote — refresh price, then continue to next venue
                quote = priceAggregator.currentBBO(order.symbol);
                order = order.withPrice(quote.ask);
            }
            // else: capacity/credit rejection → just try next venue
        }
        return OrderResult.failed("No venue could fill");
    }

    List<Venue> rankVenues(String symbol, Price quote) {
        // Rank by: expected fill probability × (1 - expected slippage)
        return venues.stream()
            .sorted(comparingDouble(v ->
                -venueStats.get(v.id).expectedValue(symbol, quote)))
            .collect(toList());
    }
}

The key is feeding execution statistics back into routing decisions. A venue with a 20% rejection rate is worth less than its headline spread implies.

Spread Validation: Detecting Stale Prices

A common bug in early trading systems: receiving a price that’s significantly stale and acting on it. A price that’s 500ms old during a news event can be 10 pips off the current market.

Simple spread validation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
boolean priceIsUsable(Price price, Instant now) {
    // Is the price too old?
    long ageMs = Duration.between(price.timestamp(), now).toMillis();
    if (ageMs > MAX_PRICE_AGE_MS) return false;

    // Is the spread abnormally wide? (Indicates illiquidity or stale data)
    double spreadPips = (price.ask() - price.bid()) / PIP_SIZE;
    if (spreadPips > MAX_SPREAD_PIPS) return false;

    // Is the price far from the last known good price? (Spike detection)
    double deviation = Math.abs(price.mid() - lastGoodMid) / PIP_SIZE;
    if (deviation > MAX_DEVIATION_PIPS) return false;

    return true;
}

The spike detection (last point) catches obvious bad data — a feed sending 1.28450 when the previous price was 1.08450 (transposition error). In a live system, this filter saves you from executing at a price that nobody would actually fill.

The Latency-Slippage Relationship

Plotting slippage against order-send latency for a month of live trading showed a clear pattern:

Latency bucket    Avg slippage    Rejection rate
<10ms             0.2 pips        3.1%
10–50ms           0.4 pips        6.8%
50–200ms          1.1 pips        14.2%
>200ms            2.8 pips        28.9%

The implication: the P&L impact of reducing pipeline latency from 50ms to 10ms is not just the latency improvement — it’s the reduction in slippage cost and rejection-induced retry latency on every trade. For a firm trading 50,000 trades per day at average size 2M EUR/USD:

  • Moving from 50ms to 10ms reduces average slippage by 0.7 pips per trade
  • At EUR/USD, 1 pip = 100 USD per 1M notional
  • 0.7 pip × 2M notional = 140 USD per trade
  • 50,000 trades/day × 140 USD = 7,000,000 USD/day improvement

This is why latency matters at trading firms. It’s not engineering purity — it’s direct revenue.