← Назад

Polymarket BTC 5-Minute Bot — Спека (26 Apr 2026)

Бот для last-second momentum arbitrage на BTC 5/15-минутных prediction markets


🎯 Цель

Бот мониторит цену BTC на Binance в реальном времени и ставит на Polymarket BTC Up/Down рынки в последние 30-60 секунд окна, эксплуатируя Chainlink oracle lag (2-15 сек).

Целевые метрики:


🧠 Как работают BTC 5-мин рынки

Механика

Polymarket: "BTC Up or Down — 1:05–1:10 AM ET"

Timeline одного 5-мин окна:
T=0s    ← Oracle фиксирует START price (Chainlink BTC/USD)
T=0-240s ← Торговля YES/NO шарами
T=240-300s ← НАШЕ ОКНО — last-second entry
T=300s  ← Oracle фиксирует END price
        ← Если END ≥ START → "Up" wins ($1.00)
        ← Если END < START → "Down" wins ($1.00)

Oracle: Chainlink BTC/USD

Почему есть edge

  1. Chainlink lag — мы видим реальную цену на Binance на 2-15 сек раньше oracle
  2. 15-20% периодов резолвятся на основе движений в ПОСЛЕДНИЕ 10 секунд
  3. Тонкая ликвидность ($5K-$50K за окно) → odds медленно реагируют
  4. Толпа не считает probability model — ставит "по ощущениям"

📦 Архитектура

┌──────────────────────────────────────────────────────┐
│                  BTC 5-Min Bot                        │
│                                                       │
│  ┌────────────┐  ┌────────────┐  ┌────────────────┐ │
│  │ Binance WS │→ │  Prob Model │→ │   Edge Detect  │ │
│  │ BTC/USDT   │  │(Monte Carlo)│  │ (model vs mkt) │ │
│  └────────────┘  └────────────┘  └────────┬───────┘ │
│                                            ↓          │
│  ┌────────────┐  ┌────────────┐  ┌────────────────┐ │
│  │Polymarket  │← │  Executor  │← │ Kelly Criterion │ │
│  │  CLOB WS   │  │(Limit Order)│  │   (Sizing)     │ │
│  └────────────┘  └────────────┘  └────────────────┘ │
│                                                       │
│  ┌────────────┐  ┌────────────┐  ┌────────────────┐ │
│  │  Chainlink  │  │  SQLite DB │  │   Telegram     │ │
│  │  Monitor    │  │  (trades)  │  │   Alerts       │ │
│  └────────────┘  └────────────┘  └────────────────┘ │
└──────────────────────────────────────────────────────┘

Компоненты

# Модуль Что делает
1 Binance WS Real-time BTC/USDT тики (у нас уже есть инфраструктура!)
2 Market Scanner Gamma API → текущий 5-мин BTC market + token IDs
3 Chainlink Monitor Отслеживает Chainlink BTC/USD oracle updates (lag detection)
4 Probability Model Monte Carlo / Brownian motion → P(Up) за оставшееся время
5 Polymarket WS WebSocket подписка на orderbook текущего рынка
6 Edge Detector model P vs market P, с учётом fees
7 Executor Limit order через CLOB API
8 Risk Manager Kelly sizing, daily limits, circuit breaker
9 Tracker + Notifier SQLite + Telegram

🔧 Tech Stack

Компонент Технология Почему
Язык Python 3.10+ py-clob-client SDK
Binance WS websockets + aiohttp Real-time BTC/USDT
Chainlink web3.py Polygon RPC, читаем oracle contract
Trading py-clob-client Polymarket CLOB API
Probability numpy + scipy Monte Carlo, Brownian motion, CDF
DB SQLite Trades log, P&L
Process PM2 Наш стандарт
Alerts Telegram Bot Через Bender

Python Dependencies

py-clob-client>=0.1.0
web3==6.14.0
websockets
aiohttp
numpy
scipy
python-dotenv
loguru
python-telegram-bot
apscheduler

