Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

Trade Markout是什么:如何衡量交易执行质量

了解trade markout的含义、正负号规则与三种参考价格,并通过一秒至五分钟的实际markout曲线分析逆向选择和执行质量。

交易完成后,在固定时间窗口内,中间价的变化称为 trade markout。计算从该笔成交在市场上打印的时刻开始。正值表示交易后市场朝有利于您的方向移动。负值表示市场走势对您不利,交易台称之为逆向选择。所选时间窗口可以是一秒,也可以是五分钟,它决定了这个指标回答的具体问题。

什么是 trade markout?

先看中间价:它是最佳买价与最佳卖价之间的中点,即NBBO的中间价。Markout取成交后指定时间窗口的中间价,减去参考价格,再根据交易方向确定正负。

**时间窗口 h 的 markout = side *(t+h 时的中间价 - 参考价格)**,其中,买入时 side 为正一,卖出时 side 为负一。

通常的混淆来自正负号的约定。买入后中间价下跌,markout为负:您买入后价格下跌,向您卖出的一方在这笔交易中处于更有利的位置。这就是用数字表示的逆向选择。卖出后中间价下跌,markout为正,因为符号会随交易方向翻转。每笔成交都有交易双方。一方的markout与另一方的markout符号相反。

三种参考价格

所减去的参考价格决定了markout衡量的内容。

  • 成交价。 Markout变成金额:比较 t+h 时头寸的价值与买入成本。您支付或赚取的spread包含在其中。
  • 成交时的中间价。 这种方法剔除spread,只衡量交易后市场如何变化,是观察逆向选择最直接的方法。
  • 您交易方向一侧的报价。 以您主动成交的报价,或您挂出的报价为基准,可以判断成交价相对于当时实际可得价格的位置。

执行质量分析通常用第二种方法衡量逆向选择,用第一种方法衡量损益。下面的第一个面板基于同一组成交,展示了两种结果。

Markout曲线及其形状含义

在多个时间窗口计算同一指标,就会得到一条曲线。曲线形状所包含的信息多于任何单个数值。

  • 曲线在最初一秒内快速下跌并持续处于低位,说明交易流被提前捕捉:成交后价格几乎立即朝不利方向移动。
  • 曲线持续数分钟下跌,说明交易流包含有关价格后续方向的真实信息。
  • 曲线在零附近持平,说明交易流影响有限。五分钟后,中间价与成交时基本相同。
  • 曲线先为负、随后回升至零附近,是被动做市商收取返佣时希望看到的形状。早期下探反映spread和即时冲击,随后回升则表示市场价格回归。

时间窗口的选择本身就是对测量对象的选择。小于一秒的markout主要反映延迟和排队位置。延长至一分钟后,它反映交易流中的短期信息。超过五分钟,通常的市场漂移开始掩盖成交本身的影响。

真实的逐笔成交Markout曲线

下方面板涵盖2026年6月10日常规交易时段的每一笔INTC成交。我们以成交前一秒的最后报价为基准,将每笔成交分类为买方主动或卖方主动,并计算其在5个时间窗口上的有符号markout平均值。两条序列基于完全相同的成交:一条以成交时的中间价为基准,另一条以成交价为基准。

查询INTC价差曲线,2026年6月10日,基于两个参考基准
每个数字背后的完整 SQL
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

900593笔分类成交中,以中间价为基准的序列在1 sec时为3.068个基点,在5 min时为2.977个基点。一个基点等于价格的百分之一,也就是百分之零点零一。因此,一笔成交的影响可能只是四舍五入误差,而一百万股的影响则可能代表实际资金。

以成交价为基准的序列起始值为-0.488个基点。两条序列在最短时间窗口上的差距,就是有效半点差:成交价偏离中间价的距离。主动成交方在买入时支付这部分成本,做市商则在卖出时获得这部分收益。第一个时间窗口之后的所有变化,都是市场价格的移动。

