Бумажная торговля, конфигурация и лимиты риска

Урок 12 курса по арбитражу. Запустите бота на живых ценах с условными деньгами, вынесите все числа в файл конфигурации и добавьте лимиты риска, которые остановят бота до ущерба от плохого часа. Короткий код на Python с проверяемым выводом.

Урок 12 курса "Собери своего арбитражного бота". Здесь завершается мир 3. У бота уже есть сканер, калькулятор размера и проверка реальностью. Сегодня сделаем его безопасным для запуска.

Прямолинейный подход

Самый грубый способ начать: вставить ключи в скрипт, жестко прописать все числа, комиссию 0.001, размер 1 и так далее прямо в функциях, а затем запустить с реальными деньгами, чтобы "посмотреть, что будет". Это работает ровно до первой неожиданности. Потом на три вопроса нет хорошего ответа. Какое число я изменил? Сколько я потерял? Почему бот продолжал торговать?

В этом уроке исправим все три проблемы, по одному инструменту для каждой.

Инструмент 1: бумажная торговля

Бумажная торговля означает, что весь бот работает на живых ценах, принимает решения и отправляет ордера в условный аккаунт, который лишь записывает, что произошло бы. Деньги не перемещаются. Вы видите, как ведет себя бот, и любая ошибка ничего не стоит.

Полезной ее делает одна деталь: у условного аккаунта есть тот же метод, что и у настоящего клиента биржи, здесь он называется trade. Бот не знает, с каким из них разговаривает. Позже при переключении на настоящий клиент ничего больше не меняется.

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

Инструмент 2: файл конфигурации

Каждое число, которое может понадобиться изменить, находится в одном месте и читается при запуске. В коде ниже это текст JSON внутри программы, поэтому его можно запустить как есть. В вашем боте это будет файл config.json.

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

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

Инструмент 3: лимиты риска

Лимит риска: правило, которое останавливает бота, когда что-то выглядит неверно, независимо от того, что считает бот. Четыре базовых лимита:

  • Максимальный размер одной сделки. Ошибка, которая запрашивает 1000 монет вместо 1, будет остановлена здесь.
  • Минимальный edge: не торгуйте, если запланированная прибыль ниже запаса безопасности, почему это важно, объяснил урок 11.
  • Максимальное число сделок за период. Вышедший из-под контроля цикл упрется в этот лимит.
  • Максимальный дневной убыток. Когда убыток превышает лимит, бот останавливается, и перезапустить его может только человек. Это аварийный выключатель.

Код

import json

CONFIG = json.loads("""{
  "fee": 0.001,
  "min_edge": 0.05,
  "max_size": 1.0,
  "max_trades": 20,
  "max_daily_loss": 0.50
}""")

def walk(levels, amount):
    money, left = 0.0, amount
    for price, size in levels:
        take = min(size, left)
        money += take * price
        left -= take
        if left <= 0:
            break
    return money, amount - left

def plan_profit(asks, bids, q, fee):
    """Profit after fees for size q, or None if a book is too thin."""
    paid, got_a = walk(asks, q)
    received, got_b = walk(bids, q)
    if got_a < q or got_b < q:
        return None
    return received * (1 - fee) - paid * (1 + fee)

class PaperAccount:
    """A fake account. A real exchange client would have the same trade() method."""
    def __init__(self):
        self.pnl, self.trades = 0.0, 0
    def trade(self, asks, bids, q, fee):
        profit = plan_profit(asks, bids, q, fee)
        self.pnl += profit
        self.trades += 1
        return profit

class Guard:
    def __init__(self, cfg):
        self.cfg, self.stopped = cfg, False
    def check(self, account, size, edge):
        if self.stopped:
            return "stopped"
        if account.pnl <= -self.cfg["max_daily_loss"]:
            self.stopped = True
            return "daily loss limit"
        if account.trades >= self.cfg["max_trades"]:
            return "trade limit"
        if size > self.cfg["max_size"]:
            return "size above limit"
        if edge is None or edge < self.cfg["min_edge"]:
            return "edge below minimum"
        return "ok"

asks = [(100.00, 2), (100.20, 3)]
bids = [(100.30, 1), (99.90, 5)]
account, guard = PaperAccount(), Guard(CONFIG)
for size in [1.0, 1.0, 1.5, 0.2]:
    edge = plan_profit(asks, bids, size, CONFIG["fee"])
    verdict = guard.check(account, size, edge)
    profit = account.trade(asks, bids, size, CONFIG["fee"]) if verdict == "ok" else None
    print(f"size {size:<4} edge {edge:+.4f} -> {verdict:20} profit {profit if profit is None else round(profit, 4)}")