📊 Шаг 1: Binance WS — Real-time BTC Price

Подключение

import asyncio, websockets, json, time

BINANCE_WS = "wss://fstream.binance.com/ws/btcusdt@aggTrade"

class BinancePriceFeed:
    def __init__(self):
        self.current_price = None
        self.price_history = []  # [(timestamp, price), ...]
        self.tick_count = 0

    async def connect(self):
        async with websockets.connect(BINANCE_WS) as ws:
            async for msg in ws:
                data = json.loads(msg)
                price = float(data['p'])
                ts = data['T'] / 1000  # ms → sec
                self.current_price = price
                self.price_history.append((ts, price))
                # Keep last 10 minutes
                cutoff = time.time() - 600
                self.price_history = [
                    (t, p) for t, p in self.price_history if t > cutoff
                ]

Что берём от Binance


📊 Шаг 2: Chainlink Oracle Monitor

Зачем

Chainlink — это oracle который Polymarket использует для resolution. Мы мониторим его чтобы:

  1. Знать START price окна (reference point)
  2. Измерять реальный lag vs Binance
  3. Предсказать NEXT oracle update

Chainlink BTC/USD на Polygon

from web3 import Web3

# Polygon RPC (бесплатный)
w3 = Web3(Web3.HTTPProvider("https://polygon-rpc.com"))

# Chainlink BTC/USD Price Feed на Polygon
CHAINLINK_BTC_USD = "0xc907E116054Ad103354f2D350FD2514433D57F6f"
AGGREGATOR_ABI = [...]  # latestRoundData()

contract = w3.eth.contract(address=CHAINLINK_BTC_USD, abi=AGGREGATOR_ABI)

def get_chainlink_price():
    """Returns (price, updated_at_timestamp)"""
    round_data = contract.functions.latestRoundData().call()
    price = round_data[1] / 1e8  # 8 decimals
    updated_at = round_data[3]    # unix timestamp
    return price, updated_at

Lag Measurement

def measure_lag(binance_price, binance_ts, chainlink_price, chainlink_ts):
    """Измеряем отставание Chainlink от Binance"""
    lag_seconds = binance_ts - chainlink_ts
    price_diff_pct = abs(binance_price - chainlink_price) / chainlink_price * 100
    return {
        "lag_seconds": lag_seconds,      # обычно 2-15 сек
        "price_diff_pct": price_diff_pct  # обычно 0.01-0.1%
    }

📐 Шаг 3: Probability Model

Вариант A: Brownian Motion (простой, быстрый)

import numpy as np
from scipy.stats import norm

def prob_up_brownian(current_price, start_price, remaining_seconds, volatility):
    """
    P(BTC end ≥ start) используя Geometric Brownian Motion

    current_price: текущая цена на Binance
    start_price: цена на момент T=0 (Chainlink start)
    remaining_seconds: сколько секунд до T=300
    volatility: σ (annualized vol, ~60% для BTC)
    """
    if remaining_seconds <= 0:
        return 1.0 if current_price >= start_price else 0.0

    # Конвертим annual vol в per-second
    sigma_per_sec = volatility / np.sqrt(365.25 * 24 * 3600)

    # Distance to threshold (в log-space)
    if current_price <= 0 or start_price <= 0:
        return 0.5

    log_distance = np.log(current_price / start_price)
    drift = -0.5 * sigma_per_sec**2 * remaining_seconds  # risk-neutral
    vol_term = sigma_per_sec * np.sqrt(remaining_seconds)

    if vol_term == 0:
        return 1.0 if current_price >= start_price else 0.0

    # P(S_T ≥ start_price) = P(Z ≥ -d)
    d = (log_distance + drift) / vol_term
    prob = norm.cdf(d)

    return round(prob, 4)

Вариант B: Monte Carlo (точнее, медленнее)