成交规模与曲线形状

逆向选择不会在所有成交中均匀出现。将同一交易时段按成交规模拆分,可以在同一坐标轴上比较小额成交与一千股及以上的大宗成交。

查询同一曲线按成交量拆分:小额成交对比大宗成交
每个数字背后的完整 SQL
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

5 min时间窗口内,不足一千股的成交平均为2.983个基点,而一千股及以上的11588笔成交平均为2.495个基点。相同总成交量由这两类成交完成后,最终的日终结果并不相同。

Markout如何成为执行报告中的spread数据

Markout是标准spread分解中缺失的一项。有效spread是主动成交方实际支付的成本,即中间价到成交价的有符号距离乘以二。将其拆成两部分:一部分是实现spread,即市场价格移动后做市商保留的部分;另一部分是逆向选择,即以中间价为基准的markout乘以二。两者之间的关系是精确的:有效spread = 实现spread + 逆向选择。

查询有效价差拆分为实现价差和逆向选择,按半小时
每个数字背后的完整 SQL
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

在东部时间09:30开始的半小时内,有效spread平均为10.651个基点。其中,8.313个基点是在六十秒时间窗口下的逆向选择部分,2.338个基点是做市商保留的部分。该面板涵盖13个半小时区间。

这些组成部分按交易场所汇总在Rule 605和606执行报告中,也是低于一美分报价规则和价格改善相关价格改善声明背后的计算依据。返佣是按每股固定金额发放的抵扣。逆向选择则是实际获得成交的报价所承担的每股变动成本。将两者放在经过测量的曲线上进行比较,而不是只看费率表中的 headline rate,正是检验maker taker费用和返佣的方法;这也与做市商为何亏损中的机制相互补充。

这些数字的计算方法

每个面板都使用2026年6月10日常规交易时段、东部时间09:30至16:00的INTC报价和成交数据。报价按秒合并,每秒只保留该秒内最后一次打印的报价。若成交价高于前一秒的中间价,则将其分类为买方主动;若低于前一秒的中间价,则分类为卖方主动,这是标准的报价分类规则。成交价恰好等于该中间价的成交不进行推断,而是从样本中剔除,因此中间价成交不在样本内。使用前一秒的中间价作为参考,可以避免参考价格提前看到未来信息。时间窗口按成交所在秒起算的完整秒数计算。只有目标秒存在报价时,该笔成交才会进入相应时间窗口。

常见问题

交易中的markout是什么?

Markout是成交后固定时间窗口内中间价变化的有符号值。正值表示成交后市场朝有利于您的交易方向移动。负值表示市场朝相反方向移动,这就是逆向选择的定义。

负markout是否总是意味着成交不佳?

不是。可成交订单在一秒时间窗口下的负markout,主要反映该订单为立即成交而支付的spread。这是即时执行的成本。更重要的是,曲线在更长时间窗口内是否继续下跌,还是趋于平稳。

Markout与滑点有什么区别?

滑点将成交价与决策价或到达价比较,因此评价的是整个订单执行过程。Markout将成交价与成交后一段时间的中间价比较,因此评价的是成交后发生的变化。前者评价母订单,后者评价单笔成交。

Markout应使用哪个时间窗口?

这取决于成交目的。小于一秒的时间窗口用于评价对延迟敏感的执行;一分钟通常是衡量流动性较高证券逆向选择的默认窗口。只有将时间窗口与数值一并披露,不同markout才具有可比性。

Markout与有效spread有什么关系?

有效spread可以分解为实现spread和逆向选择部分,而逆向选择部分等于以中间价为基准的markout乘以二。改变时间窗口后,两部分的分配也会改变。因此,公布spread统计数据时,如果不说明时间窗口,信息并不完整。


本文每个面板都附有生成数据的SQL。打开任意一个面板,替换ticker或日期,即可在Strasmore终端上从头重建同一条markout曲线。