В районе 2020 года я написал робота, который закрывал за меня клиентские валютные сделки на Мосбирже. Схема оценки рынка у него была, прямо скажем, наивная: восемь метрик, пять состояний и лестница if-ов. Эта схема два года зарабатывала банку его копейки — и умерла не от наивности.

Откуда берутся копейки

Небольшой банк котирует клиентам доллар и евро за рубль. Маржа — пять копеек к рынку; курсы публичные, они писались на билбордах по городу, тут нет никакого секрета. У части клиентов льготы, и маржа усыхала до двух-трёх копеек. Таких сделок проходило больше сотни в день, и каждую надо было закрыть на бирже: клиент купил у нас валюту, мы откупаем её на рынке; наши копейки — это разница.

Я к тому моменту всю дилерскую жизнь просидел у стакана и эти сделки закрывал руками. Про систему учёта позиции, которую я писал себе всё это время, я уже рассказывал. Сегодня расскажу про её боевое продолжение.

Гонка на пару секунд

Устроено это так. Клиент в интернет-банке кликает на курс — и курс для него замораживается. Дальше он выбирает счета в выпадашках, подписывает — пара секунд, иногда больше. Всё это время рынок едет, а цена клиенту уже обещана.

Теперь цифрами. В момент клика рынок 60,10 за доллар, клиент получил 60,15. Согласился, подписал, сделка прилетела в систему. Робот снимает срез рынка: уже 60,16. А чтобы забрать свои пять копеек, откупить надо по 60,10, которых больше нет.

И вот развилка, которую я сто раз в день проходил головой:

  • поставить заявку по 60,10 и ждать. Если рынок пойдёт дальше вверх — после обеда, при курсе 61,50, бесславно закрывать позицию с настоящим убытком;

  • откупить прямо сейчас по 60,16. Микроубыток в копейку, забыли, едем дальше;

  • поставить на покупку по 60,01 и не особо рисковать — если рынок сегодня ездит широкой пилой, за пару часов она перепилит всё, что стоит у неё на пути.

Правильный ответ зависит от того, какой сегодня рынок. Дилер это чувствует спинным мозгом. Но дилер не может чувствовать спинным мозгом сто раз в день, у него ещё межбанк, касса и отчётность. Значит, спинной мозг надо записать.

Температура рынка

Записалось это в функцию с честным названием CurMarketStatStatus. Она берёт по каждой валютной паре одним запросом восемь метрик за окно (минимум, максимум, последнюю цену, среднюю, медиану, разницу открытие-закрытие, амплитуду и разброс) и сводит их в одно из пяти состояний. Легенда прямо в коде:

'0 - no data
'1 - strong sell
'2 - sell
'3 - flat
'4 - buy
'5 - strong buy

Логика — лестница условий. Разброс маленький и цена жмётся к средней — рынок спит, flat. Рынок вырос с открытия, цена выше средней, разброс приличный — buy; а если цена при этом прижата к дневному максимуму и не откатывает — strong buy, прёт. Вниз всё зеркально:

If open_close_diff > 0 And last_val > avg_val And stddev >= 0.05 Then
    If (max_val - last_val) / last_val >= 0.0006 Then
        CurMarketStatStatus = 4
    Else
        CurMarketStatStatus = 5
    End If
End If

Никакого машинного обучения, ни одной книжки по квант-финансам. Состояние 3 зовётся flat, потому что так говорят на деске, а писал это дилер.

Тридцать ячеек цены

Дальше работает функция GetFairPrice: по стороне сделки, положению рынка относительно неё и температуре она выбирает цену заявки. Две стороны, три ситуации, пять состояний: тридцать ячеек. Ситуации в коде подписаны как есть:

If market_rate >= deal_rate Then
    'тревога!!! продали дешевле скакнувшего рынка

Внутри каждой ячейки — базовая маржа или дельта с поправкой на копейку, две или пять в зависимости от температуры. Разбирать все тридцать не буду, принцип видно и так: когда рынок дует в нашу сторону, заявка становится жаднее; когда против — скромнее; когда прёт без отката — фиксируем, что дают, и не строим из себя героев. Эти поправки на копейку-две и есть оцифрованный спинной мозг: не формула из учебника, а «я в такой ситуации делал вот так».