def prob_up_monte_carlo(current_price, start_price, remaining_seconds, volatility, n_paths=1000):
    """
    Monte Carlo симуляция N ценовых путей
    Считаем % путей где end_price ≥ start_price
    """
    sigma_per_sec = volatility / np.sqrt(365.25 * 24 * 3600)
    dt = 1  # 1 second steps

    steps = int(remaining_seconds)
    if steps <= 0:
        return 1.0 if current_price >= start_price else 0.0

    # Generate random paths
    Z = np.random.standard_normal((n_paths, steps))
    log_returns = (-0.5 * sigma_per_sec**2 * dt) + (sigma_per_sec * np.sqrt(dt) * Z)
    log_prices = np.log(current_price) + np.cumsum(log_returns, axis=1)
    end_prices = np.exp(log_prices[:, -1])

    prob = np.mean(end_prices >= start_price)
    return round(prob, 4)

Volatility Estimation (rolling)

def estimate_volatility(price_history, window=300):
    """
    Оценка σ из последних N секунд Binance тиков
    Более точная чем фиксированная 60% annual
    """
    if len(price_history) < 10:
        return 0.60  # default 60% annual

    prices = [p for _, p in price_history[-window:]]
    log_returns = np.diff(np.log(prices))

    if len(log_returns) < 5:
        return 0.60

    # Per-tick vol → annualize
    avg_dt = (price_history[-1][0] - price_history[-window][0]) / len(prices)
    if avg_dt <= 0:
        return 0.60

    vol_per_tick = np.std(log_returns)
    vol_annual = vol_per_tick * np.sqrt(365.25 * 24 * 3600 / avg_dt)

    return min(max(vol_annual, 0.20), 2.0)  # clamp 20%-200%

📐 Шаг 4: Edge Detection + Entry Logic

Main Loop (каждую секунду в последние 60 сек окна)

async def check_opportunity(window):
    """
    window: {
        "market_id": "...",
        "yes_token": "...",
        "no_token": "...",
        "start_price": 67234.50,   # Chainlink start
        "start_time": 1714200300,  # T=0 unix
        "end_time": 1714200600,    # T=300 unix
    }
    """
    now = time.time()
    remaining = window["end_time"] - now

    # Только входим в последние 60 секунд
    if remaining > 60 or remaining < 5:
        return None

    # Наша модель
    current_price = binance_feed.current_price
    start_price = window["start_price"]
    vol = estimate_volatility(binance_feed.price_history)

    model_prob_up = prob_up_brownian(current_price, start_price, remaining, vol)
    model_prob_down = 1 - model_prob_up

    # Polymarket текущие цены
    yes_price = get_polymarket_price(window["yes_token"])  # "Up"
    no_price = get_polymarket_price(window["no_token"])     # "Down"

    # Edge calculation (с учётом fees)
    # Maker fee = 0%, но если taker (FOK) = 1.8%
    # Используем limit orders → 0% fee
    edge_up = model_prob_up - yes_price
    edge_down = model_prob_down - no_price

    MIN_EDGE = 0.08  # 8% минимальный edge (строже чем weather — выше risk)

    if edge_up > MIN_EDGE:
        return {
            "side": "YES",  # bet UP
            "token": window["yes_token"],
            "price": yes_price + 0.01,  # чуть выше bid для быстрого fill
            "edge": edge_up,
            "model_prob": model_prob_up,
            "market_prob": yes_price,
            "remaining_sec": remaining
        }
    elif edge_down > MIN_EDGE:
        return {
            "side": "NO",  # bet DOWN
            "token": window["no_token"],
            "price": no_price + 0.01,
            "edge": edge_down,
            "model_prob": model_prob_down,
            "market_prob": no_price,
            "remaining_sec": remaining
        }

    return None

Timing Strategy

T=0-240s   → WAIT (собираем данные, считаем vol)
T=240-270s → SCAN (проверяем edge каждую секунду)
T=270-295s → EXECUTE (если edge есть — ставим limit order)
T=295-300s → DEADLINE (не входим — ордер может не заполниться)
T=300s     → RESOLUTION (авто, ждём результат)

