bond_mm
ВЫХОД
ШАГ
ОТМЕНЕНО
WARNING
КОШЕЛЁК
СЕССИЯ
АПТАЙМ
РАЗМЕЩЕНО
ОТМЕНЕНО
ОТКЛОНЕНО
ЗАПОЛНЕНИЙ
ПОСЛ. ДЕЙСТВИЕ
ФИД
СВЕРКА БАЛАНСА
НАЧ. БАЛАНС
ОЖИДАЕМЫЙ
ON-CHAIN
ДРИФТ
СТОРОНА
ТЕК. / МАКС.
БАЗИС
МАРК
СР. ВХОД
СР. ВЫХОД
КУП/ПРОД
РЕАЛИЗ.
НЕРЕАЛИЗ.
ЦИКЛОВ
УСРЕДН.
ПОРТФОЛИО

Рынок

Активные ордера

СторонаЦенаРазмерОстатокОчередьСтатус

Открытых ордеров нет.

Журнал действий

времятипсторонаценаразмерордерпричина

Настройки бота

Часть = shares × . Часть 1 — сейчас; часть 2 — при −% от средней; часть 3 — ещё −% и т.д.

Стоп-лосс — цена триггера 0 = выключен. Сработает когда mark ≤ триггер: бот закроет позицию по выбранному режиму и встанет на паузу до ручного Возобновить или рестарта.
soft: один лимит-SELL на «Цене выхода» — ждёт пока кто-то возьмёт. Хорошо когда хочешь зафиксировать минимальную цену, но не готов жертвовать ликвидностью. hard: FAK SELLs по best_bid, цикл до полного выхода или пока bid не упадёт ниже «Экстремум». Каждая попытка берёт что есть на текущей цене и идёт ниже. Гарантирует выход (если есть ликвидность ≥ extreme). «Цена выхода» здесь — верхний лимит первой попытки (если bid выше, ограничит).
⚠ Цена триггера () ≥ текущей mark-цены () — стоп-лосс сработает СРАЗУ при старте. Понизь триггер или поставь 0.
Тейк-профит — пассивный SELL всей позиции по этой цене (по умолчанию 0.999). На рынке с tick=0.01 цена 0.999 не выровнена и SELL автоматически дропается (тихо); как только биржа переходит на tick=0.001, ордер на 0.999 ставится — служит «потолком» при резком скачке. 0 = выключен.

Текущий режим: . Управляется только кнопкой Переключить на LIVE / SIM в шапке. Save Настроек режим не трогает.

Live-стакан

Лучший bid
Mid
Лучший ask
Спред
Мой BUYBid размерНакопл. / $ЦенаНакопл. / $Ask размерМой SELL

Стакан ещё не загружен — нажми Старт чтобы подписаться на WS-фид активного токена.

⬤ синяя ячейка «Накопл.» = накопление пересекло порог глубины (buy:, sell:)

Цена (30 мин)

Активность рынка и калькулятор объёма

Модель: Частота · цель 5–10 fills/ч · размер 300–1000

bond_mm — Полная документация

Polymarket market-maker. Этот документ описывает архитектуру, стратегию, расчёты, источники данных, задержки, защиты, настройки и логи.

1. Что делает бот

Maker-only стратегия на Polymarket CLOB V2. Бот размещает GTC (Good-Till-Cancelled) post-only лимитные ордера с двух сторон рынка (BUY + SELL) на выбранной стороне (YES или NO) одного рынка и постоянно подстраивает их под движение книги, удерживаясь в очереди не глубже заданного порога depth_*_shares.

  • Цель: зарабатывать на bid-ask spread + maker fee rebate, поддерживая запас.
  • Не делает: taker-ордера (кроме stop-loss FOK), маркет-ордера, кросс-рыночный арбитраж.
  • Один экземпляр = один рынок + одна сторона (YES либо NO). Для параллельных рынков нужен отдельный процесс.

2. Архитектура

