Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

Trade Markouts Explained: Execution Quality

What is a trade markout? The sign convention explained in plain English, plus a real markout curve measured at horizons from 1 second out to 5 minutes.

A trade markout is the change in the mid price over a fixed horizon after a fill, measured from the moment that fill printed. Positive means the market moved in your favor after you traded. Negative means it moved against you, which is what desks call adverse selection. The horizon you pick, one second or five minutes, decides which question the number is answering.

What is a trade markout?

Start with the mid price: the halfway point between the best bid and the best offer, the midpoint of the NBBO. A markout takes that mid at a chosen horizon after your fill, subtracts a reference price, and signs the result by the side you traded.

**markout at horizon h = side * (mid at t+h - reference price)**, where side is plus one if you bought and minus one if you sold.

The sign convention is where the confusion usually sits. A buy that is followed by the mid falling has a negative markout: you bought, the price went down, and whoever sold to you got the better of that trade. That is adverse selection written as a number. A sell followed by the mid falling has a positive markout, since the sign flips with the side. Every fill has two sides, and one side's markout is the other side's with the sign reversed.

The three reference prices

The reference price you subtract decides what the markout measures.

  • The fill price. The markout becomes money: what the position is worth at t+h against what you paid. The spread you paid or earned sits inside it.
  • The mid at the moment of the fill. This strips the spread out and isolates how the market moved after you traded, which is the cleanest read on adverse selection.
  • The quote on the side you traded. Measuring against the touch you took, or the touch you posted, answers how the fill compares with the price that was actually available.

Execution quality work leans on the second for adverse selection and on the first for profit and loss. Both appear in the first panel below, over one set of fills.

The markout curve and what its shape means

Run the same statistic at many horizons and you get a curve. Its shape carries more than any single number.

  • A curve that drops steeply inside the first second and stays down describes flow getting picked off: the price moves against the fill almost immediately.
  • A curve that keeps falling for minutes describes flow that carried real information about where the price was going.
  • A flat curve near zero describes benign flow. Five minutes on, the mid looks like it did at the fill.
  • A curve that starts negative and climbs back toward zero is the shape a passive maker collecting rebates is looking for. The early dip is the spread plus immediate impact, and the recovery is the market coming back.

Horizon choice is itself a decision about what you are measuring. Under a second, a markout mostly reads latency and queue position. Stretch it to a minute and it reads the short term information in the flow. Past five minutes, ordinary market drift starts to swamp the fill itself.

A real markout curve, fill by fill

The panel below takes every INTC print from the June 10, 2026 regular session, classifies each one as buy initiated or sell initiated against the last quote of the second before it printed, and averages the signed markout at 5 horizons. Two series run over identical fills: one measured from the mid at the fill, one from the fill price.

QueryINTC markout curve, June 10 2026, measured from two reference bases
The exact SQL behind every number
WITH
    mid_by_second AS
    (
        SELECT
            dateDiff('second', toDateTime('2026-06-10 13:30:00', 'UTC'), sip_timestamp) AS sec,
            argMax((toFloat64(bid_price) + toFloat64(ask_price)) / 2, sip_timestamp)    AS mid
        FROM global_markets.cache_stocks_quotes
        WHERE ticker = 'INTC'
          AND sip_timestamp >= toDateTime('2026-06-10 13:30:00', 'UTC')
          AND sip_timestamp <  toDateTime('2026-06-10 20:00:00', 'UTC')
          AND bid_price > 0
          AND ask_price > bid_price
        GROUP BY sec
    ),
    signed_fills AS
    (
        SELECT
            t.sec        AS sec,
            t.fill_price AS fill_price,
            q.mid        AS ref_mid,
            if(t.fill_price > q.mid, 1, -1) AS side
        FROM
        (
            SELECT
                dateDiff('second', toDateTime('2026-06-10 13:30:00', 'UTC'), sip_timestamp) AS sec,
                sec - 1          AS ref_sec,
                toFloat64(price) AS fill_price
            FROM global_markets.stocks_trades
            WHERE ticker = 'INTC'
              AND sip_timestamp >= toDateTime('2026-06-10 13:30:01', 'UTC')
              AND sip_timestamp <  toDateTime('2026-06-10 19:55:00', 'UTC')
              AND price > 0
              AND size > 0
        ) AS t
        INNER JOIN mid_by_second AS q ON q.sec = t.ref_sec
        WHERE t.fill_price != q.mid
    )
SELECT
    multiIf(f.horizon_s < 60,
            concat(toString(f.horizon_s), ' sec'),
            concat(toString(intDiv(f.horizon_s, 60)), ' min'))              AS horizon,
    round(avg(f.side * (fut.mid - f.ref_mid) / f.ref_mid) * 10000, 3)       AS mid_basis_bps,
    round(avg(f.side * (fut.mid - f.fill_price) / f.fill_price) * 10000, 3) AS fill_basis_bps,
    count()                                                                 AS fill_count