💰 Шаг 5: Executor + Kelly Criterion

Kelly Criterion для сайзинга

def kelly_size(prob, odds, bankroll, fraction=0.25):
    """
    Kelly Criterion — оптимальный размер ставки

    prob: наша оценка P(win)
    odds: сколько получим за $1 (= 1/price - 1)
    bankroll: текущий банкролл
    fraction: fractional Kelly (0.25 = quarter Kelly — консервативно)
    """
    if prob <= 0 or odds <= 0:
        return 0

    q = 1 - prob
    kelly_f = (prob * odds - q) / odds
    kelly_f = max(0, kelly_f)  # не ставить если negative edge

    # Fractional Kelly (safer)
    bet_size = bankroll * kelly_f * fraction

    # Clamps
    bet_size = min(bet_size, bankroll * 0.05)  # max 5% банкролла
    bet_size = min(bet_size, 100)               # max $100 per trade
    bet_size = max(bet_size, 5)                 # min $5

    return round(bet_size, 2)

# Пример: P=0.65, price=$0.52, bankroll=$500
# odds = 1/0.52 - 1 = 0.923
# kelly_f = (0.65*0.923 - 0.35)/0.923 = 0.271
# bet = $500 * 0.271 * 0.25 = $33.88 → clamped to $25 (5% of $500)

Order Execution

async def execute_bet(client, signal, bankroll):
    size = kelly_size(
        prob=signal["model_prob"],
        odds=1/signal["price"] - 1,
        bankroll=bankroll
    )

    order = OrderArgs(
        token_id=signal["token"],
        price=round(signal["price"], 2),
        size=size,
        side=BUY
    )

    signed = client.create_order(order)
    response = client.post_order(signed, OrderType.GTC)

    # Если не заполнился за 10 сек → cancel
    await asyncio.sleep(10)
    if not order_filled(response["id"]):
        client.cancel(response["id"])
        return None

    return response

🛡️ Шаг 6: Risk Manager

Правила

RISK_CONFIG = {
    "max_per_trade": 100,          # макс $100 на 1 окно
    "max_bankroll_pct": 0.05,      # макс 5% банкролла
    "max_daily_trades": 50,        # не больше 50 сделок/день
    "max_daily_loss": 150,         # -$150/день → стоп
    "max_consecutive_losses": 5,   # 5 лоссов подряд → пауза 1ч
    "min_edge": 0.08,              # 8% минимальный edge
    "min_remaining_sec": 10,       # не ставить если <10 сек до resolution
    "max_remaining_sec": 60,       # не ставить если >60 сек (слишком рано)
    "min_volume": 1000,            # skip рынки с volume < $1K
    "circuit_breaker_pct": 0.10,   # -10% банкролла за день → full stop
    "skip_low_vol_hours": True,    # пропускать 3-7 AM ET (низкий объём)
}

Фильтры по времени

# BTC волатильность зависит от времени суток
HIGH_VOL_HOURS = [
    (13, 17),  # US market open (1-5 PM ET) — лучший edge
    (8, 12),   # European session
    (21, 1),   # Asian open
]
# 3-7 AM ET — мёртвая зона, skip

📱 Шаг 7: Telegram Notifications

⚡ BTC 5M BET
Window: 1:05–1:10 AM ET
BTC: $67,450 | Start: $67,234 (+0.32%)
Model P(Up): 72% | Market: 58% | Edge: 14%
→ BUY YES @ $0.59 × $25
Remaining: 28s | Vol: $12.4K

✅ WIN — BTC UP
Window: 1:05–1:10 AM ET
End: $67,502 (≥ Start $67,234)
Profit: +$17.37 (69% ROI on bet)

📊 DAILY SUMMARY
Windows traded: 34/288
Won: 21 | Lost: 13 | WR: 61.8%
P&L: +$89.50 | Bankroll: $589.50
Best edge: 22% | Avg edge: 11.3%

