Кривые проскальзывания вместо констант: модели издержек, которые выживают при столкновении с реальной торговлей
Самая дорогая строка в большинстве бэктестов — это slippage_bps = 5. Не потому что 5 — неверное число: усредненное по году исполнений, оно вполне может быть верным, — а потому что константа неверна условно, а ваш процесс отбора стратегий — это машина для поиска и эксплуатации условных ошибок. Проскальзывание — функция размера ордера, спреда, волатильности и доступной ликвидности. Константа — это значение такой функции в одной точке, экстраполированное на все остальные, включая ликвидационный каскад в три часа ночи, когда ваш сигнал возврата к среднему срабатывает сильнее всего, а глубина стакана составляет одну десятую от обычной. Бэктест и там спишет 5 bps. Реальная торговля — нет.
Эта статья — о модели издержек внутри цикла бэктеста: о функции cost(size, market_state), которая вычитается из каждой симулируемой сделки. Речь намеренно не идет о прогнозировании воздействия ex ante для планирования исполнения — это смежная задача, рассмотренная со стороны модели в статье про моделирование ценового воздействия и со стороны оптимального планирования в статье про Алмгрен-Крисс. Здесь вопрос уже, но для тех, кто отбирает стратегии по бэктесту, — более насущный: какая функция должна стоять на месте вашей текущей константы, как ее подогнать по данным, которые у вас реально есть, и насколько сильно ответ меняет то, какую стратегию вы в итоге запустите. У последней части есть четкий эмпирический ответ: переранжируйте 20 вариантов одного семейства стратегий под четырьмя моделями издержек — и увидите, как таблица лидеров переворачивается.
Лестница моделей издержек

