Функция называлась enter_pack. Это уже должно было насторожить. Исследовательский агент написал BTC кэш-энд-керри: когда бессрочный торговался дороже спота больше чем на восемь базисных пунктов, покупаем спот, шортим перп, сидим. Фандинг и базис сделают остальное. Бэктест исполнял обе ноги в ту же миллисекунду, по mid, плюс тик вежливости. Кривая капитала была лестницей. Захватывающим это никто бы не назвал. В том и была прелесть.
В понедельник мы повысили её до бумаги.
Понедельник: пакет, который всегда исполнялся
Первый сигнал пришёл в 11:17 UTC. Базис на экране — 9.4 б.п., перп в премии. В бэктесте enter_pack вернула два исполнения и нулевой остаток. На бумаге она вернула исполнение спота на 0.40 BTC по аску и пассивный шорт по перпу, который не исполнился.
Спот стоил 4 б.п. taker-комиссии и около 1 б.п. спреда. Цена перпа, которую мы хотели, ещё стояла в стакане, потом уже нет. Через двадцать секунд котируемый базис был 5 б.п., потому что мы только что сняли спот. Через сорок секунд перп исполнился, уже после движения. Мы были в пакете. Мы были в пакете по базису, который уже не окупал вход и выход.
Бэктест записал бы «+9.4 б.п. керри, остаток 0». В блоттере были 4 б.п. спотовой taker-стоимости, 40-секундный незахеджированный лонг и гэп входа около 5. У меня была выборка из одного наблюдения и всё равно слишком много уверенности в лестнице.
Вторник: мы открыли функцию
enter_pack принимала список ног и целевой mid. Она крутила цикл, исполняла каждую ногу по mid * (1 ± slip) и писала одну строку портфеля. Второго таймстемпа не было. Ветки, в которой второй ноги не существует, не было. Юнит-тест утверждал residual == 0.
Такой тест заставляет чувствовать себя взрослым. Он никогда не упадёт.
Мы разбили пакет на два ордера с собственным состоянием: выставлен, частичный, исполнен, отменён. Неполное стало объектом первого класса. В историческом реплее по-прежнему не было настоящего стакана, поэтому мы схитрили. Задержать второе исполнение на случайную величину из понедельничного ожидания (среднее 26 секунд, хвост мне не нравился) и сдвинуть его цену на то, что спот сделал за это окно. Грубо, но к обеду на лестнице выросли зарубки: пакеты, которые вошли по 3 б.п., пакеты, которые так и не стали пакетами.
Среда: три секунды — и сбрасываем
Если вторая нога не встала за три секунды, закрываем первую. С этого и надо было начинать. Звучит консервативно, пока не оценишь аборт. Закрытие значит: ты пересекал спот, чтобы войти, и пересекаешь его, чтобы выйти. Две taker-комиссии, два спреда, никакого керри, никакого фандинга. В переписанном реплее такие аборты не были редкостью. Они кучковались на барах, где базис выглядел особенно «настоящим», а это обычно те бары, где через спот уже что-то течёт.
К среде вечером у нас было семнадцать бумажных сигналов, с которыми можно было спорить.
| Бэктест, утро понедельника | Бумага, к среде | |
|---|---|---|
| Вход | Обе ноги по mid, один таймстемп | Спот по аску; перп через 5–40 с, или никогда |
| Остаток | Всегда 0 | Без хеджа на 10 из 17 сигналов |
| Комиссии на рабочей сделке | Два taker | Два taker, или четыре, если абортируем |
| Драйвер PnL | Базис + фандинг | То, что сделал спот, пока мы ждали |
У коллеги до сих пор есть ноутбук, который считает пакет листинговым продуктом, BTC_CARRY, с плоским «налогом на исполнение» в 2 б.п. Мне этот ноутбук нравился. Он выдавал ту же лестницу, что и агент. Налог — это среднее. Сироты — это распределение. Средние не сбивает в тонкий час; 0.40 BTC оставшегося спота — сбивает.
Четверг: остаток, потом кофе
Три секунды абортировали почти всё, стоило нам ещё и ждать в очереди. На бумаге ослабили до пятнадцати. В четверг в 02:11 UTC спот исполнился, перп — нет. Я всё ещё был в фазе горя «повисит, снимут», когда пошёл варить кофе.
02:11:08 исполнен BTCUSDT 0.40 покупка
02:11:08 выставлен BTCUSDT-PERP 0.40 продажа, post-only
02:11:41 всё ещё в работе
02:26:00 оценка остатка −18.1 б.п.
BTC просел на 18 б.п. в тихом окне. Остаток и был всей сделкой. Фандинг, когда напечатался, начислился на перп, которого у нас не было. Спот просто сидел лонгом. В овернайт-пути исходного бэктеста одноногой позиции никогда не было, поэтому он никогда не задавал вопрос, который мы задаём любой другой книге: какой худший бар может напечатать этот остаток и где стоп для позиции, которую мы не собирались держать?
Семь полных пакетов тоже не герои. К тому моменту, как напечаталась вторая нога, наш собственный съём спота уже надавил на индекс, и базис входа был на 3–5 б.п. хуже снимка, по которому сработал сигнал. Порог в восемь б.п. был числом про экран.
Чего бы мы не стали делать в следующий раз
Мы бы не выкатывали мультилег одной функцией исполнения. То же правило для керри перп-спот, календаря или вертикального колла, который агент выставляет двумя лимитками и потом празднует, когда обе «исполняются» по mid.
- Состояние ордера по каждой ноге, включая частичные. Пакет — родитель двух тикетов.
- Таймер максимального сиротства, который реально срабатывает, и путь закрытия, который платит два спреда на выходе.
- Mark-to-book по остатку, со стопом под оставшуюся ногу. Нетто пакета — неправильный номинал.
- Никакого зачтённого керри, никакого начисленного фандинга, пока обе ноги не встали. Неполное есть неполное.
- Запретить mid-on-mid по умолчанию для всего, у чего два тикета. Если уж гадать — гадай хуже на тонкой ноге и позже на втором исполнении.
И мы бы не пустили test_pack_is_atomic в репозиторий. Atomic — свойство матчинг-движка, а не вспомогательной функции. Спот BTC и бессрочный BTC — два стакана, которые иногда рифмуются. На бумаге они перестают рифмоваться, и только поэтому лестница вообще когда-либо была интересной.