КомпонентНазначение
FastAPI + Alpine.js UIHTTP API + WS-стрим состояния браузеру (этот интерфейс).
StrategyEngineTick-loop (100ms) — считает intents, согласует с ladder, дёргает backend.
LiveExecution / SimExecutionBackend, размещающий ордера. Live — через py-clob-client-v2 (EIP-712 V2 подписи). Sim — queue-aware fill model.
MarketWsFeedWebSocket рыночного канала Polymarket — стрим book/trades.
UserWs (внутри LiveExecution)WebSocket пользовательского канала — fills/acks/cancellations по нашим ордерам.
OrderbookCacheIn-memory книга (двусторонняя), trade ring, snapshot для tick-loop.
LadderStateОтслеживание состояния каждого нашего ордера: pending / partial / filled / cancelled / rejected.
BalanceWatcher30s-loop on-chain чтение pUSD balanceOf через web3 для over-spend защиты + audit.
PositionReconcile10s-loop on-chain чтение CTF balanceOf(owner, token_id) — drift detection + auto-heal.
book_poll_loop5s REST fallback на /book — страховка, если рыночный WS замолчит.
open_orders_sync_loop15s REST sync с /data/orders — лечит ladder ↔ PM drift.

3. MM-стратегия

3.1 Walk-to-deepest-fit pricing

На каждом tick для BUY: выбирается самая низкая цена, у которой cum_volume_above + size_at_level ≤ depth_buy_shares. Для SELL — симметрично, самая высокая цена. Это даёт нам максимальную позицию в очереди (deepest fit), не превышая порог depth.

Если ни одна существующая цена не проходит, включается step-into-spread fallback: BUY = best_bid + 1 tick, SELL = best_ask − 1 tick (одиночка, queue = 0). Ограничено собственным spread (нельзя пересечь).

3.2 Move-up / move-down с debounce

Когда walk на новом tick выбирает цену лучше существующего ордера ≥1 tick, мы готовы отменить и переразместить (move-down для BUY, move-up для SELL). Чтобы не сгорать на тонких всплесках книги, мы применяем стабильность 1.0 сек — отмена сработает только если новая цена держится 10+ tick'ов подряд.

3.3 Multi-order top-up

Существующие ордера, которые ещё в очереди (queue ≤ depth), сохраняются — мы не теряем приоритет ради swap. Если есть незаполненный capacity (после SELL-fill, например), бот ставит дополнительный ордер при той же или близкой цене вместо отмены и пересоздания.

3.4 Self-cross guard

Если step-into-spread fallback хочет BUY @ p и SELL @ p одновременно (узкий spread), BUY-сторона снимается. Post-only ордера самопересечения отклоняются Polymarket.

3.5 SELL price floor

SELL никогда не размещается ниже avg_entry + min_sell_offset_ticks × tick. Защищает от продаж в убыток на маркет-мейке.

3.6 Settlement gates

После fill'а вторая сторона ждёт on-chain подтверждения (или 8s fallback):

  • После BUY fill → SELL заблокирован до подтверждения, что CTF-шары появились on-chain.
  • После SELL fill → BUY заблокирован до подтверждения, что pUSD пришли on-chain.
  • Reactive: BalanceWatcher вызывает priority-tick через 3s после fill'а.

4. Расчёты размеров и цен

4.1 BUY size

free = max_position_shares − held_for_buy

# Балансная ёмкость:
locked_usd = Σ(price × remaining) для всех активных BUY
           + Σ(price × size) для отменённых BUY в окне 20с (cancel-grace)
           + Σ matched_orders_usd за последние 30с (PM settlement bucket)
effective_budget = (balance − locked_usd) × 0.99   # 1% headroom

# На каждый intent:
per_share_cost = walk_price × 1.02                  # 1.5% fee buffer + rounding
size = min(intent.size, effective_budget / per_share_cost)
size = floor(size)                                  # BUY всегда целое число шар