Как и лестница симуляции исполнений, модели издержек образуют лестницу достоверности, где каждая ступень требует больше данных и устраняет одно конкретное систематическое искажение. Ступени накопительны: каждая сохраняет члены предыдущей и добавляет один свой.
M0: константа в bps
Одно число, применяемое к каждой сделке. Три режима отказа, в порядке нарастания ущерба:
- Слеп к размеру. Клип на 5 млн платят одинаково. Любое сравнение стратегий по разным уровням емкости лишено смысла.
- Слеп к режиму. Спокойный вторничный день и коллапс FTX обходятся одинаково. Эта ошибка — не шум; она антикоррелирована ровно с теми состояниями рынка, где событийно-ориентированные стратегии концентрируют свою торговлю.
- Токсичен для отбора. Когда вы перебираете 200 комбинаций параметров, оптимизатор обнаруживает, что при плоских издержках оборот дешев относительно реальности, и смещает весь поиск в сторону высокочастотных вариантов. Ошибка в издержках превращается в направленную силу при отборе моделей, а не в симметричный шум для PnL.
M1: пропорционально спреду
Списываем измеренный половинный спред на момент принятия решения (плюс комиссию, которая детерминирована и всегда должна быть отдельной строкой). Это первая модель, которая вообще реагирует на рынок: спреды расширяются на новостях, на открытии сессий, в тонких альткоинах, поэтому издержки теперь сонаправленно движутся хотя бы с одной переменной ликвидности. Чего ей все еще не хватает: спред отражает цену исполнения одного клипа по цене лучшей котировки. Все, что крупнее, проходит через стакан, и это движение для невидимо.
M2: масштабирование по волатильности
Добавляем член, пропорциональный краткосрочной волатильности. Обоснование одновременно эмпирическое и структурное: спреды и глубину задают маркетмейкеры, чей риск неблагоприятного отбора и инвентарный риск масштабируются с (логика котирования Авеллянеды-Стоикова в обратную сторону), поэтому стоимость запроса ликвидности естественным образом выражается в единицах волатильности. Когда у вас вообще нет данных по стакану — только дневные бары, длинная история — с порядка 0,05–0,1 от дневной волатильности уже намного лучше любой константы, потому что она хотя бы "дышит" вместе с режимом рынка.
M3: квадратный корень от доли участия
Член размера. — размер вашего ордера, — объем рынка за релевантное окно, — волатильность за то же окно. Эта функциональная форма — не просто удобство; это один из наиболее многократно воспроизведенных эмпирических результатов в рыночной микроструктуре, и стоит точно разобраться, откуда он взялся.
Теоретическая отправная точка — Kyle (1985), "Continuous auctions and insider trading" (Econometrica 53(6), 1315–1335). В модели Кайла маркетмейкер устанавливает цену как линейную функцию чистого потока ордеров, , где
— воздействие на единицу потока пропорционально неопределенности стоимости актива относительно объема шумовой торговли. Лямбда Кайла дала области ее единицу измерения ликвидности (глубина равна ) и первое проверяемое утверждение: издержки на торгуемый доллар зависят от соотношения информации и ликвидности, а не от константы. Но модель линейна по размеру, а данные говорят об обратном.
Данные говорят о вогнутости. Almgren, Thum, Hauptmann and Li (2005), "Direct estimation of equity market impact" (Risk, июль 2005), подогнали модель примерно на 700 000 ордеров на акции США со столов Citigroup и обнаружили, что временное воздействие растет как доля участия в степени — они явно протестировали и отвергли и линейную зависимость, и чистый квадратный корень в пользу , — тогда как постоянное воздействие оказалось равным , статистически неотличимым от линейного (что обнадеживает, поскольку Huberman and Stanzl (2004) показали, что нелинейное постоянное воздействие допускает манипуляции). Tóth et al. (2011), "Anomalous price impact and the critical nature of liquidity in financial markets" (Physical Review X 1, 021006), используя почти 500 000 метаордеров Capital Fund Management на фьючерсных рынках, получили с порядка единицы — и дали теперь уже стандартное объяснение: видимый стакан — исчезающе малая доля скрытой ликвидности, которая локально линейна по цене вокруг мида, из-за чего стоимость его прохождения масштабируется как квадратный корень. Более поздние работы уточняют края: Zarinelli, Treccani, Farmer and Lillo (2015), "Beyond the square root" (Market Microstructure and Liquidity 1(2)), показали на семи миллионах институциональных метаордеров ANcerno, что квадратный корень хорошо описывает данные на протяжении примерно двух порядков величины размера ордера, а логарифмическая форма распространяется на пять; Bucci et al. (2019, Physical Review Letters 122, 108302) задокументировали переход от линейного (кайловского) воздействия при очень малой доле участия к квадратно-корневому выше этого уровня. А Frazzini, Israel and Moskowitz (2018), "Trading Costs" (SSRN 3229719), используя $1,7 трлн реальных исполнений AQR за 19 лет, обнаружили реализованные издержки на порядок ниже более ранних академических оценок — предупреждение о том, что коэффициенты, подогнанные по чужому потоку с иной срочностью и иным содержанием информации, не переносятся на ваш.
Для цикла бэктеста практический вывод сводится к трем утверждениям. Воздействие масштабируется с волатильностью. Воздействие вогнуто по размеру, с показателем степени, который стоит считать равным , пока ваши собственные данные не укажут на . Предэкспонента имеет порядок единицы, но варьируется по площадкам, активам и — что критично — по вашему собственному торговому стилю, поэтому следующий раздел посвящен подгонке, а не копированию. Глубокая калибровка (ядра затухания, разложение на транзитное и постоянное воздействие, кросс-воздействие) относится к статье о моделировании воздействия; бэктесту нужна только сама кривая.
Расчетный пример для порядка величины. Бессрочный фьючерс BTC, дневная волатильность bps, дневной объем площадки V_d = \6Y = 0.75\delta = 0.5$:
| Размер ордера | Член воздействия | + половина спреда (0,5 bp) | ||
|---|---|---|---|---|
| $50 тыс. | 0,0008% | 0,0029 | 0,5 bp | 1,0 bp |
| $500 тыс. | 0,008% | 0,0091 | 1,7 bp | 2,2 bp |
| $5 млн | 0,083% | 0,0289 | 5,4 bp | 5,9 bp |
| $50 млн | 0,83% | 0,0913 | 17,1 bp | 17,6 bp |
Три порядка величины в размере дают разброс издержек примерно в 18 раз. Ни одна константа не покрывает эту таблицу, и любое сравнение стратегий, варьирующее размер позиции при фиксированном slippage_bps, молча сравнивает яблоки при 5 млн.
Подгонка кривой: по своим исполнениям — или ничьим
По собственным исполнениям
Правильные данные — это ваш собственный TCA-журнал: одна строка на родительский ордер с мидом на момент прихода, реализованным VWAP исполнения, размером ордера и состоянием рынка на момент принятия решения — ровно тот набор данных, который измерение implementation shortfall выдает как побочный продукт. Shortfall в bps относительно прихода, нормированный на одновременную волатильность и регрессированный на долю участия, напрямую дает вам и .
Ловушка — шум. Shortfall одного ордера — это воздействие плюс ценовой шум за окно исполнения, и шум доминирует: для 10-минутного исполнения при дневной волатильности 250 bps член шума составляет bps против сигнала воздействия порядка 3 bps. Отношение сигнал/шум 0,15 на одно наблюдение — вот почему Almgren et al. понадобилось 700 тыс. ордеров и почему подгонять нужно по медианам бакетов, а не по сырым точкам:
import numpy as np
import pandas as pd
def fit_sqrt_curve(orders: pd.DataFrame) -> tuple[float, float]:
"""orders: one row per parent order.
is_bps -- implementation shortfall vs arrival mid, bps, cost-positive
sigma_bps -- volatility at arrival (same window used at simulation time)
q_over_v -- parent size / market volume over the execution window
Fits I = Y * sigma * (Q/V)^delta on participation-bucket medians."""
df = orders.query("q_over_v > 0 and sigma_bps > 0").copy()
df["i_norm"] = df.is_bps / df.sigma_bps # impact in vol units
df["bucket"] = pd.qcut(np.log10(df.q_over_v), 12, duplicates="drop")
b = (df.groupby("bucket", observed=True)
.agg(i=("i_norm", "median"), qv=("q_over_v", "median")))
b = b[b.i > 0] # noisy buckets can go negative; drop, don't clip
delta, log_y = np.polyfit(np.log(b.qv), np.log(b.i), 1)
return float(np.exp(log_y)), float(delta)
На нескольких тысячах криптородительских ордеров эта подгонка уже стабильна с точностью примерно ±30% по и ±0,1 по — грубо, но разница между подогнанной кривой и угаданной константой — это не 30%, это вся форма целиком. Переподгоняйте раз в квартал; дрейф сам по себе диагностичен (рост при постоянной доле участия означает либо изменение режима ликвидности площадки, либо то, что ваш поток ордеров стал более информированным/более заметным).
Холодный старт: исполнений еще нет
До первого реального ордера у вас есть только публичные данные и два честных варианта.
Пройти стакан по глубине. Усредните стоимость прохождения снимков L2 на каждом размере — точно так же, как это делает симулятор исполнений для отдельной сделки, но агрегированно в виде кривой. Это нижняя граница с известным знаком: снимок показывает ликвидность, которая пережила чужой поток, динамика восполнения невидима, а ваши собственные более ранние дочерние ордера успеют сдвинуть стакан до прихода более поздних.
def curve_from_book(snapshots, sizes_usd, safety=1.75):
"""snapshots: iterable of (mid, asks), asks = [(price, qty), ...] best-first.
Depth-walk cost is a LOWER bound on realized cost -- scale it."""
rows = []
for q_usd in sizes_usd:
costs = []
for mid, asks in snapshots:
rem, paid, got = q_usd / mid, 0.0, 0.0
for p, q in asks:
x = min(q, rem)
paid += x * (p - mid); got += x; rem -= x
if rem <= 0:
break
if rem <= 0:
costs.append(paid / got / mid * 1e4)
rows.append((q_usd, safety * float(np.median(costs))))
return pd.DataFrame(rows, columns=["q_usd", "cost_bps"])
Отследить публичные сделки. Разбейте агрессорские сделки по размеру, измерьте движение мида через несколько секунд после каждой и получите эмпирическую кривую воздействия. Это восстанавливает вогнутую форму и примерно верное масштабирование по , но уровень смещен в направлении, которое нельзя определить заранее: публичные принты — это решения других трейдеров, поэтому крупные принты условно информированы (что завышает измеренное воздействие относительно вашего неинформированного ребалансирования), тогда как самые чувствительные к ликвидности трейдеры дробят ордера именно для того, чтобы скрыться (что занижает его). Используйте это для формы, а не для уровня.
В любом случае протокол холодного старта таков: взять кривую по публичным данным, применить коэффициент безопасности 1,5–2, торговать мелко и начать заполнять TCA-таблицу, которая ее заменит. Публичная кривая — это строительные леса, а не несущая стена.
Зависимость от режима: тот же ордер, издержки в пять раз выше