FROM
(
    SELECT
        sec,
        fill_price,
        ref_mid,
        side,
        horizon_s,
        sec + horizon_s AS future_sec
    FROM signed_fills
    ARRAY JOIN [1, 5, 15, 60, 300] AS horizon_s
) AS f
INNER JOIN mid_by_second AS fut ON fut.sec = f.future_sec
GROUP BY f.horizon_s
ORDER BY f.horizon_s
Run this yourself

Across 900593 classified fills, the mid basis series reads 3.068 basis points at 1 sec and 2.977 basis points at 5 min. A basis point is one hundredth of one percent of the price, so one fill moves a rounding error and a million shares moves real money.

The fill basis series opens at -0.488 basis points. The distance between the two series at the shortest horizon is the effective half spread: how far the print landed from the mid. A taker pays that on entry and a maker earns it. Everything past the first horizon is the market moving.

Fill size and the shape of the curve

Adverse selection does not land evenly across fills. Splitting the same session by print size puts small fills and blocks of a thousand shares or more on the same axes.

QueryThe same curve, split by print size: small fills against blocks
The exact SQL behind every number
WITH
    mid_by_second AS
    (
        SELECT
            dateDiff('second', toDateTime('2026-06-10 13:30:00', 'UTC'), sip_timestamp) AS sec,
            argMax((toFloat64(bid_price) + toFloat64(ask_price)) / 2, sip_timestamp)    AS mid
        FROM global_markets.cache_stocks_quotes
        WHERE ticker = 'INTC'
          AND sip_timestamp >= toDateTime('2026-06-10 13:30:00', 'UTC')
          AND sip_timestamp <  toDateTime('2026-06-10 20:00:00', 'UTC')
          AND bid_price > 0
          AND ask_price > bid_price
        GROUP BY sec
    ),
    signed_fills AS
    (
        SELECT
            t.sec        AS sec,
            t.fill_size  AS fill_size,
            q.mid        AS ref_mid,
            if(t.fill_price > q.mid, 1, -1) AS side
        FROM
        (
            SELECT
                dateDiff('second', toDateTime('2026-06-10 13:30:00', 'UTC'), sip_timestamp) AS sec,
                sec - 1          AS ref_sec,
                toFloat64(price) AS fill_price,
                size             AS fill_size
            FROM global_markets.stocks_trades
            WHERE ticker = 'INTC'
              AND sip_timestamp >= toDateTime('2026-06-10 13:30:01', 'UTC')
              AND sip_timestamp <  toDateTime('2026-06-10 19:55:00', 'UTC')
              AND price > 0
              AND size > 0
        ) AS t
        INNER JOIN mid_by_second AS q ON q.sec = t.ref_sec
        WHERE t.fill_price != q.mid
    )
SELECT
    multiIf(f.horizon_s < 60,
            concat(toString(f.horizon_s), ' sec'),
            concat(toString(intDiv(f.horizon_s, 60)), ' min'))                                   AS horizon,
    round(avgIf(f.side * (fut.mid - f.ref_mid) / f.ref_mid, f.fill_size < 1000) * 10000, 3)       AS small_fill_bps,
    round(avgIf(f.side * (fut.mid - f.ref_mid) / f.ref_mid, f.fill_size >= 1000) * 10000, 3)      AS block_fill_bps,
    countIf(f.fill_size >= 1000)                                                                 AS block_fill_count
FROM
(
    SELECT
        sec,
        fill_size,
        ref_mid,
        side,
        horizon_s,
        sec + horizon_s AS future_sec
    FROM signed_fills
    ARRAY JOIN [1, 5, 15, 60, 300] AS horizon_s
) AS f
INNER JOIN mid_by_second AS fut ON fut.sec = f.future_sec
GROUP BY f.horizon_s
HAVING countIf(f.fill_size < 1000) > 0
   AND countIf(f.fill_size >= 1000) > 0
ORDER BY f.horizon_s
Run this yourself

At the 5 min horizon, prints under a thousand shares average 2.983 basis points, while the 11588 prints of a thousand shares or more average 2.495. Two quotes filling the same total volume against those two groups do not finish the day in the same place.

How markouts become the spread numbers in execution reports

The markout is the missing term in the standard spread decomposition. The effective spread is what a taker actually paid: twice the signed distance from the mid to the print. Split that in two. One piece is the realized spread, what a maker keeps once the market has moved. The other piece is the adverse selection, which is the mid basis markout multiplied by two. The identity is exact: effective spread = realized spread + adverse selection.

