← К списку статей
July 20, 2026
5 мин. чтения

Implementation shortfall и самодельный TCA: как измерить, сколько реально стоит исполнение

Implementation shortfall и самодельный TCA: как измерить, сколько реально стоит исполнение
#execution
#tca
#implementation-shortfall
#markouts
#transaction-costs
#algotrading
#backtest
#market-microstructure

У каждого институционального деска есть пайплайн анализа транзакционных издержек (TCA). Почти ни у кого, кто гоняет крипто-бота, его нет. Типичная схема: логируются сделки, суммируются комиссии, а разница между PnL бэктеста и PnL в реальной торговле называется "slippage" — единственный необъясненный остаток, в который сваливаются задержка, спред, импакт, неблагоприятный отбор (adverse selection) и все неисполненные заявки, по которым цена ушла. Издержку, измеряемую одним числом, невозможно исправить. Механизм для правильного измерения существует с 1988 года, он не проприетарный, и поверх собственных логов сделок это примерно 200 строк Python. В этой статье мы его строим.

Выигрыш — не более красивый дашборд. В вашем бэктесте есть модель издержек: константа slippage, вероятность исполнения, коэффициент импакта — и каждый параметр в ней сейчас угадан. TCA поверх реальных сделок — единственная точка опоры, относительно которой эти параметры можно откалибровать. Сторону симуляции этого контура мы построили в статье Симуляция исполнения: лестница от фантазий цены закрытия к реальности с учетом очереди; эта статья строит сторону измерения.

Бумага против реальности: что на самом деле измерял Перольд

Основополагающий прием принадлежит Андре Перольду (1988, "The Implementation Shortfall: Paper Versus Reality," Journal of Portfolio Management 14(3), 4-9). Запускаем два портфеля параллельно. Бумажный портфель исполняет каждое решение мгновенно, в неограниченном объеме, без издержек, по цене, действующей в момент принятия решения. Реальный портфель — это то, что бот сделал на самом деле: частичные исполнения, погоня за котировками, отмена остатков, комиссии. Implementation shortfall — это разница в их доходности.

Это определение важно именно тем, что оно отказывается что-либо прятать. Отчет по комиссиям показывает комиссии. Отчет "исполнение против лимитной цены" не показывает вообще ничего (вы никогда не исполняетесь хуже своего лимита — по построению). Бумажный портфель выставляет счет за все: дрейф между решением и выходом на рынок, пересеченный спред, вызванный импакт и — что критично — заявки, которые так и не исполнились, пока цена уходила. Вагнер и Эдвардс (1993, "Best Execution," Financial Analysts Journal 49(1), 65-71) назвали видимые комиссии верхушкой айсберга; для всего, что имеет оборот, доминирует подводная часть.

Зафиксируем обозначения. Родительская заявка: сторона s{+1,1}s \in \{+1, -1\} (покупка/продажа), объем XX. Цена решения PdP_d — это mid, который стратегия видела в момент срабатывания сигнала. Исполнения приходят как (pj,xj)(p_j, x_j) с суммарным исполненным объемом x=jxjXx = \sum_j x_j \le X. На горизонте TT (родительская заявка завершена, отменена или истек таймаут) mid равен PTP_T. Явные комиссии — FF. Implementation shortfall в денежном выражении:

IS=sjxj(pjPd)издержки исполнения  +  s(Xx)(PTPd)издержки упущенной возможности  +  F,\mathrm{IS} = \underbrace{s \sum_j x_j\,(p_j - P_d)}_{\text{издержки исполнения}} \;+\; \underbrace{s\,(X - x)(P_T - P_d)}_{\text{издержки упущенной возможности}} \;+\; F,

нормализуется в базисные пункты делением на бумажный номинал XPdX \cdot P_d. Положительное значение означает, что вы заплатили. Первое слагаемое — сколько стоили ваши исполнения относительно бумажного портфеля; второе — издержки упущенной возможности по Перольду: неисполненный остаток, оцененный по цене, до которой она убежала; третье — единственная часть, которую признает выписка биржи.

Замечание про терминологию: индустрия говорит "arrival price" (цена выхода на рынок), и в большинстве equity TCA arrival означает mid в момент, когда заявка достигла рынка. Для бота момент принятия решения и момент выхода на рынок различаются на величину собственной внутренней задержки плюс ожидание в очереди из-за rate limit — это реальная, измеримая издержка. Поэтому мы держим оба таймстампа и обе цены и позволяем декомпозиции разделить их.