📁 Структура проекта

/home/app/polymarket-btc-5m-bot/
├── .env                    # PRIVATE_KEY, TELEGRAM_TOKEN, POLYGON_RPC
├── ecosystem.config.js     # PM2 config
├── requirements.txt
├── bot.py                  # Main entry point + async event loop
├── binance_feed.py         # Binance WS BTC/USDT real-time
├── chainlink.py            # Chainlink oracle price + lag monitor
├── market_scanner.py       # Gamma API → current BTC 5-min markets
├── polymarket_ws.py        # Polymarket CLOB WS → live orderbook
├── probability.py          # Brownian motion + Monte Carlo models
├── edge_detector.py        # Model P vs Market P comparison
├── executor.py             # CLOB limit order placement
├── risk.py                 # Kelly sizing + risk limits + circuit breaker
├── tracker.py              # SQLite: trades, P&L, stats
├── notifier.py             # Telegram alerts
├── config.py               # Constants, risk params, timing
├── data/
│   └── btc-5m-bot.db      # SQLite database
└── logs/
    └── bot.log

🔑 Разница от Weather Bot

Параметр Weather Bot BTC 5M Bot
Частота 10-50 ставок/день 50-100 ставок/день
Окно решения 24 часа 5 минут
Edge source Прогноз точнее толпы Chainlink oracle lag
Win Rate 70-85% 55-65%
Тип edge Information Latency/Speed
Сложность ★★☆☆☆ ★★★☆☆
Real-time Нет (check every 5 min) Да (tick-by-tick)
Капитал $200-500 $500-1000
Риск Низкий Средний

🚀 План реализации (6 шагов)

# Шаг Что делаем Время
1 Scaffold + Auth Проект, .env, py-clob-client, тест connection 30 мин
2 Binance WS + Chainlink Real-time BTC feed + oracle monitor + lag measurement 1-2 часа
3 Market Scanner Gamma API → находим текущие BTC 5-min markets + token IDs 1 час
4 Probability + Edge Brownian motion model + edge detection + entry logic 1-2 часа
5 Executor + Risk Kelly sizing + limit orders + risk manager + circuit breaker 1-2 часа
6 Test + Deploy Тест с $5-10 реальных (3-5 окон), PM2, Telegram alerts 2-3 часа

Итого: 2-3 дня до рабочего бота.


⚠️ Критические моменты

  1. Timing критичен — вход в последние 30-60 сек, ордер должен заполниться за <10 сек
  2. Limit vs Market — limit = 0% fee, но может не заполниться. Market = 1.8% fee, гарантированный fill. Trade-off!
  3. Chainlink адрес — верифицировать что Polymarket использует именно этот feed
  4. Volatility spikes — во время новостей (FOMC, CPI) модель может врать. Можно skip эти окна или наоборот — больший edge
  5. Конкуренция — десятки ботов уже фармят эти рынки, средний arb window = 2.7 сек
  6. Polygon RPC — бесплатный может быть медленным, рассмотреть Alchemy/QuickNode ($0)
  7. web3==6.14.0 — пиннить!
  8. Нет testnet — тест с $5 реальных

💵 Стартовые затраты

Что Сколько
USDC на Polygon $500-1000
MATIC на газ $2-3
Polygon RPC $0 (free tier)
Binance WS $0
Итого $502-1003

🤔 Open Questions (обсудить с Rick)

  1. Начинать с Weather Bot или параллельно оба? Weather проще, BTC потенциально прибыльнее
  2. Shared infrastructure — оба бота используют py-clob-client, SQLite, Telegram. Общий monorepo или отдельные?
  3. Капитал — $200 на weather + $500 на BTC = $700 total. Ок?
  4. Polygon wallet — создать новый отдельный или есть?
  5. Конкуренция на BTC — 2.7 сек окно арбитража. Наш VPS в Ванкувере достаточно быстрый? Может нужен VPS ближе к Polygon node?