Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

Czym jest markout transakcji i jak mierzyć jakość?

Wyjaśnienie konwencji znaków w analizie markout oraz prezentacja krzywej dla horyzontów od jednej sekundy do pięciu minut. Analiza jakości egzekucji zleceń w ujęciu ilościowym.

Czym jest markout transakcji?

Punktem wyjścia jest cena mid: punkt środkowy między najlepszą ofertą kupna (best bid) a najlepszą ofertą sprzedaży (best offer), czyli środek NBBO. Markout oblicza się, biorąc cenę mid w wybranym horyzoncie czasowym po wykonaniu zlecenia, odejmując od niej cenę odniesienia i nadając wynikowi znak zależny od strony transakcji.

**markout w horyzoncie h = strona * (mid w t+h - cena odniesienia)**, gdzie strona wynosi plus jeden w przypadku kupna i minus jeden w przypadku sprzedaży.

Konwencja znaków jest zazwyczaj źródłem nieporozumień. Kupno, po którym następuje spadek ceny mid, daje ujemny markout: kupiono aktywa, cena spadła, a sprzedający uzyskał lepsze warunki transakcji. Jest to tzw. selekcja negatywna (adverse selection) wyrażona liczbowo. Sprzedaż, po której cena mid spada, daje dodatni markout, ponieważ znak zmienia się wraz ze stroną transakcji. Każde wykonanie zlecenia ma dwie strony, a markout jednej strony jest markoutem drugiej strony ze zmienionym znakiem.

Trzy ceny odniesienia

Cena odniesienia, którą odejmujemy, decyduje o tym, co mierzy markout.

  • Cena wykonania. Markout staje się wartością pieniężną: określa, ile warta jest pozycja w momencie t+h w stosunku do zapłaconej kwoty. Zawiera w sobie spread, który został zapłacony lub zarobiony.
  • Cena mid w momencie wykonania. Eliminuje to wpływ spreadu i pozwala wyizolować ruch rynku po transakcji, co stanowi najbardziej precyzyjny wskaźnik selekcji negatywnej.
  • Kwotowanie po stronie, po której zawarto transakcję. Pomiar względem poziomu, po którym dokonano transakcji lub wystawiono zlecenie, pozwala ocenić wykonanie w odniesieniu do ceny, która była faktycznie dostępna.

Analiza jakości egzekucji opiera się na drugim wariancie w przypadku selekcji negatywnej oraz na pierwszym w przypadku zysków i strat. Oba warianty przedstawiono w pierwszym panelu poniżej dla określonego zbioru transakcji.

Krzywa markoutu i znaczenie jej kształtu

Wykonanie tej samej statystyki dla wielu horyzontów czasowych pozwala wykreślić krzywą. Jej kształt niesie więcej informacji niż pojedyncza liczba.

  • Krzywa, która gwałtownie spada w ciągu pierwszej sekundy i pozostaje na niskim poziomie, opisuje przepływ zleceń, które są „wybierane” (picked off): cena zmienia się na niekorzyść wykonania niemal natychmiast.
  • Krzywa, która spada przez kolejne minuty, opisuje przepływ niosący istotne informacje o kierunku ruchu cen.
  • Płaska krzywa w pobliżu zera opisuje neutralny przepływ. Po pięciu minutach cena mid wygląda tak samo, jak w momencie wykonania.
  • Krzywa, która zaczyna się od wartości ujemnej i wrasta w kierunku zera, jest typowa dla pasywnego animatora rynku (market maker) pobierającego rabaty. Początkowy spadek to spread plus natychmiastowy wpływ rynkowy, a powrót do zera to stabilizacja rynku.

Wybór horyzontu jest decyzją o tym, co dokładnie mierzymy. Poniżej sekundy markout odczytuje głównie opóźnienia (latency) i pozycję w kolejce. W horyzoncie minuty odczytuje krótkoterminowe informacje zawarte w przepływie zleceń. Powyżej pięciu minut zwykły dryf rynkowy zaczyna przeważać nad samym wykonaniem zlecenia.

Rzeczywista krzywa markoutu, transakcja po transakcji

Poniższy panel uwzględnia każdy print (wykonaną transakcję) dla INTC z sesji regularnej z dnia 10 czerwca 2026 r., klasyfikuje każdą z nich jako inicjowaną kupnem lub sprzedażą względem ostatniego kwotowania z sekundy poprzedzającej transakcję i uśrednia markout w horyzontach 5. Dwie serie dotyczą tych samych transakcji: jedna mierzona od ceny mid w momencie wykonania, druga od ceny wykonania.

ZapytanieKrzywa markout dla INTC, 10 czerwca 2026 r., wyznaczona na podstawie dwóch punktów odniesienia
Dokładny kod SQL dla każdej liczby
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

Dla 900593 sklasyfikowanych transakcji seria oparta na mid wynosi 3.068 punktów bazowych w 1 sec oraz 2.977 punktów bazowych w 5 min. Punkt bazowy to jedna setna procenta ceny, więc pojedyncza transakcja przesuwa cenę o błąd zaokrąglenia, a milion akcji przesuwa realny kapitał.

Seria oparta na cenie wykonania otwiera się na poziomie -0.488 punktów bazowych. Odległość między dwiema seriami w najkrótszym horyzoncie to efektywny pół-spread: odległość, w jakiej print znalazł się od ceny mid. Taker płaci to przy wejściu, a maker zarabia. Wszystko powyżej pierwszego horyzontu to ruch rynku.

Wielkość zlecenia a kształt krzywej

Selekcja negatywna nie rozkłada się równomiernie na wszystkie transakcje. Podział tej samej sesji według wielkości printu pozwala zestawić małe zlecenia oraz bloki o wielkości tysiąca akcji lub więcej na tych samych osiach.