Декомпозиция: задержка, импакт, тайминг, упущенная возможность, комиссии

Одно число IS говорит вам, что исполнение дорогое. Оно не говорит, почему, а способы исправления для разных компонентов совершенно разные — задержку и издержки импакта не решить одним и тем же изменением. Расширенный implementation shortfall Роберта Кисселла (Kissell, 2006, "The Expanded Implementation Shortfall: Understanding Transaction Cost Components," Journal of Trading 1(3), 6-16; развернуто изложено в The Science of Algorithmic Trading and Portfolio Management, Academic Press, 2013) разбивает итог на компоненты, каждый из которых относится к отдельной стадии жизненного цикла заявки. Практическая версия с использованием arrival mid P0P_0 (mid в момент первого подтверждения биржей):

IS=sX(P0Pd)задержка  +  sjxj(pjP0)издержки торговли  +  s(Xx)(PTP0)упущенная возможность  +  F.\mathrm{IS} = \underbrace{s\,X\,(P_0 - P_d)}_{\text{задержка}} \;+\; \underbrace{s \sum_j x_j\,(p_j - P_0)}_{\text{издержки торговли}} \;+\; \underbrace{s\,(X - x)(P_T - P_0)}_{\text{упущенная возможность}} \;+\; F.

Тождество раскрывается точно до определения Перольда — раскройте слагаемые, и P0P_0 сократится. У каждой части свой владелец:

Издержки задержки sX(P0Pd)sX(P_0 - P_d): дрейф цены между срабатыванием сигнала и выходом первой дочерней заявки на биржу в статус live. Это ваша инфраструктура — сериализация, сеть, очереди rate limit, риск-проверки. Если у сигнала есть настоящее альфа на коротком горизонте, издержки задержки — это то место, где оно утекает первым; устойчиво положительное среднее говорит о том, что рынок движется в вашу сторону еще до того, как вы выходите на рынок — затухающее моментум-альфа, либо кто-то более быстрый торгует тот же сигнал.

Издержки торговли sjxj(pjP0)s\sum_j x_j(p_j - P_0): во что обошлись исполнения относительно arrival — пересеченный спред плюс рыночный импакт плюс дрейф внутри расписания исполнения. Это оценка работы алгоритма исполнения, и именно эта величина реально моделируется в исследованиях по execution. Альмгрен, Тум, Хауптманн и Ли (2005, "Direct Estimation of Equity Market Impact," Risk 18(7), 58-62) измерили ее примерно на 700 000 заявках по американским акциям с десков Citigroup (декабрь 2001 - июнь 2003) и обнаружили, что издержки торговли масштабируются с дневной волатильностью и долей участия в объеме — временный импакт следует степенному закону от темпа торговли с показателем около 3/5, постоянный импакт близок к линейному по размеру. Мы повторно используем эту функциональную форму при калибровке.

Тайминг-риск: не слагаемое в декомпозиции среднего, а дисперсия вокруг него. Растягивание родительской заявки во времени снижает ожидаемый импакт, но подвергает вас волатильности; для расписания с оставшейся позицией q(t)q(t) стандартное отклонение издержек масштабируется как σ0Tq(t)2dt\sigma \sqrt{\int_0^T q(t)^2\,dt}. Это ровно тот компромисс, который оптимизирует фреймворк Альмгрена-Криса. В вашем TCA-отчете это проявляется как дисперсия IS по родительским заявкам — приводите стандартное отклонение рядом с каждым средним, иначе все внимание достанется среднему, а все ваши деньги — хвостам.

Издержки упущенной возможности s(Xx)(PTP0)s(X - x)(P_T - P_0): неисполненный объем, оцененный по терминальной цене. Для пассивных стратегий это регулярно самый крупный и наименее изученный компонент, и именно это слагаемое делает весь фреймворк честным — подробнее в разделе про ловушки, потому что его пропуск — самый распространенный способ обмануть себя с помощью TCA.

Комиссии FF: явная часть. В крипте логируйте комиссии после скидок (VIP-уровни, токен-ребейты, конвертированные по цене на момент исполнения) и сохраняйте знак мейкер-ребейтов — отрицательная комиссия это данные, а не шум.

