Рынок
| ЦЕНА | SHARES | ИТОГО | |
|---|---|---|---|
| Продажа | |||
| Посл.: Спред: | |||
| Покупка | |||
Активные ордера
| Сторона | Цена | Размер | Остаток | Очередь | Статус |
|---|---|---|---|---|---|
| ✓подтв ⏳ ✓баланс ?баланс |
Открытых ордеров нет.
Журнал действий
| время | тип | сторона | цена | размер | ордер | причина |
|---|---|---|---|---|---|---|
Настройки бота
Часть = shares × . Часть 1 — сейчас; часть 2 — при −% от средней; часть 3 — ещё −% и т.д.
0 = выключен.
Сработает когда mark ≤ триггер: бот закроет позицию по выбранному режиму
и встанет на паузу до ручного Возобновить или рестарта.
0.999). На рынке с tick=0.01 цена 0.999
не выровнена и SELL автоматически дропается (тихо); как только
биржа переходит на tick=0.001, ордер на 0.999 ставится — служит
«потолком» при резком скачке. 0 = выключен.
Текущий режим: . Управляется только кнопкой Переключить на LIVE / SIM в шапке. Save Настроек режим не трогает.
Live-стакан
| Мой BUY | Bid размер | Накопл. / $ | Цена | Накопл. / $ | Ask размер | Мой SELL |
|---|---|---|---|---|---|---|
Стакан ещё не загружен — нажми Старт чтобы подписаться на WS-фид активного токена.
⬤ синяя ячейка «Накопл.» = накопление пересекло порог глубины (buy:, sell:)
Цена (30 мин)
Активность рынка и калькулятор объёма
Модель: Частота · цель 5–10 fills/ч · размер 300–1000Выбери sub-market и направление в панели «Рынок» (или через «Использовать в боте»), чтобы калькулятор заработал.
Загружаю активность по активному токену…
Публичные сделки по активному токену — единый источник данных: вкладки окон фильтруют один и тот же список, статистика агрегируется из него, рекомендация считается по статистике выбранного окна.
① Лента сделок
Ни одна сделка не подходит под фильтры. Расширь окно или понизь «Мин».
② Расчётная статистика (активное окно)
③ Сравнение окон — одни данные на разных горизонтах
| Окно | Сделок | сд/мин | p25 | p50 | p75 | p90 | Sell-takers/мин |
|---|---|---|---|---|---|---|---|
④ Время до заполнения по уровням bid (по throughput активного окна)
| Цена | Очередь на уровне | Накопл. впереди (если встать) | Ожидаемое ETA |
|---|---|---|---|
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 UI | HTTP API + WS-стрим состояния браузеру (этот интерфейс). |
StrategyEngine | Tick-loop (100ms) — считает intents, согласует с ladder, дёргает backend. |
LiveExecution / SimExecution | Backend, размещающий ордера. Live — через py-clob-client-v2 (EIP-712 V2 подписи). Sim — queue-aware fill model. |
MarketWsFeed | WebSocket рыночного канала Polymarket — стрим book/trades. |
UserWs (внутри LiveExecution) | WebSocket пользовательского канала — fills/acks/cancellations по нашим ордерам. |
OrderbookCache | In-memory книга (двусторонняя), trade ring, snapshot для tick-loop. |
LadderState | Отслеживание состояния каждого нашего ордера: pending / partial / filled / cancelled / rejected. |
BalanceWatcher | 30s-loop on-chain чтение pUSD balanceOf через web3 для over-spend защиты + audit. |
PositionReconcile | 10s-loop on-chain чтение CTF balanceOf(owner, token_id) — drift detection + auto-heal. |
book_poll_loop | 5s REST fallback на /book — страховка, если рыночный WS замолчит. |
open_orders_sync_loop | 15s 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_threshold | 1.0 × depth | Существующий ордер живёт, пока его queue ≤ depth. Strict. |
| move-up / move-down порог | 0.5 × tick | Срабатывает на ≥1 tick улучшение. |
| WALK_STABILITY_S | 1.0 сек | Цена walk-а должна продержаться столько перед move. |
| MIN_REPRICE_REST_S | 1.0 сек | Только неблагоприятный reprice ждёт, чтобы отфильтровать фантомную стену. |
5. Источники данных и задержки
| Источник | Канал | Период | Назначение |
|---|---|---|---|
| Polymarket Market WS | wss://ws-subscriptions-clob.polymarket.com/ws/market | real-time | book diffs + публичные trades |
| Polymarket User WS | .../ws/user | real-time | наши fills, acks, cancellations |
| REST book poll | /book?token_id=… | 5 сек | fallback если WS замолчал на тихом рынке |
| Open-orders sync | /data/orders | 15 сек + on-reject burst | лечит ladder ↔ PM drift |
pUSD balanceOf | Polygon RPC | 30 сек + priority после fill'а | over-spend защита |
CTF balanceOf(token) | Polygon RPC | 10 сек | детектор drift'а позиции |
5.1 Задержки (по убыванию)
| Источник задержки | Окно | Что делаем |
|---|---|---|
| PM cancel broadcast lag | 5–15 сек | CANCEL_GRACE_S=20 — отменённый ордер считается «нашим» в очереди ещё 20с |
| PM settlement (CTF transfer) | 3–8 сек | Settlement gate блокирует противоположную сторону |
PM matched_orders bucket | 3–8 сек | Recent BUY fill учитывается как locked в balance до подтверждения on-chain |
| WS-стрим тихого рынка | до 120 сек | REST poll каждые 5с страхует |
| Position auto-heal trigger | 30 сек | Стабильный отрицательный 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 capqueue_at_placeна строго лучших ценах
6.7 Stop-loss
Опциональный. Если stop_loss_enabled + stop_loss_price заданы и
mark-price пересекает trigger, бот: cancel_all → FOK SELL
по всей позиции по stop_loss_exit_price (или mark). После этого
stop-loss state — suspended (ждёт ручного reset).
7. Настройки кошелька / proxy
| Поле | Описание |
|---|---|
name | Произвольное имя для UI. |
address | EOA (Externally Owned Account) — публичный адрес кошелька-подписанта. |
private_key | Хранится зашифрованным (Argon2id + ChaCha20-Poly1305) под master_password. Plain-text storage опционален (env BOND_MM_PLAINTEXT_PK). |
signature_type | 0 = EOA (подпись прямо адресом), 1 = Poly Proxy, 2 = Gnosis Safe (большинство пользователей Polymarket). |
funder_address | Адрес, который реально держит pUSD / CTF. Для sig_type=2 авто-вычисляется из EOA через SAFE_PROXY_FACTORY.computeProxyAddress — это и есть твой Polymarket profile. |
rpc_url | Personal Polygon RPC (Alchemy / QuickNode). Если не задан — используется public node (могут быть rate-limit'ы). |
8. Настройки рынка и стратегии
| Поле | Значение по умолчанию | Описание |
|---|---|---|
market_slug | — | Slug из URL Polymarket, напр. us-x-iran-permanent-peace-deal-by-may-22-2026. |
direction | YES / NO | На какой стороне маркетим (YES-token либо NO-token). |
max_position_shares | — | Жёсткий потолок позиции. BUY-сторона перестаёт ставить ордера при достижении. |
depth_buy_shares | 200 | Максимум очереди впереди для BUY (Σ size'ов на строго лучших ценах + same-level). Walk-pricing подбирает цену чтобы влезть. |
depth_sell_shares | 200 | Симметрично для SELL. |
min_order_shares | 5 | Polymarket требует минимум $2 ИЛИ 5 шар. 5 — безопаснее. |
min_sell_offset_ticks | 1 | SELL никогда ниже avg_entry + offset × tick. 0 = продаём по avg_entry, отрицательное = в убыток (не разрешено). |
sizing_variant | single | single — один ордер на сторону на полную свободную ёмкость; ladder — усреднение вниз: max_position делится на ladder_levels равных частей, по одной части на текущем рынке. |
ladder_levels | 3 | Только для ladder — на сколько равных частей делится max_position. |
step_pct | 2.0 | Только для ladder. Следующая часть включается, когда цена опускается на step_pct % ниже текущей средней цены входа (пересчёт после каждого fill). Так каждая часть докупается ниже → средняя цена входа снижается. 0 = выключить шаг (все части сразу). Ratchet сбрасывается при выходе в ноль. |
stop_loss_* | off | Опционально: enabled, price, exit_price. |
9. SIM vs LIVE режимы
| Аспект | SIM | LIVE |
|---|---|---|
| Цель | Тест стратегии без риска | Реальная торговля |
| Заполнения | Queue-aware fill model — наблюдаемые публичные trades эродируют queue впереди, после чего наш ордер «исполняется» | Реальные fills через user-WS |
| Ack | Синхронный (сразу после place) | Асинхронный, приходит через user-WS |
| Подпись | Не нужна — синтетика | EIP-712 V2 (CTF / NegRisk exchange domain) |
| Balance watcher | Off | 30s loop on-chain pUSD |
| Settlement gates | Off (мгновенно) | 3–8с после fill'а |
| Position reconcile | Off | 10s 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.db | SQLite: 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). |
reject | PM или локальный guard отказал. Поле error — verbatim текст ошибки. |
alert | Оператор-видимое сообщение (severity: info / warn / error). |
stop_loss | Stop-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).