PolyTools Trade Cost 0.1.1 / src/content/live-book.js

142 lines · 5,400 bytes · SHA-256 5ed9fa43605a3bd8972725b78b75a7aa3505a99b141331591605a1c2b3355ca6

1/*
2 * PolyTools — live order book over the public CLOB market WebSocket.
3 * wss://ws-subscriptions-clob.polymarket.com/ws/market (docs.polymarket.com/market-data/realtime-data, 2026-10-29)
4 * subscribe {assets_ids:[token], type:'market'} → "book" snapshot, then "price_change" deltas (size = new level
5 * size, 0 removes the level; side BUY = bids, SELL = asks), "tick_size_change"; heartbeat: send "PING" every 10 s.
6 * The first frame is an array. polymarket.com's CSP allows wss://*.polymarket.com, so this runs in the content script.
7 * create(onChange) → { subscribe(token), close(), raw() → CLOB-shaped book or null, ready, token, lastMsg, tick }
8 */
9(function (root) {
10 'use strict';
11
12 const WS_URL = 'wss://ws-subscriptions-clob.polymarket.com/ws/market';
13 const WATCHDOG_MS = 30000;
14
15 function create(onChange, opts) {
16 const o = Object.assign({ pingMs: 10000, watchdogMs: WATCHDOG_MS }, opts);
17 let ws = null;
18 let token = null;
19 let wanted = false;
20 let ready = false;
21 let bids = new Map();
22 let asks = new Map();
23 let tick = null;
24 let lastMsg = 0;
25 let lastFrame = 0; // any frame incl. PONG: detects half-open sockets
26 let ping = null;
27 let retry = 0;
28 let retryTimer = null;
29
30 const key = (p) => String(Number(p));
31 const toMap = (levels) => {
32 const m = new Map();
33 for (const l of levels || []) { const s = Number(l.size); if (s > 0) m.set(key(l.price), s); }
34 return m;
35 };
36
37 function apply(m) {
38 if (!m || typeof m !== 'object') return false;
39 if (m.event_type === 'book' && String(m.asset_id) === token) {
40 bids = toMap(m.bids); asks = toMap(m.asks); ready = true; lastMsg = Date.now();
41 retry = 0; // a snapshot means the subscription really works: reset the backoff here
42 return true;
43 }
44 if (m.event_type === 'price_change' && Array.isArray(m.price_changes) && ready) {
45 let hit = false;
46 for (const c of m.price_changes) {
47 if (String(c.asset_id) !== token) continue;
48 const side = c.side === 'BUY' ? bids : asks;
49 const s = Number(c.size);
50 if (s > 0) side.set(key(c.price), s); else side.delete(key(c.price));
51 hit = true;
52 }
53 if (hit) lastMsg = Date.now();
54 return hit;
55 }
56 if (m.event_type === 'tick_size_change' && String(m.asset_id) === token) {
57 tick = Number(m.new_tick_size) || tick;
58 return true;
59 }
60 return false;
61 }
62
63 function open() {
64 if (!wanted || !token) return;
65 let sock;
66 try { sock = new WebSocket(WS_URL); } catch (e) { reconnect(); return; }
67 ws = sock;
68 sock.onopen = () => {
69 if (sock !== ws) return;
70 lastFrame = Date.now();
71 sock.send(JSON.stringify({ assets_ids: [token], type: 'market' }));
72 clearInterval(ping);
73 ping = setInterval(() => {
74 if (sock !== ws) return;
75 // no frame (not even PONG) for 30 s: half-open after sleep or a network switch → drop and reconnect
76 if (Date.now() - lastFrame > o.watchdogMs) { try { sock.close(); } catch (_) { /* ignore */ } return; }
77 if (sock.readyState === 1) sock.send('PING');
78 }, o.pingMs);
79 };
80 sock.onmessage = (e) => {
81 if (sock !== ws) return;
82 lastFrame = Date.now();
83 if (e.data === 'PONG') return;
84 let d;
85 try { d = JSON.parse(e.data); } catch (_) { return; }
86 let changed = false;
87 for (const m of Array.isArray(d) ? d : [d]) changed = apply(m) || changed;
88 if (changed) onChange();
89 };
90 sock.onclose = () => {
91 if (sock !== ws) return;
92 clearInterval(ping);
93 ws = null;
94 const was = ready;
95 ready = false;
96 reconnect();
97 if (was) onChange(); // fall back to polling until the socket is back
98 };
99 sock.onerror = () => { try { sock.close(); } catch (_) { /* ignore */ } };
100 }
101
102 function reconnect() {
103 if (!wanted) return;
104 clearTimeout(retryTimer);
105 retryTimer = setTimeout(() => { retryTimer = null; open(); }, Math.min(30000, 1000 * 2 ** retry++));
106 }
107
108 function close() {
109 wanted = false;
110 clearTimeout(retryTimer); retryTimer = null;
111 clearInterval(ping); ping = null;
112 const sock = ws;
113 ws = null;
114 ready = false;
115 if (sock) { try { sock.close(); } catch (_) { /* ignore */ } }
116 }
117
118 function subscribe(t) {
119 if (!t) { close(); return; }
120 if (t === token && wanted) return;
121 close();
122 token = String(t); wanted = true; retry = 0;
123 bids = new Map(); asks = new Map(); tick = null; lastMsg = 0;
124 open();
125 }
126
127 function raw() {
128 if (!ready) return null;
129 const lv = (m) => [...m].map(([price, size]) => ({ price, size: String(size) }));
130 return { bids: lv(bids), asks: lv(asks), timestamp: String(lastMsg) };
131 }
132
133 return {
134 subscribe, close, raw,
135 get ready() { return ready; }, get token() { return token; }, get lastMsg() { return lastMsg; }, get tick() { return tick; },
136 };
137 }
138
139 root.PWL = root.PWL || {};
140 root.PWL.liveBook = { create, WS_URL };
141 if (typeof module !== 'undefined' && module.exports) module.exports = root.PWL.liveBook;
142})(typeof globalThis !== 'undefined' ? globalThis : this);