Разобранный пример

Покупка X=10X = 10 BTC. Сигнал срабатывает при Pd=60,000P_d = 60{,}000; бумажный номинал $600,000. Первая дочерняя заявка подтверждена при mid P0=60,012P_0 = 60{,}012. За следующие две минуты 8 BTC исполняются по VWAP 60,072; цена уходит в тренд, алгоритм соблюдает свой лимит, оставшиеся 2 BTC отменяются при mid PT=60,240P_T = 60{,}240. Смешанная комиссия 2.5 базисных пункта от исполненного номинала.

Компонент Формула USD б.п. от бумажного
Задержка 10×(60,01260,000)10 \times (60{,}012 - 60{,}000) 120 2.0
Издержки торговли 8×(60,07260,012)8 \times (60{,}072 - 60{,}012) 480 8.0
Упущенная возможность 2×(60,24060,012)2 \times (60{,}240 - 60{,}012) 456 7.6
Комиссии 8×60,072×2.5e-48 \times 60{,}072 \times 2.5\text{e-}4 120 2.0
Итого IS 1,176 19.6

Выписка биржи показывает $120. Реальная стоимость исполнения решения составила $1,176 — в десять раз больше, причем два крупнейших компонента невидимы для учета на основе комиссий. Примерно 40% этой суммы пришлось на объем, который так и не был торгован. TCA-отчет, анализирующий только исполненные сделки, оценил бы эту родительскую заявку в 8 б.п. и назвал бы все в порядке.

Waterfall-декомпозиция implementation shortfall от бумажного портфеля к реальному портфелю

Markout: оценка неблагоприятного отбора

Implementation shortfall оценивает родительскую заявку. Он ничего не говорит о качестве отдельных исполнений — конкретно о том, систематически ли вы торгуете с контрагентами, которые знают что-то, чего не знаете вы. Это измеряется markout: каждое исполнение оценивается по mid на фиксированном горизонте после самого события.

πτ=s(mtf+τpf)mtf×104 б.п.,\pi_\tau = \frac{s\,(m_{t_f + \tau} - p_f)}{m_{t_f}} \times 10^4 \ \text{б.п.},

где pfp_f — цена исполнения в момент времени tft_f, а mtm_t — mid в момент времени tt. Это удельный mark-to-market PnL исполнения на горизонте τ\tau, и у него чистая анатомия при τ=0\tau = 0: мейкерское исполнение начинается с ++половины спреда (вы купили по бид, mid выше вас); тейкерское исполнение начинается с -половины спреда. То, что происходит по мере роста τ\tau, — это информационное содержание сделки:

  • τ=1\tau = 1с выделяет снайпинг и подбор устаревших котировок (stale-quote pickoff). Если ваши мейкерские исполнения уже в минусе через секунду после исполнения, значит более быстрые участники бьют по вашим котировкам в момент, когда те становятся неверно оцененными — ваш цикл обновления котировок медленнее их цикла триггера. Этот markout — диагностика задержки, а не диагностика стратегии.
  • τ=10\tau = 10с измеряет классический неблагоприятный отбор: исполнения, за которыми следует продолжение движения через вашу цену. Для маркет-мейкера это издержки, которые должен перекрыть захват спреда; микроструктура акций называет родственную величину realized spread, институционализированную в отчетности по SEC Rule 605 на горизонте 5 минут. Крипта движется быстрее; эквивалентный диапазон — 10-60с.
  • τ=60\tau = 60с показывает, несут ли исполнения моментум против вас за пределами горизонта микроструктуры — для тейкерских стратегий это вопрос, превышает ли альфа сигнала на 60с спред плюс импакт, заплаченные при входе. Тейкерская markout-кривая, начинающаяся с 4-4 б.п. и никогда не пересекающая ноль, — это стратегия, которая платит за входы, которые ее альфа не способна окупить.

