Files
py_polymarket_hft_mm/utils/orderbook.py
T

348 lines
11 KiB
Python

import os
import bisect
import time
import json
import logging
import threading
import websocket
from enum import Enum
from config import POLYMARKET_WS_MARKET_URL, TRADING_BPS_THRESHOLD
from py_clob_client import OrderArgs
from py_clob_client.order_builder.constants import BUY
from utils.clob_client import get_client
from utils.inventory import get_inventory
logger = logging.getLogger(__name__)
class SIGNALES(Enum):
UP = "UP"
DOWN = "DOWN"
NEUTRAL = "NEUTRAL"
class OrderBook:
def __init__(self, up_token_id: str, down_token_id: str, slug: str):
self.up_token_id = up_token_id
self.down_token_id = down_token_id
self.slug = slug
self.ws_url = POLYMARKET_WS_MARKET_URL
self.client = get_client()
self.orderbook = {
"best_bid": 0.0,
"best_ask": 0.0,
"last_update": None,
"order_book": {"bids": [], "asks": []},
}
self.signed_orders_cache = {}
self.ws = None
self.running = False
self.thread = None
self.lock = threading.Lock()
self.monitoring_thread = None
self.monitoring_running = False
self.last_signal = SIGNALES.NEUTRAL
self.inventory = 0
self.inventory_thread = None
self.inventory_running = False
self.create_signed_orders_cache()
def _on_message(self, ws, message):
try:
data = json.loads(message)
event_type = data.get("event_type")
if event_type == "book":
self._update_order_book_snapshot(data)
elif event_type == "price_change":
self._process_price_change(data)
except Exception as e:
logger.error(f"⚠️ Error processing WebSocket message: {e}")
def _on_error(self, ws, error):
logger.error(f"⚠️ WebSocket error: {error}")
def _on_close(self, ws, close_status_code, close_msg):
logger.info("🔌 WebSocket disconnected")
if self.running:
logger.info("🔄 Attempting reconnect...")
threading.Timer(0.1, self._connect).start()
def _on_open(self, ws):
logger.info("✅ WebSocket connected - Streaming prices for UP token only")
payload = {"type": "market", "assets_ids": [self.up_token_id]}
ws.send(json.dumps(payload))
def _connect(self):
if not self.running:
return
try:
self.ws = websocket.WebSocketApp(
self.ws_url,
on_open=self._on_open,
on_message=self._on_message,
on_error=self._on_error,
on_close=self._on_close,
)
self.ws.run_forever()
except Exception as e:
logger.error(f"❌ WebSocket connection error: {e}")
if self.running:
threading.Timer(0.1, self._connect).start()
def start(self):
if self.running:
logger.warning("⚠️ Price stream already running")
return
self.running = True
self.monitoring_running = True
self.inventory_running = True
self.thread = threading.Thread(target=self._connect, daemon=True)
self.thread.start()
self.monitoring_thread = threading.Thread(
target=self._continuous_trading_monitor, daemon=True
)
self.monitoring_thread.start()
self.inventory_thread = threading.Thread(
target=self._inventory_updater, daemon=True
)
self.inventory_thread.start()
logger.info(
"WebSocket price stream, trading monitor, and inventory updater started"
)
def stop(self):
self.running = False
self.monitoring_running = False
self.inventory_running = False
if self.ws:
self.ws.close()
logger.info(
"🛑 WebSocket price stream, trading monitor, and inventory updater stopped"
)
def _inventory_updater(self):
logger.info("Started inventory updater thread")
while self.inventory_running:
try:
self.inventory = get_inventory(self.slug)
except Exception as e:
logger.error(f"Error updating inventory: {e}")
time.sleep(2)
logger.info("Stopped inventory updater thread")
def is_connected(self):
with self.lock:
return self.orderbook["last_update"] is not None
def get_current_market_data(self):
if self.orderbook["last_update"] is None:
return None
orderbook = self.orderbook["order_book"]
bids = []
asks = []
for bid in orderbook.get("bids", []):
if isinstance(bid, dict):
bids.append([float(bid["price"]), float(bid["size"])])
else:
bids.append([float(bid[0]), float(bid[1])])
for ask in orderbook.get("asks", []):
if isinstance(ask, dict):
asks.append([float(ask["price"]), float(ask["size"])])
else:
asks.append([float(ask[0]), float(ask[1])])
bids.sort(key=lambda x: x[0], reverse=True)
asks.sort(key=lambda x: x[0])
if not bids or not asks:
return None
best_bid_price = bids[0][0]
best_bid_volume = bids[0][1]
best_ask_price = asks[0][0]
best_ask_volume = asks[0][1]
# Calculate micro-price
total_volume = best_bid_volume + best_ask_volume
if total_volume > 0:
micro_price = (
(best_bid_price * best_ask_volume) + (best_ask_price * best_bid_volume)
) / total_volume
else:
micro_price = (best_bid_price + best_ask_price) / 2
mid_price = (best_bid_price + best_ask_price) / 2
micro_vs_mid_bps = (micro_price - mid_price) * 10000
return {
"best_bid_price": best_bid_price,
"best_ask_price": best_ask_price,
"micro_price": micro_price,
"mid_price": mid_price,
"micro_vs_mid_bps": micro_vs_mid_bps,
"bids": bids,
"asks": asks,
}
def _continuous_trading_monitor(self):
logger.info("Started continuous trading monitor")
while self.monitoring_running:
try:
market_data = self.get_current_market_data()
if not market_data:
time.sleep(0.1)
continue
micro_vs_mid_bps = market_data["micro_vs_mid_bps"]
current_signal = None
if micro_vs_mid_bps > TRADING_BPS_THRESHOLD:
current_signal = SIGNALES.UP
elif micro_vs_mid_bps < -TRADING_BPS_THRESHOLD:
current_signal = SIGNALES.DOWN
else:
current_signal = SIGNALES.NEUTRAL
if current_signal and current_signal != self.last_signal:
self.last_signal = current_signal
time.sleep(0.005)
except Exception as e:
logger.error(f"Error in continuous trading monitor: {e}")
time.sleep(1)
logger.info("Stopped continuous trading monitor")
def create_signed_orders_cache(self):
start = time.time()
prices = [0.01]
while prices[-1] < 0.99:
prices.append(round(prices[-1] + 0.01, 2))
client = get_client()
for price in prices:
for token_id in [self.up_token_id, self.down_token_id]:
order_args = OrderArgs(
token_id=token_id,
price=price,
size=5,
side=BUY,
)
signed_order = client.create_order(order_args)
self.signed_orders_cache[(token_id, price)] = signed_order
end = time.time()
logger.info(
f"Pre-created signed orders cache for tokens in {round((end - start) * 1000)} milliseconds"
)
def clear_screen(self):
os.system("cls" if os.name == "nt" else "clear")
def _update_order_book_snapshot(self, new_orderbook):
asset_id = new_orderbook.get("asset_id")
# Only process UP token as down token is just the opposite side
if asset_id != self.up_token_id:
return
with self.lock:
self.orderbook["best_bid"] = (
new_orderbook["bids"][-1]["price"] if new_orderbook["bids"] else 0.0
)
self.orderbook["best_ask"] = (
new_orderbook["asks"][-1]["price"] if new_orderbook["asks"] else 0.0
)
self.orderbook["order_book"]["bids"] = new_orderbook.get("bids", [])
self.orderbook["order_book"]["asks"] = new_orderbook.get("asks", [])
self.orderbook["last_update"] = time.time()
def _update_orderbook_incremental(self, asset_id, update):
if asset_id != self.up_token_id:
return
price = float(update["price"])
side = update["side"]
size = float(update["size"])
self.orderbook["best_bid"] = float(update["best_bid"])
self.orderbook["best_ask"] = float(update["best_ask"])
orderbook = self.orderbook["order_book"]
book_side = orderbook["bids"] if side == "BUY" else orderbook["asks"]
if book_side and isinstance(book_side[0], dict):
book_side = [
[float(item["price"]), float(item["size"])] for item in book_side
]
if side == "BUY":
orderbook["bids"] = book_side
else:
orderbook["asks"] = book_side
idx = bisect.bisect_left(book_side, [price, 0])
if idx < len(book_side) and book_side[idx][0] == price:
if size == 0:
del book_side[idx]
else:
book_side[idx][1] = size
elif size > 0:
book_side.insert(idx, [price, size])
if size > 0:
if side == "BUY":
asks = orderbook["asks"]
if asks and isinstance(asks[0], dict):
asks = [
[float(item["price"]), float(item["size"])] for item in asks
]
orderbook["asks"] = asks
cull_idx = bisect.bisect_right(asks, [price, float("inf")])
orderbook["asks"] = asks[cull_idx:]
else: # side == "SELL"
bids = orderbook["bids"]
if bids and isinstance(bids[0], dict):
bids = [
[float(item["price"]), float(item["size"])] for item in bids
]
orderbook["bids"] = bids
cull_idx = bisect.bisect_left(bids, [price, 0])
orderbook["bids"] = bids[:cull_idx]
def _process_price_change(self, data):
price_changes = data.get("price_changes", [])
for change in price_changes:
asset_id = change.get("asset_id")
if asset_id != self.up_token_id:
continue
with self.lock:
self._update_orderbook_incremental(asset_id, change)
self.orderbook["last_update"] = time.time()