ZapytanieTa sama krzywa z podziałem na wielkość zlecenia: transakcje drobne a bloki
Dokładny kod SQL dla każdej liczby
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

W horyzoncie 5 min printy poniżej tysiąca akcji osiągają średnio 2.983 punktów bazowych, podczas gdy 11588 printów o wielkości tysiąca akcji lub więcej osiąga średnio 2.495. Dwa kwotowania realizujące ten sam łączny wolumen wobec tych dwóch grup nie kończą dnia w tym samym miejscu.

Jak markouty stają się spreadami w raportach z egzekucji

Markout jest brakującym elementem w standardowej dekompozycji spreadu. Efektywny spread to to, co taker faktycznie zapłacił: dwukrotność odległości od ceny mid do ceny printu. Dzielimy to na dwie części. Jedną jest zrealizowany spread, czyli to, co zatrzymuje maker po ruchu rynku. Drugą jest selekcja negatywna, czyli markout oparty na mid pomnożony przez dwa. Tożsamość jest dokładna: efektywny spread = zrealizowany spread + selekcja negatywna.

ZapytanieDekompozycja spreadu efektywnego na spread zrealizowany i selekcję negatywną, w ujęciu półgodzinnym
Dokładny kod SQL dla każdej liczby
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

W półgodzinnym bloku rozpoczynającym się o 09:30 czasu wschodniego, efektywny spread wynosi średnio 10.651 punktów bazowych, z czego 8.313 to składnik selekcji negatywnej w horyzoncie sześćdziesięciu sekund, a 2.338 to zysk makera. Panel obejmuje 13 półgodzinnych przedziałów.

Te składniki to wielkości podsumowywane dla każdego miejsca obrotu w raportach z egzekucji Rule 605 i 606 i stanowią one podstawę arytmetyczną dla twierdzeń o poprawie ceny (price improvement), które wiążą się z zasadą sub-penny i poprawą ceny. Rabat jest stałym kredytem na akcję. Selekcja negatywna jest zmiennym kosztem na akcję dla transakcji, które faktycznie otrzymuje kwotowanie. Ważenie jednego względem drugiego na podstawie zmierzonej krzywej, zamiast na podstawie cennika, jest weryfikacją stojącą za opłatami i rabatami typu maker-taker i wiąże się z mechaniką opisaną w dlaczego animatorzy rynku tracą pieniądze.

Jak obliczono te liczby

Każdy panel odczytuje kwotowania i printy INTC z sesji regularnej z dnia 10 czerwca 2026 r., od 09:30 do 16:00 czasu wschodniego. Kwotowania są sprowadzane do jednej ceny mid na sekundę (ostatnia cena wewnątrz tej sekundy). Print jest klasyfikowany jako inicjowany kupnem, gdy znalazł się powyżej ceny mid z poprzedniej sekundy, i jako inicjowany sprzedażą, gdy znalazł się poniżej (zgodnie ze standardową regułą kwotowania). Transakcje zawarte dokładnie po cenie mid są pomijane, więc egzekucje po cenie środkowej znajdują się poza próbką. Użycie ceny mid z poprzedniej sekundy jako odniesienia zapobiega „zaglądaniu w przyszłość”. Horyzonty to pełne sekundy liczone od sekundy printu, a transakcja wchodzi do horyzontu tylko wtedy, gdy w docelowej sekundzie istnieje kwotowanie.

FAQ

Czym jest markout w tradingu?

Markout to podpisana zmiana ceny mid w ustalonym horyzoncie czasowym po wykonaniu transakcji. Wartość dodatnia oznacza, że rynek poruszył się w kierunku transakcji po jej wykonaniu. Wartość ujemna oznacza, że poruszył się w przeciwnym kierunku, co jest definicją selekcji negatywnej.

Czy ujemny markout zawsze oznacza złe wykonanie?

Nie. Ujemny markout w horyzoncie jednej sekundy dla zlecenia rynkowego to w większości spread, który zlecenie to zapłaciło – jest to koszt natychmiastowej realizacji. Ważniejsze jest to, czy krzywa nadal spada w dłuższych horyzontach, czy się wypłaszcza.

Jaka jest różnica między markoutem a slippage?

Slippage porównuje wykonanie z ceną decyzji lub ceną wejścia, oceniając całą ścieżkę zlecenia. Markout porównuje wykonanie z ceną mid w horyzoncie po wykonaniu, oceniając to, co wydarzyło się później. Jedno ocenia zlecenie nadrzędne, drugie ocenia pojedynczą transakcję.

Jakiego horyzontu powinien używać markout?

Zależy to od celu transakcji. Horyzonty poniżej sekundy oceniają egzekucję wrażliwą na opóźnienia, podczas gdy jedna minuta jest standardem dla selekcji negatywnej w przypadku płynnych aktywów. Publikowanie horyzontu obok liczby sprawia, że dwa markouty są w ogóle porównywalne.

Jak markout odnosi się do efektywnego spreadu?

Efektywny spread dzieli się na zrealizowany spread oraz składnik selekcji negatywnej, a ten składnik to markout oparty na mid pomnożony przez dwa. Zmiana horyzontu powoduje zmianę podziału między tymi dwiema wartościami, dlatego opublikowana statystyka spreadu jest niekompletna bez podania horyzontu.


Każdy panel tutaj jest dostarczany wraz z kodem SQL, który go wygenerował. Wystarczy otworzyć panel, zmienić ticker lub datę, a ta sama krzywa markoutu zostanie odtworzona od podstaw na terminalu Strasmore.