Асимметрия мейкер/тейкер — это вся экономика пассивной торговли, уместившаяся в две кривые. Мейкерская кривая, начинающаяся с +2+2 б.п. (половина спреда) и затухающая до 1-1 б.п. к 60с, говорит: вы захватываете спред и отдаете больше, и никакой уровень ребейта это не исправит. Та же кривая, устанавливающаяся на +0.7+0.7 б.п., говорит, что котировочный движок окупает себя. Это же ровно то, что наивные бэктесты с исполнением по касанию (touch-fill) просто игнорируют — симулятор, исполняющий вас всякий раз, когда цена касается лимита, игнорирует тот факт, что быть исполненным коррелирует с тем, что вы оказались неправы, поэтому уровню с учетом очереди в лестнице симуляции исполнения нужны измеренные markout в качестве входных данных, а не как допущение.

Markout-кривые в зависимости от горизонта для мейкерских и тейкерских исполнений, показывающие затухание неблагоприятного отбора

Вычисление — это merge_asof поверх собственного потока mid:

import pandas as pd

def markouts(fills: pd.DataFrame, mids: pd.DataFrame,
             horizons=("1s", "10s", "60s")) -> pd.DataFrame:
    """fills: [ts, price, qty, side, liquidity]; mids: [ts, mid].
    Both UTC-indexed and sorted. Mid stream must be from YOUR captured
    feed, not candles reconstructed later."""
    fills = fills.sort_values("ts").reset_index(drop=True)
    mids  = mids.sort_values("ts")
    out = fills.copy()
    for h in horizons:
        probe = fills[["ts"]].copy()
        probe["ts"] = probe["ts"] + pd.Timedelta(h)
        m = pd.merge_asof(probe, mids, on="ts", direction="backward")
        out[f"mo_{h}"] = (fills["side"] * (m["mid"].values - fills["price"])
                          / fills["price"] * 1e4)
    return out

def markout_report(mo: pd.DataFrame) -> pd.DataFrame:
    """Qty-weighted markouts by liquidity flag. Weighting matters:
    a 0.001 BTC fill and a 2 BTC fill are not equal evidence."""
    cols = [c for c in mo.columns if c.startswith("mo_")]
    def agg(g):
        w = g["qty"] / g["qty"].sum()
        return pd.Series({c: (g[c] * w).sum() for c in cols}
                         | {"n": len(g), "qty": g["qty"].sum()})
    return mo.groupby("liquidity").apply(agg)

Разрежьте отчет дальше по символу, часу дня и дистанции котировки от mid. Самый практически полезный срез для мейкерской стратегии — markout по позиции в очереди на момент исполнения: исполнения в начале свежей очереди ценятся совершенно иначе, чем исполнения, где уровень был пробит насквозь через вас.

Пайплайн: что логировать

TCA умирает на уровне логирования, а не на уровне математики. Вышеописанной математике нужны числа, которые большинство ботов выбрасывает, и ни одно из них потом не восстановить из истории биржи. Обязательные требования:

  • Mid на момент решения из собственного потока данных, в момент срабатывания сигнала. Не свеча биржи, не последующая реконструкция. Бенчмарк — это "цена, в которую верила стратегия, когда принимала решение" — только собственный процесс в этот момент это знает.
  • Оба таймстампа: момент решения и момент первого подтверждения, иначе издержки задержки неизмеримы и незаметно сливаются с издержками торговли.
  • Каждое исполнение с биржевым таймстампом, комиссией и флагом мейкер/тейкер — время получения на вашей стороне загрязнено собственной входящей задержкой.
  • Отмененные и истекшие родительские заявки, логируемые наравне со всем остальным. Заявки с нулевым исполнением — самые дорогие строки в таблице.
  • Постоянный поток mid (или L1-поток) с гранулярностью 100-250мс, хранящийся достаточно долго для вычисления markout. Если вы уже записываете стаканы для симулятора исполнения, это бесплатно.

Достаточно двух плоских таблиц:

PARENTS = {
    "parent_id":    "str",
    "strategy":     "str",
    "symbol":       "str",
    "venue":        "str",
    "algo":         "str",        # twap | pov | sniper | quote | ...
    "side":         "int8",       # +1 buy, -1 sell
    "qty":          "float64",    # parent size, base units
    "limit_px":     "float64",    # NaN for unconstrained
    "decision_ts":  "datetime64[ns, UTC]",
    "decision_mid": "float64",    # mid your feed showed at decision_ts
    "arrival_ts":   "datetime64[ns, UTC]",   # first exchange ack
    "arrival_mid":  "float64",
    "end_ts":       "datetime64[ns, UTC]",   # filled / cancelled / expired
    "terminal_mid": "float64",
    "mkt_volume":   "float64",    # market volume over [arrival_ts, end_ts]
    "sigma_bps":    "float64",    # realized vol estimate at decision time
}