Каждый член в зависит от состояния рынка, и во время всплеска волатильности все они одновременно движутся против вас. Спреды пропорциональны краткосрочной волатильности, поэтому расширяется в 5–10 раз. входит в член воздействия напрямую. Объем тоже растет — что наивно снижает долю участия, — но растет он медленнее, чем в числителе, и это компенсирующее смещение скрывает более неприятный эффект: котируемая глубина вблизи мида схлопывается намного быстрее торгуемого объема, поэтому реализованная кривая становится круче, чем предсказывает подогнанная формула по одним лишь стрессовым входным данным.
Конкретно, с подогнанной выше кривой, ордер на $3 млн BTC:
- Спокойный день: bps, спред 1 bp, V_d = \6= 0.5 + 0.75 \times 150 \times \sqrt{0.0005} = 0.5 + 2.5 = 3.0$ bps.
- Кризисный день (уровня LUNA, FTX): bps, спред 6 bps, V_d = \18= 3.0 + 0.75 \times 700 \times \sqrt{0.000167} = 3.0 + 6.8 = 9.8$ bps.
Формула дает рост в 3,3 раза. Реализованный TCA в стрессовые дни устойчиво оказывается выше формулы — это схлопывание глубины, которое член с квадратным корнем не видит, — и именно здесь уместен измеренный стресс-множитель. В наших исполнениях остаток по кризисному бакету составляет 1,4–1,8x поверх формулы со стрессовыми входными данными, доводя итоговые стрессовые издержки до 4,5–6x от спокойных для того же ордера. Это и есть честное происхождение утверждения "тот же ордер стоит в 5 раз дороже при всплеске волатильности": примерно 2x от за вычетом эффекта объема, 2x от спреда и остальное — от того, что глубина испаряется быстрее, чем допускает статистика объема.
Реализация — это условная модель издержек: подгоняйте кривую по режимным бакетам или, что эквивалентно, подгоняйте одну кривую плюс таблицу множителей, привязанную к квантилю реализованной волатильности, вычисленному на момент принятия решения (только скользящее окно назад — режимная метка, вычисленная по всей выборке, — это заглядывание вперед, контрабандой протащенное через модель издержек):
| Режим (квантиль скользящей волатильности за 1ч) | Частота | Множитель спреда | Множитель кривой (измеренный) |
|---|---|---|---|
| Спокойный (< p25) | 25% | 0,6x | 0,7x |
| Нормальный (p25–p75) | 50% | 1,0x | 1,0x |
| Повышенный (p75–p95) | 20% | 1,8x | 1,6x |
| Стресс (> p95) | 5% | 5–10x | 3–6x |
Frazzini, Israel and Moskowitz (2018) фиксируют то же явление в своих институциональных данных: реализованные издержки сильно сонаправленно движутся с текущей волатильностью, и модель издержек без члена, меняющегося во времени с волатильностью, ошибается ровно в хвостах. Причина, по которой эта таблица важнее, чем можно подумать, глядя на ее 5%-ную стрессовую строку, — корреляция: стратегии не выбирают эти строки равномерно. Система пробоя волатильности проводит большую часть своей торговли в двух нижних строках. Спокойный маркетмейкер зарабатывает в верхней строке и выбивается по стопам через нижнюю. Взвешивание сделок каждой стратегии по ее собственной режимной гистограмме — вместо начисления всем безусловного среднего — единственная по величине поправка PnL на всей этой странице для событийно-ориентированных систем, и она стоит больше, чем разница между любыми двумя соседними ступенями лестницы.
Эксперимент на чувствительность: четыре модели издержек, одна перетасованная таблица лидеров
Если бы модель издержек меняла PnL на постоянную величину, ничто из этого не имело бы значения для отбора — каждый вариант сдвинулся бы одинаково, и argmax уцелел бы. Но это не так, потому что чувствительность к издержкам различается между вариантами на порядок величины. Эксперимент, который делает это конкретным:
Постановка. Одно семейство стратегий — возврат к среднему по z-score и пробой Дончиана на бессрочных фьючерсах BTC и ETH, минутные бары, 18 месяцев — с перебором по горизонту удержания (15м / 1ч / 4ч / 24ч) и порогу входа: 20 вариантов, односторонний оборот от 0,4x до 11x книги в день, клипы по $250 тыс., исполняемые за 5 минут. Каждый вариант бэктестируется по одному разу под каждой моделью издержек:
- M0: только комиссии;
- M1: комиссии + константа 5 bps;
- M2: комиссии + половина спреда + ;
- M3: комиссии + подогнанная кривая , с таблицей режимных множителей выше.
Одни и те же исполнения, одни и те же сигналы, один и тот же путь кода — модель издержек передается в бэктестер как аргумент конструктора, как и любой полноценный параметр:
from dataclasses import dataclass
@dataclass(frozen=True)
class SqrtCost:
y: float = 0.75 # fitted impact coefficient
delta: float = 0.5 # fitted exponent
fee_bps: float = 2.5 # taker fee, always separate
def cost_bps(self, q_usd: float, st) -> float:
part = q_usd / st.window_volume_usd
return (self.fee_bps + st.stress_mult *
(st.spread_bps / 2 + self.y * st.sigma_bps * part ** self.delta))
for model in [ZeroCost(), ConstCost(5.0), VolCost(0.08), SqrtCost()]:
for variant in grid:
results[(model, variant)] = backtest(variant, cost_model=model)
Результаты (типичный прогон; годовая чистая доходность):
| Вариант | Оборот/день | M0 | M1 (5 bp) | M2 | M3 (кривая) | Ранг M1 → M3 |
|---|---|---|---|---|---|---|
| MR-15m, z=1.0 | 11,4x | +187% | +61% | +9% | −14% | 1 → 17 |
| MR-15m, z=1.5 | 7,9x | +141% | +52% | +15% | −2% | 2 → 13 |
| BO-1h, k=2 | 3,1x | +74% | +43% | +28% | +19% | 3 → 4 |
| MR-1h, z=2.0 | 2,4x | +58% | +38% | +25% | +21% | 4 → 2 |
| BO-4h, k=3 | 0,9x | +39% | +33% | +29% | +26% | 7 → 1 |

