Бот для last-second momentum arbitrage на BTC 5/15-минутных prediction markets
Бот мониторит цену BTC на Binance в реальном времени и ставит на Polymarket BTC Up/Down рынки в последние 30-60 секунд окна, эксплуатируя Chainlink oracle lag (2-15 сек).
Целевые метрики:
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)
┌──────────────────────────────────────────────────────┐
│ 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 |
| Компонент | Технология | Почему |
|---|---|---|
| Язык | 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 |
py-clob-client>=0.1.0
web3==6.14.0
websockets
aiohttp
numpy
scipy
python-dotenv
loguru
python-telegram-bot
apscheduler
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
]
Chainlink — это oracle который Polymarket использует для resolution. Мы мониторим его чтобы:
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
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%
}
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)
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)
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%
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
T=0-240s → WAIT (собираем данные, считаем vol)
T=240-270s → SCAN (проверяем edge каждую секунду)
T=270-295s → EXECUTE (если edge есть — ставим limit order)
T=295-300s → DEADLINE (не входим — ордер может не заполниться)
T=300s → RESOLUTION (авто, ждём результат)
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)
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
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
⚡ 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 | 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 |
| Риск | Низкий | Средний |
| # | Шаг | Что делаем | Время |
|---|---|---|---|
| 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 дня до рабочего бота.
| Что | Сколько |
|---|---|
| USDC на Polygon | $500-1000 |
| MATIC на газ | $2-3 |
| Polygon RPC | $0 (free tier) |
| Binance WS | $0 |
| Итого | $502-1003 |