FILLS = {
    "parent_id": "str",
    "ts":        "datetime64[ns, UTC]",  # exchange timestamp
    "price":     "float64",
    "qty":       "float64",
    "fee":       "float64",      # quote ccy, post-discount, signed
    "liquidity": "str",          # maker | taker
    "venue":     "str",
}

Функция атрибуции — это прямая транскрипция декомпозиции:

import numpy as np

def is_decomposition(p: pd.Series, fills: pd.DataFrame) -> dict:
    """Expanded implementation shortfall for one parent, bps of paper
    notional. Sign convention: positive = cost."""
    s, X = p["side"], p["qty"]
    paper = X * p["decision_mid"]
    x = fills["qty"].sum()
    delay = s * X * (p["arrival_mid"] - p["decision_mid"])
    trade = s * ((fills["price"] - p["arrival_mid"]) * fills["qty"]).sum()
    oppty = s * (X - x) * (p["terminal_mid"] - p["arrival_mid"])
    fees  = fills["fee"].sum()
    bps   = lambda v: 1e4 * v / paper
    return {
        "parent_id": p["parent_id"],
        "delay_bps": bps(delay), "trade_bps": bps(trade),
        "oppty_bps": bps(oppty), "fees_bps":  bps(fees),
        "is_bps":    bps(delay + trade + oppty + fees),
        "fill_ratio": x / X,
        "participation": x / max(p["mkt_volume"], x),
    }

def tca_table(parents: pd.DataFrame, fills: pd.DataFrame) -> pd.DataFrame:
    fg = dict(tuple(fills.groupby("parent_id")))
    empty = fills.iloc[0:0]
    rows = [is_decomposition(p, fg.get(p["parent_id"], empty))
            for _, p in parents.iterrows()]
    return parents.merge(pd.DataFrame(rows), on="parent_id")

И запросы атрибуции — это то место, где TCA перестает быть бухгалтерией и становится исследованием:

tca = tca_table(parents, fills)

def wavg(g: pd.DataFrame, col: str) -> float:
    w = g["qty"] * g["decision_mid"]          # notional weights
    return (g[col] * w).sum() / w.sum()

comp = ["delay_bps", "trade_bps", "oppty_bps", "fees_bps", "is_bps"]
by_strat = tca.groupby("strategy").apply(
    lambda g: pd.Series({c: wavg(g, c) for c in comp}
                        | {"is_std": g["is_bps"].std(), "n": len(g)}))

done = tca[tca["fill_ratio"] > 0.99]
done["pov_bin"] = pd.qcut(done["participation"], 6)
impact_curve = done.groupby("pov_bin").apply(lambda g: wavg(g, "trade_bps"))

tca["hour"] = tca["arrival_ts"].dt.hour
by_hour = tca.groupby("hour").apply(lambda g: wavg(g, "is_bps"))

by_venue = tca.groupby("venue").apply(
    lambda g: pd.Series({"is_bps": wavg(g, "is_bps"),
                         "is_std": g["is_bps"].std(),
                         "fill_ratio": g["fill_ratio"].mean(), "n": len(g)}))

Запрос 4 — это входная таблица, которую оптимизирует умный роутер заявок — маршрутизация без TCA по площадкам — это маршрутизация по расписанию комиссий, то есть по наименьшему компоненту издержек (Умная маршрутизация заявок в крипте берет эту таблицу как отправную точку). Вместе с модулем markout это и есть обещанные ~200 строк.

Замыкание контура: калибровка модели издержек бэктеста

Вот здесь пайплайн окупает себя. Ваш бэктест содержит утверждения: slippage_bps = 5, кривую вероятности исполнения, коэффициент импакта. Каждое из них — это утверждение о реальном исполнении, а TCA-таблица и есть реальное исполнение. Контур: измерить с помощью TCA, подобрать модель издержек, прогнать бэктест с подобранной моделью, торговать, измерить заново.

Замкнутый контур калибровки от реальных исполнений через TCA-атрибуцию к модели издержек бэктеста