print("pnl", round(account.pnl, 4), "trades", account.trades)

print("loss streak:")
account, guard = PaperAccount(), Guard(CONFIG)
for i in range(6):
    account.pnl -= 0.20
    print(i, round(account.pnl, 2), guard.check(account, 1.0, 0.1))

Вывод:

size 1.0  edge +0.0997 -> ok                   profit 0.0997
size 1.0  edge +0.0997 -> ok                   profit 0.0997
size 1.5  edge -0.0502 -> size above limit     profit None
size 0.2  edge +0.0199 -> edge below minimum   profit None
pnl 0.1994 trades 2
loss streak:
0 -0.2 ok
1 -0.4 ok
2 -0.6 daily loss limit
3 -0.8 stopped
4 -1.0 stopped
5 -1.2 stopped

Разберите вывод.

  • Первые две сделки проходят все проверки и записываются: по 0.0997, всего 0.1994. Значение 0.0997 можно проверить вручную по уроку 10.
  • Размер 1.5 останавливает лимит размера, он также принес бы убыток: -0.0502.
  • У размера 0.2 положительный edge 0.0199, но он ниже минимального edge 0.05, поэтому бот его пропускает. Такая маленькая прибыль не стоит риска из урока 11.
  • В серии убытков бот теряет 0.20 на шаг. Защита срабатывает на третьем шаге, когда общий результат составляет -0.6. Лимит равен 0.50, убыток превысил его на 0.10, потому что проверка выполняется перед каждой сделкой и видит только уже полученные убытки. Лимит останавливает рост убытка, но не может отменить последнюю сделку. Установите его ниже убытка, который вы действительно можете себе позволить.
  • После "daily loss limit" защита навсегда отвечает "stopped". Сбросить ее может только перезапуск человеком. Так и задумано.

Перед запуском на реальных деньгах

Разумный порядок действий: переходите к следующему шагу только тогда, когда предыдущий стал скучным:

  1. Бумажная торговля не меньше недели с логами. Сравните заявленную прибыль с оценкой из урока 11.
  2. Настоящая биржа, минимальный размер, который допускает биржа, и лимит убытка в несколько долларов. Цель: проверить подключение, ордера и комиссии, а не заработать.
  3. Медленно увеличивайте размер, только пока реальные результаты совпадают с результатами бумажной торговли.

Если реальные результаты намного хуже результатов бумажной торговли, разница и есть ваши реальные издержки: задержка, неудачные этапы, другие боты. Найдите причину, прежде чем увеличивать размер.

Попросите агента ИИ написать и проверить код

Расплывчатый запрос:

Добавь защиту в моего бота.

Запрос с проверками:

Напиши safe.py с plan_profit, PaperAccount и Guard из урока 12, читая настройки из config.json (fee 0.001, min_edge 0.05, max_size 1.0, max_trades 20, max_daily_loss 0.50). Проверь с asks [(100.00, 2), (100.20, 3)] и bids [(100.30, 1), (99.90, 5)]: размеры 1.0, 1.0, 1.5, 0.2 должны дать ok, ok, size above limit, edge below minimum, а итоговый pnl должен быть 0.1994. Проверка 2: убыток 0.20 на шаг должен вывести "daily loss limit" на шаге 2 и "stopped" после этого. Читай ключи API только из переменных окружения и никогда не печатай их. Не подключайся ни к одной бирже.

Проверьте pnl 0.1994 на калькуляторе, прежде чем принять работу агента.

Ваше упражнение

  1. Запустите код и сравните результат с выводом выше.
  2. Вынесите конфигурацию в настоящий файл config.json и прочитайте его через json.load. Измените min_edge на 0.10 и посмотрите, какие сделки пропускаются.
  3. Измените защиту, чтобы она проверяла убыток после каждой сделки и останавливалась на первой сделке, которая пересекает лимит. Исчезает ли превышение? Не полностью: последняя сделка все равно происходит.
  4. Добавьте лимит "максимальная открытая позиция": если после неудачного этапа бот одновременно держит больше 2 монет, он останавливается.

Что дальше

Мир 3 завершен: у вас есть целый бот, который ищет возможности, рассчитывает размер, моделирует и защищает. Мир 4 сделает его быстрее и шире: урок 13 начнется со скорости, почему Python становится медленным, что с этим делать и как в картину входит Rust.