Ранговые корреляции (Kendall ) между таблицами лидеров: — константа почти ничего не переупорядочивает, это налог, а не модель. — шесть из первой десятки по M1 покидают первую десятку под M3, а чемпион по M1 переходит от +61% к отрицательному значению. — один только член волатильности восстанавливает большую часть переупорядочивания; член размера довершает дело для вариантов, торгующих крупно относительно объема окна. Механизм в точности повторяет токсичность отбора со ступени M0: плоские 5 bps субсидируют сделки, которые на самом деле стоят 15 bps (крупные, быстрые, в стрессе), и переоблагают те, что стоят 2 (мелкие, терпеливые, в спокойном режиме), поэтому ранжирование по M1 систематически продвигает варианты, чьи истинные издержки занижены сильнее всего. Оптимизатор нашел не альфу — он нашел член ошибки вашей модели издержек.
Именно поэтому модель издержек принадлежит той же ментальной категории, что и разбиение walk-forward или целевая функция: это полноценный параметр бэктеста, который вы варьируете, а не константа среды, которую задают один раз. Если ранг варианта стабилен между M2 и M3, его преимущество реально относительно издержек. Если его ранг рушится при подъеме по лестнице, значит вы узнали, что это был артефакт модели издержек — то же самое эпистемическое событие, что и стратегия, умирающая на вневыборочных данных в walk-forward оптимизации, и заслуживающая того же отклика. А если вы отобрали argmax из 200 вариантов под M1, применима логика дефлированного коэффициента Шарпа с поворотом: вы провели поиск с множественным тестированием, где сама тестовая статистика была смещена в пользу записей с высоким оборотом.
Честные границы: PnL как полоса значений
Финальная дисциплина следует из признания того, что модель издержек остается неопределенной даже после подгонки. У есть доверительный интервал, стресс-множители оценены по горстке хвостовых дней, а будущее выберет собственные режимы. Поэтому перестаньте отчитываться о чистом PnL как об одном числе. Отчитывайтесь о нем как о полосе значений по именованным сценариям издержек:
- Оптимистичный: нижняя граница по прохождению глубины стакана, без стресс-множителей. Никогда не отчитывайтесь по нему в одиночку; его единственная задача — ограничить лучший случай.
- Реалистичный: подогнанная кривая, подогнанные режимные множители — ваша центральная оценка.
- Пессимистичный: , спред , стрессовая строка всегда применяется на уровне не ниже своего значения p75. Грубо говоря: подогнанная кривая, вычисленная на неблагоприятном краю доверительного интервала каждого параметра.
Два правила принятия решений делают полосу операционной. Первое: распределяйте капитал по пессимистичному сценарию, отчитывайтесь по реалистичному. Стратегия, которая выживает только под реалистичной моделью, — это ставка на то, что в вашей подгонке издержек нет ошибки в дорогую сторону, — ставка, которую вы не делали осознанно. Второе: отслеживайте коэффициент хрупкости — ширину полосы, деленную на валовой PnL. Из эксперимента выше: BO-4h при обороте 0,9x показывает валовые +39%, полосу [+22%, +29%], хрупкость 0,44. MR-15m при обороте 11,4x показывает валовые +187%, полосу [−31%, +34%] — хрупкость 2,2, и знак стратегии переворачивается внутри полосы. У второй стратегии нет определимого знака при текущем уровне ваших знаний об издержках; честные утверждения здесь — "мы не знаем" и "снижайте оборот, пока не узнаете". Бэктест, который печатает +34% и опускает край полосы на −31%, не оптимистичен — он нефальсифицируем.
Полоса ничего не стоит в производстве — это еще три записи в свипе моделей издержек, который вы теперь и так проводите, — и она меняет разговоры. "Эта стратегия делает 20%" приглашает к вере. "Эта стратегия делает 11–19% в наших сценариях издержек и остается положительной при пессимистичном" приглашает к распределению капитала.
Куда это встраивается
В движке бэктеста модель издержек стоит ровно там же, где симулятор исполнений возвращает исполненное количество: исполнения определяют что вы получили, кривая издержек определяет сколько вы за это заплатили, а ступени выше L2 на этой лестнице (прохождение глубины, модели очереди) постепенно поглощают модель издержек в саму модель исполнения — кривая является параметрическим заместителем, который вы используете, когда не симулируете стакан напрямую. Параметры берутся из вашего собственного TCA-журнала, когда он есть, и из масштабированных с запасом безопасности публичных данных, когда его нет. Функциональная форма берется из сорока лет теории и измерений — Kyle (1985) объясняет, почему издержки на доллар вообще зависят от ликвидности, Almgren et al. (2005) и Tóth et al. (2011) дают сам вогнутый закон, а Алмгрен-Крисс выступает планировочным аналогом и нейросетевые модели воздействия — прогнозным передним краем. Что добавляет эта статья — так это рабочий процесс: подогнать кривую, обусловить ее режимом, свипать ее как параметр и отчитываться полосой. Ничто из этого не эффектно. Все это дешевле, чем узнать вживую, на какой стороне от slippage_bps = 5 на самом деле находилась ваша стратегия.
Авторы
Инженер торговых систем
Разработка торговых ботов с 2017 года: межбиржевой арбитраж (подключал до 30 бирж), парный арбитраж на коинтеграции между спотом и фьючерсами, скальпинг, фронтраннинг, торговля по новостям, сентиментный анализ, трендовые алгоритмы, а также алгоритмы управления и балансировки портфелей. Делает выставление ордеров до 1 мс, warehouse для big data, бэктестинг-движки, AI-агентов и интерфейсы для ботов (в т.ч. open-source profitmaker.cc). Стек: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, архитектура.