Для тейкерских издержек позаимствуем функциональную форму у Альмгрена и др. (2005), а не изобретаем свою. Их результат — издержки пропорциональны дневной волатильности, умноженной на степень доли участия в объеме — дает двухпараметрическую модель:

trade_bpsσbps=a(xV)b,\frac{\text{trade\_bps}}{\sigma_{\text{bps}}} = a \cdot \left(\frac{x}{V}\right)^{b},

с оценкой для акций b0.6b \approx 0.6 как априорным значением. Подбирайте ее на усредненных по бинам данных, а не на сырых родительских заявках — издержки отдельных родительских заявок доминируются шумом (это и есть слагаемое тайминг-риска), и лог-логарифмическая регрессия на сырых данных с удовольствием подгонится под шум:

done = tca[(tca["fill_ratio"] > 0.99) & (tca["participation"] > 0)]
done["norm_cost"] = done["trade_bps"] / done["sigma_bps"]
bins = done.groupby(pd.qcut(done["participation"], 8)).agg(
    pov=("participation", "mean"), cost=("norm_cost", "mean"))
bins = bins[bins["cost"] > 0]                # can't log a negative bin
b, log_a = np.polyfit(np.log(bins["pov"]), np.log(bins["cost"]), 1)

def taker_cost_bps(participation, sigma_bps, a=np.exp(log_a), b=b):
    return a * sigma_bps * participation ** b

А затем будьте честны насчет доверительных интервалов. У команды Альмгрена было 700 000 заявок, и они все равно указали показатель временного импакта как 0.600±0.0380.600 \pm 0.038; бот с 2 000 родительскими заявками не может позволить себе свободно оценивать bb по каждому символу отдельно. Практичный режим: объединяйте данные по символам после нормализации на σ\sigma, сильно стягивайте bb к 0.6 (или просто зафиксируйте его и подбирайте только aa), пересчитывайте ежемесячно. Если подобранное aa дрейфует вверх, значит ваш след на рынке вырос или рынок обмелел — в любом случае бэктесту нужно было об этом знать.

Для мейкерских стратегий цели калибровки другие и приходят из двух других модулей:

  • Вероятность исполнения: модель очереди симулятора исполнения предсказывает P(исполнение)P(\text{исполнение}) по ячейкам (дистанция от спреда, позиция в очереди); лог родительских заявок дает реализованные доли исполнения по этим же ячейкам. Расхождение — это баг симулятора с привязкой к конкретной ячейке сетки.
  • Неблагоприятный отбор: замените неявное допущение симулятора "исполнения взаимозаменяемы" на измеренную таблицу markout — симулированное мейкерское исполнение на дистанции dd в час hh несет измеренный π10s(d,h)\pi_{10s}(d, h) как немедленную mark-to-market скидку. Это единственное изменение и есть разница между мейкерским бэктестом, который галлюцинирует, и тем, который отслеживает реальность; это был недостающий вход калибровки, отмеченный в статье про симуляцию исполнения.
  • Дисперсия издержек: подавайте в бэктест распределение IS, а не его среднее. Модель издержек, сдвигающая только среднее, не способна воспроизвести просадки, которые создает тайминг-риск; даже логнормальное распределение, подобранное по IS отдельных родительских заявок, лучше константы.

Полное рассмотрение кривых slippage — функциональные формы, обусловленность режимом, где ломается степенной закон — это отдельная статья: Кривые slippage и модели издержек для бэктестов. Мысль здесь архитектурная: модели из той статьи невозможно подобрать без таблиц из этой.

Ловушки

Литература по TCA существует десятилетиями, и столько же существуют способы ее обмануть. Три режима отказа объясняют большинство случаев самообмана.

Игра с бенчмарком. Любой бенчмарк, кроме arrival price, можно "обнять". Классика — VWAP: алгоритм, оцениваемый против интервального VWAP, может отслеживать его с точностью до базисного пункта, пока позиция теряет двадцать пунктов относительно arrival, потому что бенчмарк дрейфует вместе с ценой, которую вы же и толкаете — а при значимой доле участия ваши же принты и есть VWAP, так что отслеживание его — это самопроверка домашнего задания. Политику бенчмарков мы разобрали в статье TWAP против VWAP против POV; правило со стороны TCA проще: бенчмарки выбираются до начала торговли, и IS относительно arrival вычисляется всегда, даже когда планировщик оценивается по своему собственному бенчмарку расписания. Более тонкий вариант — игра с arrival: если компонент, устанавливающий decision_ts, может видеть краткосрочный моментум, он может выбирать момент "решения" так, чтобы приукрасить слагаемое задержки. Таймстампы решений принадлежат слою сигнала и логируются до запуска какой-либо логики исполнения.