QueryEffective spread split into realized spread and adverse selection, by half hour
The exact SQL behind every number
WITH
    mid_by_second AS
    (
        SELECT
            dateDiff('second', toDateTime('2026-06-10 13:30:00', 'UTC'), sip_timestamp) AS sec,
            argMax((toFloat64(bid_price) + toFloat64(ask_price)) / 2, sip_timestamp)    AS mid
        FROM global_markets.cache_stocks_quotes
        WHERE ticker = 'INTC'
          AND sip_timestamp >= toDateTime('2026-06-10 13:30:00', 'UTC')
          AND sip_timestamp <  toDateTime('2026-06-10 20:00:00', 'UTC')
          AND bid_price > 0
          AND ask_price > bid_price
        GROUP BY sec
    ),
    signed_fills AS
    (
        SELECT
            t.sec + 60   AS future_sec,
            t.et_time    AS et_time,
            t.fill_price AS fill_price,
            q.mid        AS ref_mid,
            if(t.fill_price > q.mid, 1, -1) AS side
        FROM
        (
            SELECT
                dateDiff('second', toDateTime('2026-06-10 13:30:00', 'UTC'), sip_timestamp) AS sec,
                sec - 1          AS ref_sec,
                toFloat64(price) AS fill_price,
                formatDateTime(toStartOfInterval(toTimeZone(sip_timestamp, 'America/New_York'), toIntervalMinute(30)), '%H:%i') AS et_time
            FROM global_markets.stocks_trades
            WHERE ticker = 'INTC'
              AND sip_timestamp >= toDateTime('2026-06-10 13:30:01', 'UTC')
              AND sip_timestamp <  toDateTime('2026-06-10 19:55:00', 'UTC')
              AND price > 0
              AND size > 0
        ) AS t
        INNER JOIN mid_by_second AS q ON q.sec = t.ref_sec
        WHERE t.fill_price != q.mid
    )
SELECT
    f.et_time                                                                  AS et_time,
    round(avg(2 * f.side * (f.fill_price - f.ref_mid) / f.ref_mid) * 10000, 3) AS effective_spread_bps,
    round(avg(2 * f.side * (f.fill_price - fut.mid) / f.ref_mid) * 10000, 3)   AS realized_spread_bps,
    round(avg(2 * f.side * (fut.mid - f.ref_mid) / f.ref_mid) * 10000, 3)      AS adverse_selection_bps,
    count()                                                                    AS fill_count
FROM signed_fills AS f
INNER JOIN mid_by_second AS fut ON fut.sec = f.future_sec
GROUP BY f.et_time
ORDER BY f.et_time
Run this yourself

In the half hour beginning 09:30 Eastern, the effective spread averages 10.651 basis points, of which 8.313 is the adverse selection term at a sixty second horizon and 2.338 is what a maker retains. The panel covers 13 half hour buckets.

Those components are the quantities summarized venue by venue in Rule 605 and 606 execution reports, and they are the arithmetic behind the price improvement claims that ride on the sub penny rule and price improvement. A rebate is a fixed credit per share. Adverse selection is a variable cost per share on the fills a quote actually receives. Weighing one against the other over a measured curve, rather than over a headline rate card, is the check behind maker taker fees and rebates, and it sits alongside the mechanics in why market makers lose money.

How these numbers were computed

Every panel reads INTC quotes and prints from the June 10, 2026 regular session, 09:30 to 16:00 Eastern. Quotes collapse to one mid per second, the last one printed inside that second. A print is classified buy initiated when it landed above the mid of the previous second and sell initiated when it landed below, which is the standard quote rule. Prints sitting exactly on that mid are dropped rather than guessed at, so midpoint executions sit outside the sample. Using the previous second's mid as the reference keeps the reference from peeking forward. Horizons are whole seconds counted from the second of the print, and a fill enters a horizon only when a quote exists in the target second.

FAQ

What is a markout in trading?

A markout is the signed change in the mid price over a fixed horizon after a fill. Positive means the market moved in the direction of your trade after you got filled. Negative means it moved the other way, which is the definition of adverse selection.

Is a negative markout always a bad fill?

No. A negative markout at one second on a marketable order is mostly the spread that order paid, which is the price of getting done immediately. What matters more is whether the curve keeps falling at longer horizons or flattens out.

What is the difference between a markout and slippage?

Slippage compares a fill against a decision price or an arrival price, so it grades the whole path of an order. A markout compares a fill against the mid at a horizon after the fill, so it grades what happened next. One scores the parent order, the other scores the individual fill.

Which horizon should a markout use?

It depends on what the fill was for. Sub second horizons grade latency sensitive execution, while one minute is the common default for adverse selection on liquid names. Publishing the horizon next to the number is what makes two markouts comparable at all.

How does a markout relate to the effective spread?

The effective spread splits into the realized spread plus an adverse selection component, and that component is the mid basis markout multiplied by two. Change the horizon and the split between the two moves with it, which is why a published spread statistic is incomplete without its horizon attached.


Every panel here ships with the SQL that produced it. Open one, swap the ticker or the date, and the same markout curve rebuilds from scratch on the Strasmore terminal.