Обвязка, на которой всё держится

Интеллекта тут, как видите, немного. Больше половины робота — обвязка, и она была важнее интеллекта. Котировки и сделки он брал из базы QUIK-шлюза, заявки ставил транзакционным файлом — старый добрый способ, когда в файл пишется строка «ММВБ-Валюта: Транзакции», а терминал её подхватывает. В логе робота осталась строчка «ждём разблокировки транзакции»: однопоточная жизнь во всей красе.

Отдельно робот знал расписание. В коде прямо в секундах от полуночи прошито: после 16:44 инструменты «сегодня» уже не берём, работаем только «завтра»; своповая связка закрывается в 17:15. Биржевая кухня — константами.

И ещё одну работу он забрал себе целиком: заявки, выставленные на «сегодня» и не успевшие исполниться, робот сам переносил на «завтра». До него это была отдельная ежедневная боль: помнить, что надо перетащить, не забыть перетащить, а когда заявок много — ещё и приличная гимнастика для мелкой моторики.

А на успешно закрытую сделку робот играл tada.WAV. Сотня сделок в день — сотня маленьких фанфар из динамика системного блока. Коллеги, кажется, ненавидели этот звук. Мне он нравился: каждая фанфара — забранные копейки.

Почему наивное работало

Схема профилирования — довольно наивная, я это говорю сам и раньше комментаторов. Но у её наивности была причина работать: она не пыталась предсказывать рынок. Она повторяла руки. У этих рук к тому моменту был двадцатилетний бэктест на живых деньгах, и лестница из пяти состояний — это просто самое компактное описание того, что руки уже умели. Пять состояний, не пятьдесят: спинной мозг тоньше не различает, и не надо.
И, кстати, работало все просто отлично. После внедрения, в конце дня оставалось гораздо меньше незакрытых заявок, которые приходилось снимать.

Как это кончилось

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

Робот остался в коде. Рынок, под который он был заточен, кончился раньше, чем он сам.

Возражения

«Это не алго-трейдинг». Не алго-трейдинг, конечно. Это автоматизация исполнения: робот не искал прибыльные сделки, он закрывал уже совершённые с минимальной потерей маржи. На гордое слово «алго» не претендую, слово «робот» он носил честно.

«Где бэктест, где метрики качества?» Бэктестом были годы закрывания этих же сделок руками. Понимаю, как это звучит, но у задачи «не потерять три копейки на сотне сделок в день» цена ошибки — три копейки, а не карьера. Наивная схема, которую понимаешь до последней ячейки, здесь бьёт умную, которую не понимаешь: когда рынок сошёл с ума, я точно знал, почему робота пора выключать.

«Почему не готовая система исполнения?» Малый банк, 2020 год, бюджет ноль, разработчик — я вечерами. Выбор стоял не «самописный или вендорский», а «самописный или руками».

«VB6 в 2020-е?!» Да. Он подключался к QUIK, слал заявки и играл фанфары до последнего рабочего дня. Про судьбу всей этой системы на VB6 у меня отдельные статьи, там драмы больше.

Что я из этого вынес

  1. Автоматизируй рефлекс, который уже работает, а не стратегию из книжки. У рефлекса есть многолетний бэктест, у книжки — только обещания.

  2. Градаций нужно столько, сколько различает человек, которого ты оцифровываешь. Руки различали пять состояний рынка — робот получил пять, и этого хватило.

  3. Обвязка важнее интеллекта. Надёжно получить сделку, надёжно поставить заявку и не поставить её дважды — вот где живёт настоящая сложность, а не в формуле цены.

  4. Прибитые в коде параметры — дневник: по ним потом датируешь эпохи точнее, чем по памяти. Моя память говорила «пять копеек», код ответил «в конце было тридцать пять».

  5. У всякой автоматизации есть рынок, под который она заточена. Когда рынок уходит, выключить вовремя не менее важно, чем вовремя запустить.

Файл tada.WAV так и лежит в папке рядом с exe. Больше не играет.