Смещение выжившего в анализе только исполненных сделок. Обусловьте анализ издержек только исполнениями, и пассивное исполнение начнет казаться бесплатным. Конкретно: 100 пассивных заявок на покупку, на один тик ниже mid. Шестьдесят исполняются и, будучи пассивными, исполняются по ценам в среднем на 3 б.п. лучше arrival: измеренная "издержка" 3-3 б.п., отчет, которым можно гордиться. Сорок неисполненных — это как раз те, где цена ушла вверх; оцените их на отмене как 25 б.п. неблагоприятного движения, и честное число — 0.6×(3)+0.4×25=+8.20.6 \times (-3) + 0.4 \times 25 = +8.2 б.п. Отчет только по исполненным и честный отчет различаются на 11 б.п. и по знаку. Это не краевой случай — это сам механизм пассивной торговли: получение исполнения коррелирует с тем, что цена проходит сквозь вас, и это то же самое обусловливание, которое превращает бэктесты с исполнением по касанию в фантазию. Издержки упущенной возможности — не опциональное уточнение IS; это слагаемое, которое защищает все измерение от смещения отбора. У этого же смещения есть тейкерский вариант: IOC-заявки, которые промахнулись, заявки, отклоненные rate limit или риск-проверками — если промахи не логируются, издержки промаха неизмеримы, а они наибольшие именно на быстрых рынках, где стратегия больше всего хотела эту сделку.

Разные мелкие ловушки, коротко: arrival mid, реконструированный из биржевых свечей (ваш поток и свеча расходятся именно тогда, когда это важно); усреднение б.п. по родительским заявкам без весов по номиналу (тысяча пылевых исполнений перевешивает одну настоящую заявку); комиссии, залогированные до скидки; markout-mid, взятый с другой площадки, чем исполнение (кросс-венью базис, маскирующийся под неблагоприятный отбор); а на перпетуалах — начисление funding, просачивающееся в окно исполнения — funding это издержка, но не издержка исполнения, и их смешивание отравляет оба анализа.

Что сделать на этой неделе

Добавьте две таблицы логов в своего бота — схема выше это список колонок, а не проект. Ничего не восстанавливайте задним числом; две недели честных логов лучше года реконструкций. Запустите декомпозицию и три горизонта markout. Вы узнаете, какой компонент доминирует (почти никогда не комиссии), подвергаются ли ваши пассивные исполнения неблагоприятному отбору сверх захвата спреда, и насколько далеко константа издержек в бэктесте отстоит от измеренной кривой. Затем встройте подобранную кривую обратно в симулятор и перезапустите бэктест, который изначально сказал вам запускать эту стратегию. Бумажный портфель Перольда спустя тридцать восемь лет остается единственным честным противником, который есть у вашего реального.

Дисклеймер: Информация в этой статье предоставлена исключительно в образовательных и ознакомительных целях и не является финансовым, инвестиционным или торговым советом. Торговля криптовалютами сопряжена с высоким риском убытков.

Авторы

Eugen Soloviov
Eugen Soloviov

Инженер торговых систем

Разработка торговых ботов с 2017 года: межбиржевой арбитраж (подключал до 30 бирж), парный арбитраж на коинтеграции между спотом и фьючерсами, скальпинг, фронтраннинг, торговля по новостям, сентиментный анализ, трендовые алгоритмы, а также алгоритмы управления и балансировки портфелей. Делает выставление ордеров до 1 мс, warehouse для big data, бэктестинг-движки, AI-агентов и интерфейсы для ботов (в т.ч. open-source profitmaker.cc). Стек: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, архитектура.

Newsletter

Будьте в курсе событий

Подпишитесь на нашу рассылку, чтобы получать эксклюзивную аналитику по AI-трейдингу и обновления платформы.

Мы уважаем вашу конфиденциальность. Отписаться можно в любой момент.