Если BalanceWatcher ещё не прочитал баланс (только что старт), BUY полностью замораживается до первого чтения.

4.2 SELL size — точная (100% позиции)

onchain_held = CTF balanceOf(funder, token_id) / 1e6   # 6 знаков после запятой

# Within 30s recent SELL-fill: min(local, onchain) — settlement safety
# Otherwise: max(local, onchain) — handle manual buys
held_for_sell = min/max(local_position, onchain_held)

# SELL cap:
existing_sell = Σ remaining для активных SELL + Σ для cancel-grace SELL
ceiling = onchain_held − 1e-6                       # одна микро-шара safety
free_for_sell = ceiling − existing_sell             # ровно столько можно ещё разместить
sell_gap = min(held_for_sell, free_for_sell)        # FRACTIONAL — round(x, 6)

В отличие от BUY, SELL размер дробный до 6 знаков после запятой — так распродаётся 100% позиции без остатка (например, 234.768778 шар).

4.3 Queue-cap при размещении

queue_ahead = Σ size для всех уровней по строго лучшей цене
            + size_at_intent_price (same-level)
            − own_at_intent_price                    # мы — не очередь сами для себя
            − own_cancel_grace_at_strictly_better    # отменённые ордера PM ещё считает

if queue_ahead > depth_buy_shares (или depth_sell_shares):
    drop intent — не размещаем

4.4 Keep tolerance + move thresholds

ПараметрЗначениеЭффект
keep_threshold1.0 × depthСуществующий ордер живёт, пока его queue ≤ depth. Strict.
move-up / move-down порог0.5 × tickСрабатывает на ≥1 tick улучшение.
WALK_STABILITY_S1.0 секЦена walk-а должна продержаться столько перед move.
MIN_REPRICE_REST_S1.0 секТолько неблагоприятный reprice ждёт, чтобы отфильтровать фантомную стену.

5. Источники данных и задержки

ИсточникКаналПериодНазначение
Polymarket Market WSwss://ws-subscriptions-clob.polymarket.com/ws/marketreal-timebook diffs + публичные trades
Polymarket User WS.../ws/userreal-timeнаши fills, acks, cancellations
REST book poll/book?token_id=…5 секfallback если WS замолчал на тихом рынке
Open-orders sync/data/orders15 сек + on-reject burstлечит ladder ↔ PM drift
pUSD balanceOfPolygon RPC30 сек + priority после fill'аover-spend защита
CTF balanceOf(token)Polygon RPC10 секдетектор drift'а позиции

5.1 Задержки (по убыванию)

Источник задержкиОкноЧто делаем
PM cancel broadcast lag5–15 секCANCEL_GRACE_S=20 — отменённый ордер считается «нашим» в очереди ещё 20с
PM settlement (CTF transfer)3–8 секSettlement gate блокирует противоположную сторону
PM matched_orders bucket3–8 секRecent BUY fill учитывается как locked в balance до подтверждения on-chain
WS-стрим тихого рынкадо 120 секREST poll каждые 5с страхует
Position auto-heal trigger30 секСтабильный отрицательный drift → snap local = on-chain

6. Защиты и исключения

6.1 Reject loop breaker

После 2 balance/allowance reject'ов на одну и ту же (side, price) в течение 5 сек — эта цена попадает в blacklist на 30 секунд. Параллельно запускается immediate sync открытых ордеров. Защищает от бесконечного цикла «попытка → reject → попытка».

6.2 Circuit breaker

10 подряд non-balance, non-local-guard reject'ов от PM → бот ставится на паузу, оператор должен «Возобновить». Balance/allowance reject'ы и наши локальные guard'ы (queue_cap_at_place) НЕ считаются в счётчик — они самокорректирующиеся.

6.3 Position auto-heal

Если on-chain shares > local position более чем на 1 шару и это держится 30+ сек — бот автоматически синхронизирует local = on-chain (ручная покупка / пропущенный fill). Одновременно BalanceWatcher ресинкается expected → actual, чтобы не дублировать ghost-fill alert. только negative drift

6.4 Stale-book guard

Если книга не обновлялась 120+ сек, бот алертит и пересоздаёт MarketWsFeed. Между 30с и 120с tick'и просто пропускаются (молча).

6.5 Cross-market WS filter

User-WS стримит все trade'ы кошелька по всем рынкам. Бот фильтрует по asset_id == active_token_id — иначе ручные сделки на других рынках портили бы расчёт позиции и баланса на текущем.

6.6 Cancel-grace в расчётах

Только что отменённый локально ордер 20 секунд продолжает учитываться в:

  • own_buy/sell на уровне (walk не считает свой ордер очередью для себя)
  • locked_usd для balance cap (PM ещё держит залог)
  • existing_sell_active для SELL cap
  • queue_at_place на строго лучших ценах

6.7 Stop-loss

Опциональный. Если stop_loss_enabled + stop_loss_price заданы и mark-price пересекает trigger, бот: cancel_allFOK SELL по всей позиции по stop_loss_exit_price (или mark). После этого stop-loss state — suspended (ждёт ручного reset).

7. Настройки кошелька / proxy

ПолеОписание
nameПроизвольное имя для UI.
addressEOA (Externally Owned Account) — публичный адрес кошелька-подписанта.
private_keyХранится зашифрованным (Argon2id + ChaCha20-Poly1305) под master_password. Plain-text storage опционален (env BOND_MM_PLAINTEXT_PK).
signature_type0 = EOA (подпись прямо адресом), 1 = Poly Proxy, 2 = Gnosis Safe (большинство пользователей Polymarket).
funder_addressАдрес, который реально держит pUSD / CTF. Для sig_type=2 авто-вычисляется из EOA через SAFE_PROXY_FACTORY.computeProxyAddress — это и есть твой Polymarket profile.
rpc_urlPersonal Polygon RPC (Alchemy / QuickNode). Если не задан — используется public node (могут быть rate-limit'ы).

8. Настройки рынка и стратегии

ПолеЗначение по умолчаниюОписание
market_slugSlug из URL Polymarket, напр. us-x-iran-permanent-peace-deal-by-may-22-2026.
directionYES / NOНа какой стороне маркетим (YES-token либо NO-token).
max_position_sharesЖёсткий потолок позиции. BUY-сторона перестаёт ставить ордера при достижении.
depth_buy_shares200Максимум очереди впереди для BUY (Σ size'ов на строго лучших ценах + same-level). Walk-pricing подбирает цену чтобы влезть.
depth_sell_shares200Симметрично для SELL.
min_order_shares5Polymarket требует минимум $2 ИЛИ 5 шар. 5 — безопаснее.
min_sell_offset_ticks1SELL никогда ниже avg_entry + offset × tick. 0 = продаём по avg_entry, отрицательное = в убыток (не разрешено).
sizing_variantsinglesingle — один ордер на сторону на полную свободную ёмкость; ladder — усреднение вниз: max_position делится на ladder_levels равных частей, по одной части на текущем рынке.
ladder_levels3Только для ladder — на сколько равных частей делится max_position.
step_pct2.0Только для ladder. Следующая часть включается, когда цена опускается на step_pct % ниже текущей средней цены входа (пересчёт после каждого fill). Так каждая часть докупается ниже → средняя цена входа снижается. 0 = выключить шаг (все части сразу). Ratchet сбрасывается при выходе в ноль.
stop_loss_*offОпционально: enabled, price, exit_price.

9. SIM vs LIVE режимы

АспектSIMLIVE
ЦельТест стратегии без рискаРеальная торговля
ЗаполненияQueue-aware fill model — наблюдаемые публичные trades эродируют queue впереди, после чего наш ордер «исполняется»Реальные fills через user-WS
AckСинхронный (сразу после place)Асинхронный, приходит через user-WS
ПодписьНе нужна — синтетикаEIP-712 V2 (CTF / NegRisk exchange domain)
Balance watcherOff30s loop on-chain pUSD
Settlement gatesOff (мгновенно)3–8с после fill'а
Position reconcileOff10s loop on-chain CTF

Можно безопасно стартовать SIM, дождаться нескольких fills'ов, оценить стратегию, потом переключиться на LIVE на том же рынке.

10. Логи и наблюдаемость

10.1 Файлы

ПутьСодержимое
logs/session_<id>.jsonlСтруктурированный JSON-лог одной сессии: actions, alerts, raw httpx, WS events. По одному файлу на сессию.
logs/env_snapshots/Снимки книги ±5с вокруг каждого action — для post-mortem.
data/bond_mm.dbSQLite: wallets, configs, sessions, bot_actions, fills, pnl_snapshots, markets.

10.2 Типы событий в session log

action_typeОписание
placeОрдер успешно размещён, есть order_id от PM.
ackБиржа подтвердила (пришло по user-WS). UI показывает «подтв».
cancelОдин или несколько ордеров отменены. Поле reason: no_match / tick_mismatch / stop_loss / etc.
fillЗаполнение (полное или частичное). size — float (Polymarket поддерживает partial maker fills).
rejectPM или локальный guard отказал. Поле error — verbatim текст ошибки.
alertОператор-видимое сообщение (severity: info / warn / error).
stop_lossStop-loss сработал.

10.3 Анализ

# подсчитать действия по типу:
grep '"action_type"' logs/session_125.jsonl | grep -oE '"action_type": "[^"]+"' | sort | uniq -c

# поток только fills:
grep '"action_type": "fill"' logs/session_125.jsonl | jq '.'

# проверить orders, отвергнутые балансом:
grep '"reason": ".*not enough balance' logs/session_125.jsonl | wc -l

11. Эксплуатация и troubleshooting

11.1 Запуск

# macOS / Linux:
./install_macos.sh        # один раз
python -m bond_mm.main    # запуск (http://127.0.0.1:9099)

11.2 Частые ситуации

СимптомЧто значитДействие
Бот молчит, ladder пустойВозможные причины: позиция у max_position; стена в книге больше depth; min_order_shares не вмещаетсяРаскрой UI «Sizing» — он подсказывает, какой knob подкрутить
«Polymarket отверг … not enough balance»Bot's locked-collateral расчёт расходится с PM. Обычно — недавно отменённый ордер ещё держится в matched_orders / active_orders на PM-сторонеСрабатывает reject loop breaker + immediate sync. Само должно пройти за 30с.
«Дрифт позиции» (negative)Ручная покупка либо пропущенный fill. Auto-heal сработает через 30сПодожди, проверь Δ shares × средняя цена соответствует balance Δ
«Дрифт позиции» (positive — local > on-chain)Возможно ghost-fill: бот думает что владеет, а on-chain нетОстанови, сверь на Polygonscan, проверь fills в session log
Стакан в UI пустойWS feed не подключился или маркет неликвидныйПодожди 5с (REST poll), либо restart feed

11.3 Кнопки управления

  • Старт — запускает engine, импортирует существующие ордера PM и позицию on-chain.
  • Пауза — engine продолжает читать данные, но не размещает.
  • Возобновить — снимает паузу + clears reject blacklist.
  • Стоп — cancel_all + остановка engine + закрытие сессии.

11.4 Безопасность

  • Master password хранится только в памяти. Перезапуск процесса = lock.
  • Приватный ключ шифруется Argon2id (64MB memory cost) + ChaCha20-Poly1305.
  • Бот HTTP API доступен только на 127.0.0.1:9099 по умолчанию. Не публикуй порт.
  • Не запускай два процесса бота с одним кошельком на одном рынке — будет рассогласование ladder.

Версия: bond_mm v1 (2026). Полный исходный код — в директории установки (см. README).

Консоль