@nevuamarkets/poly-websockets 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OrderBookCache = void 0;
4
+ exports.sortDescendingInPlace = sortDescendingInPlace;
5
+ function sortDescendingInPlace(bookSide) {
6
+ bookSide.sort((a, b) => parseFloat(b.price) - parseFloat(a.price));
7
+ }
8
+ function sortAscendingInPlace(bookSide) {
9
+ bookSide.sort((a, b) => parseFloat(a.price) - parseFloat(b.price));
10
+ }
11
+ class OrderBookCache {
12
+ constructor() {
13
+ this.bookCache = {};
14
+ }
15
+ /**
16
+ * Replace full book (after a `book` event)
17
+ */
18
+ replaceBook(event) {
19
+ let lastPrice = null;
20
+ let lastMidpoint = null;
21
+ let lastSpread = null;
22
+ if (this.bookCache[event.asset_id]) {
23
+ lastPrice = this.bookCache[event.asset_id].price;
24
+ lastMidpoint = this.bookCache[event.asset_id].midpoint;
25
+ lastSpread = this.bookCache[event.asset_id].spread;
26
+ }
27
+ this.bookCache[event.asset_id] = {
28
+ bids: [...event.bids],
29
+ asks: [...event.asks],
30
+ price: lastPrice,
31
+ midpoint: lastMidpoint,
32
+ spread: lastSpread,
33
+ };
34
+ /* Polymarket book events are currently sorted as such:
35
+ * - bids (buys) ascending
36
+ * - asks (sells) descending
37
+ *
38
+ * So we maintain this order in the cache.
39
+ */
40
+ sortAscendingInPlace(this.bookCache[event.asset_id].bids);
41
+ sortDescendingInPlace(this.bookCache[event.asset_id].asks);
42
+ }
43
+ /**
44
+ * Update a cached book from a `price_change` event.
45
+ *
46
+ * Returns true if the book was updated.
47
+ * Throws if the book is not found.
48
+ */
49
+ upsertPriceChange(event) {
50
+ const book = this.bookCache[event.asset_id];
51
+ if (!book) {
52
+ throw new Error(`Book not found for asset ${event.asset_id}`);
53
+ }
54
+ for (const change of event.changes) {
55
+ const { price, size, side } = change;
56
+ if (side === 'BUY') {
57
+ const i = book.bids.findIndex(bid => bid.price === price);
58
+ if (i !== -1) {
59
+ book.bids[i].size = size;
60
+ }
61
+ else {
62
+ book.bids.push({ price, size });
63
+ // Ensure the bids are sorted ascending
64
+ sortAscendingInPlace(book.bids);
65
+ }
66
+ }
67
+ else {
68
+ const i = book.asks.findIndex(ask => ask.price === price);
69
+ if (i !== -1) {
70
+ book.asks[i].size = size;
71
+ }
72
+ else {
73
+ book.asks.push({ price, size });
74
+ // Ensure the asks are sorted descending
75
+ sortDescendingInPlace(book.asks);
76
+ }
77
+ }
78
+ }
79
+ }
80
+ /**
81
+ * Return `true` if best-bid/best-ask spread exceeds `cents`.
82
+ *
83
+ * Side effect: updates the book's spread
84
+ *
85
+ * Throws if either side of the book is empty.
86
+ */
87
+ spreadOver(assetId, cents = 0.1) {
88
+ const book = this.bookCache[assetId];
89
+ if (!book)
90
+ throw new Error(`Book for ${assetId} not cached`);
91
+ if (book.asks.length === 0)
92
+ throw new Error('No asks in book');
93
+ if (book.bids.length === 0)
94
+ throw new Error('No bids in book');
95
+ /*
96
+ * Polymarket book events are currently sorted as such:
97
+ * - bids ascending
98
+ * - asks descending
99
+ */
100
+ const highestBid = book.bids[book.bids.length - 1].price;
101
+ const lowestAsk = book.asks[book.asks.length - 1].price;
102
+ const highestBidNum = parseFloat(highestBid);
103
+ const lowestAskNum = parseFloat(lowestAsk);
104
+ const spread = lowestAskNum - highestBidNum;
105
+ if (isNaN(spread)) {
106
+ throw new Error(`Spread is NaN: lowestAsk '${lowestAsk}' highestBid '${highestBid}'`);
107
+ }
108
+ /*
109
+ * Update spead, 3 precision decimal places, trim trailing zeros
110
+ */
111
+ book.spread = parseFloat(spread.toFixed(3)).toString();
112
+ // Should be safe for 0.### - precision values
113
+ return spread > cents;
114
+ }
115
+ /**
116
+ * Calculate the midpoint of the book, rounded to 3dp, no trailing zeros
117
+ *
118
+ * Side effect: updates the book's midpoint
119
+ *
120
+ * Throws if
121
+ * - the book is not found or missing either bid or ask
122
+ * - the midpoint is NaN.
123
+ */
124
+ midpoint(assetId) {
125
+ const book = this.bookCache[assetId];
126
+ if (!book)
127
+ throw new Error(`Book for ${assetId} not cached`);
128
+ if (book.asks.length === 0)
129
+ throw new Error('No asks in book');
130
+ if (book.bids.length === 0)
131
+ throw new Error('No bids in book');
132
+ /*
133
+ * Polymarket book events are currently sorted as such:
134
+ * - bids ascending
135
+ * - asks descending
136
+ */
137
+ const highestBid = book.bids[book.bids.length - 1].price;
138
+ const lowestAsk = book.asks[book.asks.length - 1].price;
139
+ const highestBidNum = parseFloat(highestBid);
140
+ const lowestAskNum = parseFloat(lowestAsk);
141
+ const midpoint = (highestBidNum + lowestAskNum) / 2;
142
+ if (isNaN(midpoint)) {
143
+ throw new Error(`Midpoint is NaN: lowestAsk '${lowestAsk}' highestBid '${highestBid}'`);
144
+ }
145
+ /*
146
+ * Update midpoint, 3 precision decimal places, trim trailing zeros
147
+ */
148
+ book.midpoint = parseFloat(midpoint.toFixed(3)).toString();
149
+ return parseFloat(midpoint.toFixed(3)).toString();
150
+ }
151
+ clear(assetId) {
152
+ if (assetId) {
153
+ delete this.bookCache[assetId];
154
+ }
155
+ else {
156
+ for (const k of Object.keys(this.bookCache)) {
157
+ delete this.bookCache[k];
158
+ }
159
+ }
160
+ }
161
+ /**
162
+ * Get a book entry by asset id.
163
+ *
164
+ * Return null if the book is not found.
165
+ */
166
+ getBookEntry(assetId) {
167
+ if (!this.bookCache[assetId]) {
168
+ return null;
169
+ }
170
+ return this.bookCache[assetId];
171
+ }
172
+ }
173
+ exports.OrderBookCache = OrderBookCache;
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Represents a single price level in the order book
3
+ * @example
4
+ * { price: "0.01", size: "510000" }
5
+ */
6
+ export type PriceLevel = {
7
+ price: string;
8
+ size: string;
9
+ };
10
+ /**
11
+ * Represents a price_change event from Polymarket WebSocket
12
+ * @example
13
+ * {
14
+ * asset_id: "39327269875426915204597944387916069897800289788920336317845465327697809453999",
15
+ * changes: [
16
+ * { price: "0.044", side: "SELL", size: "611" }
17
+ * ],
18
+ * event_type: "price_change",
19
+ * hash: "a0b7cadf869fc288dbbf65704996fe818cc97d6a",
20
+ * market: "0x5412ae25e97078f814157de948459d59c6221b4c4c495fdd57b536543ad36729",
21
+ * timestamp: "1749371014925"
22
+ * }
23
+ */
24
+ export type PriceChangeEvent = {
25
+ asset_id: string;
26
+ changes: {
27
+ price: string;
28
+ side: string;
29
+ size: string;
30
+ }[];
31
+ event_type: 'price_change';
32
+ hash: string;
33
+ market: string;
34
+ timestamp: string;
35
+ };
36
+ /**
37
+ * Represents a Polymarket book
38
+ * @example
39
+ * {
40
+ * bids: [
41
+ * { price: "0.01", size: "510000" },
42
+ * { price: "0.02", size: "3100" }
43
+ * ],
44
+ * asks: [
45
+ * { price: "0.99", size: "58.07" },
46
+ * { price: "0.97", size: "178.73" }
47
+ * }
48
+ */
49
+ export type Book = {
50
+ bids: PriceLevel[];
51
+ asks: PriceLevel[];
52
+ };
53
+ /**
54
+ * Represents a book event from Polymarket WebSocket
55
+ * @example
56
+ * {
57
+ * market: "0xf83fb46dd70a4459fcc441a8511701c463374c5c3c250f585d74fda85ddfb7c9",
58
+ * asset_id: "101007741586870489619361069512452187353898396425142157315847015703471254508752",
59
+ * timestamp: "1740759191594",
60
+ * hash: "c0e51b1cfdbcb1b2aec58feaf7b01004019a89c6",
61
+ * bids: [
62
+ * { price: "0.01", size: "510000" },
63
+ * { price: "0.02", size: "3100" }
64
+ * ],
65
+ * asks: [
66
+ * { price: "0.99", size: "58.07" },
67
+ * { price: "0.97", size: "178.73" }
68
+ * ],
69
+ * event_type: "book"
70
+ * }
71
+ */
72
+ export type BookEvent = {
73
+ market: string;
74
+ asset_id: string;
75
+ timestamp: string;
76
+ hash: string;
77
+ bids: PriceLevel[];
78
+ asks: PriceLevel[];
79
+ event_type: 'book';
80
+ };
81
+ /**
82
+ * Represents a last trade price event from Polymarket WebSocket
83
+ * @example
84
+ * {
85
+ * asset_id: "101007741586870489619361069512452187353898396425142157315847015703471254508752",
86
+ * event_type: "last_trade_price",
87
+ * fee_rate_bps: "0",
88
+ * market: "0xf83fb46dd70a4459fcc441a8511701c463374c5c3c250f585d74fda85ddfb7c9",
89
+ * price: "0.12",
90
+ * side: "BUY",
91
+ * size: "8.333332",
92
+ * timestamp: "1740760245471"
93
+ * }
94
+ */
95
+ export type LastTradePriceEvent = {
96
+ asset_id: string;
97
+ event_type: 'last_trade_price';
98
+ fee_rate_bps: string;
99
+ market: string;
100
+ price: string;
101
+ side: 'BUY' | 'SELL';
102
+ size: string;
103
+ timestamp: string;
104
+ };
105
+ /**
106
+ * Represents a tick size change event from Polymarket WebSocket
107
+ * @example
108
+ * {
109
+ * event_type: "tick_size_change",
110
+ * asset_id: "65818619657568813474341868652308942079804919287380422192892211131408793125422",
111
+ * market: "0xbd31dc8a20211944f6b70f31557f1001557b59905b7738480ca09bd4532f84af",
112
+ * old_tick_size: "0.01",
113
+ * new_tick_size: "0.001",
114
+ * timestamp: "100000000"
115
+ * }
116
+ */
117
+ export type TickSizeChangeEvent = {
118
+ asset_id: string;
119
+ event_type: 'tick_size_change';
120
+ market: string;
121
+ old_tick_size: string;
122
+ new_tick_size: string;
123
+ timestamp: string;
124
+ };
125
+ /**
126
+ * Union type representing all possible event types from Polymarket WebSocket
127
+ * @example BookEvent
128
+ * {
129
+ * market: "0xf83fb46dd70a4459fcc441a8511701c463374c5c3c250f585d74fda85ddfb7c9",
130
+ * asset_id: "101007741586870489619361069512452187353898396425142157315847015703471254508752",
131
+ * timestamp: "1740759191594",
132
+ * hash: "c0e51b1cfdbcb1b2aec58feaf7b01004019a89c6",
133
+ * bids: [{ price: "0.01", size: "510000" }],
134
+ * asks: [{ price: "0.99", size: "58.07" }],
135
+ * event_type: "book"
136
+ * }
137
+ *
138
+ * @example LastTradePriceEvent
139
+ * {
140
+ * asset_id: "101007741586870489619361069512452187353898396425142157315847015703471254508752",
141
+ * event_type: "last_trade_price",
142
+ * fee_rate_bps: "0",
143
+ * market: "0xf83fb46dd70a4459fcc441a8511701c463374c5c3c250f585d74fda85ddfb7c9",
144
+ * price: "0.12",
145
+ * side: "BUY",
146
+ * size: "8.333332",
147
+ * timestamp: "1740760245471"
148
+ * }
149
+ *
150
+ * @example PriceChangeEvent
151
+ * {
152
+ * asset_id: "39327269875426915204597944387916069897800289788920336317845465327697809453999",
153
+ * changes: [
154
+ * { price: "0.044", side: "SELL", size: "611" }
155
+ * ],
156
+ * event_type: "price_change",
157
+ * hash: "a0b7cadf869fc288dbbf65704996fe818cc97d6a",
158
+ * market: "0x5412ae25e97078f814157de948459d59c6221b4c4c495fdd57b536543ad36729",
159
+ * timestamp: "1749371014925"
160
+ * }
161
+ *
162
+ * @example TickSizeChangeEvent
163
+ * {
164
+ * event_type: "tick_size_change",
165
+ * asset_id: "65818619657568813474341868652308942079804919287380422192892211131408793125422",
166
+ * market: "0xbd31dc8a20211944f6b70f31557f1001557b59905b7738480ca09bd4532f84af",
167
+ * old_tick_size: "0.01",
168
+ * new_tick_size: "0.001",
169
+ * timestamp: "100000000"
170
+ * }
171
+ */
172
+ export type PolymarketWSEvent = BookEvent | LastTradePriceEvent | PriceChangeEvent | TickSizeChangeEvent;
173
+ /**
174
+ * Represents a price update event
175
+ *
176
+ * This is an event that is emitted to faciliate price update events. It is
177
+ * not emitted by the Polymarket WebSocket directly.
178
+ *
179
+ * See https://docs.polymarket.com/polymarket-learn/trading/how-are-prices-calculated
180
+ *
181
+ * TLDR: The prices displayed on Polymarket are the midpoint of the bid-ask spread in the orderbook,
182
+ * UNLESS that spread is over $0.10, in which case the **last traded price** is used.
183
+ */
184
+ export interface PolymarketPriceUpdateEvent {
185
+ event_type: 'price_update';
186
+ asset_id: string;
187
+ timestamp: string;
188
+ triggeringEvent: LastTradePriceEvent | PriceChangeEvent;
189
+ book: Book;
190
+ price: string;
191
+ midpoint: string;
192
+ spread: string;
193
+ }
194
+ /**
195
+ * Represents the handlers for the Polymarket WebSocket
196
+ */
197
+ export type WebSocketHandlers = {
198
+ onBook?: (events: BookEvent[]) => Promise<void>;
199
+ onLastTradePrice?: (events: LastTradePriceEvent[]) => Promise<void>;
200
+ onTickSizeChange?: (events: TickSizeChangeEvent[]) => Promise<void>;
201
+ onPriceChange?: (events: PriceChangeEvent[]) => Promise<void>;
202
+ onPolymarketPriceUpdate?: (events: PolymarketPriceUpdateEvent[]) => Promise<void>;
203
+ onError?: (error: Error) => Promise<void>;
204
+ onWSClose?: (groupId: string, code: number, reason: string) => Promise<void>;
205
+ onWSOpen?: (groupId: string, assetIds: string[]) => Promise<void>;
206
+ };
207
+ /**
208
+ * Type guard to check if an event is a BookEvent
209
+ * @example
210
+ * if (isBookEvent(event)) {
211
+ * // event is now typed as BookEvent
212
+ * console.log(event.bids);
213
+ * }
214
+ */
215
+ export declare function isBookEvent(event: PolymarketWSEvent): event is BookEvent;
216
+ /**
217
+ * Type guard to check if an event is a LastTradePriceEvent
218
+ * @example
219
+ * if (isLastTradePriceEvent(event)) {
220
+ * // event is now typed as LastTradePriceEvent
221
+ * console.log(event.side);
222
+ * }
223
+ */
224
+ export declare function isLastTradePriceEvent(event: PolymarketWSEvent): event is LastTradePriceEvent;
225
+ /**
226
+ * Type guard to check if an event is a PriceChangeEvent
227
+ * @example
228
+ * if (isPriceChangeEvent(event)) {
229
+ * // event is now typed as PriceChangeEvent
230
+ * console.log(event.changes);
231
+ * }
232
+ */
233
+ export declare function isPriceChangeEvent(event: PolymarketWSEvent): event is PriceChangeEvent;
234
+ /**
235
+ * Type guard to check if an event is a TickSizeChangeEvent
236
+ * @example
237
+ * if (isTickSizeChangeEvent(event)) {
238
+ * // event is now typed as TickSizeChangeEvent
239
+ * console.log(event.old_tick_size);
240
+ * }
241
+ */
242
+ export declare function isTickSizeChangeEvent(event: PolymarketWSEvent): event is TickSizeChangeEvent;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isBookEvent = isBookEvent;
4
+ exports.isLastTradePriceEvent = isLastTradePriceEvent;
5
+ exports.isPriceChangeEvent = isPriceChangeEvent;
6
+ exports.isTickSizeChangeEvent = isTickSizeChangeEvent;
7
+ /**
8
+ * Type guard to check if an event is a BookEvent
9
+ * @example
10
+ * if (isBookEvent(event)) {
11
+ * // event is now typed as BookEvent
12
+ * console.log(event.bids);
13
+ * }
14
+ */
15
+ function isBookEvent(event) {
16
+ return (event === null || event === void 0 ? void 0 : event.event_type) === 'book';
17
+ }
18
+ /**
19
+ * Type guard to check if an event is a LastTradePriceEvent
20
+ * @example
21
+ * if (isLastTradePriceEvent(event)) {
22
+ * // event is now typed as LastTradePriceEvent
23
+ * console.log(event.side);
24
+ * }
25
+ */
26
+ function isLastTradePriceEvent(event) {
27
+ return (event === null || event === void 0 ? void 0 : event.event_type) === 'last_trade_price';
28
+ }
29
+ /**
30
+ * Type guard to check if an event is a PriceChangeEvent
31
+ * @example
32
+ * if (isPriceChangeEvent(event)) {
33
+ * // event is now typed as PriceChangeEvent
34
+ * console.log(event.changes);
35
+ * }
36
+ */
37
+ function isPriceChangeEvent(event) {
38
+ return (event === null || event === void 0 ? void 0 : event.event_type) === 'price_change';
39
+ }
40
+ /**
41
+ * Type guard to check if an event is a TickSizeChangeEvent
42
+ * @example
43
+ * if (isTickSizeChangeEvent(event)) {
44
+ * // event is now typed as TickSizeChangeEvent
45
+ * console.log(event.old_tick_size);
46
+ * }
47
+ */
48
+ function isTickSizeChangeEvent(event) {
49
+ return (event === null || event === void 0 ? void 0 : event.event_type) === 'tick_size_change';
50
+ }
@@ -0,0 +1,19 @@
1
+ import Bottleneck from 'bottleneck';
2
+ import WebSocket from 'ws';
3
+ export declare enum WebSocketStatus {
4
+ PENDING = "pending",// New group that is pending connection
5
+ ALIVE = "alive",// Group is connected and receiving events
6
+ DEAD = "dead",// Group is disconnected
7
+ CLEANUP = "cleanup"
8
+ }
9
+ export type WebSocketGroup = {
10
+ groupId: string;
11
+ assetIds: Set<string>;
12
+ wsClient: WebSocket | null;
13
+ status: WebSocketStatus;
14
+ };
15
+ export type SubscriptionManagerOptions = {
16
+ burstLimiter?: Bottleneck;
17
+ reconnectAndCleanupIntervalMs?: number;
18
+ maxMarketsPerWS?: number;
19
+ };
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebSocketStatus = void 0;
4
+ var WebSocketStatus;
5
+ (function (WebSocketStatus) {
6
+ WebSocketStatus["PENDING"] = "pending";
7
+ WebSocketStatus["ALIVE"] = "alive";
8
+ WebSocketStatus["DEAD"] = "dead";
9
+ WebSocketStatus["CLEANUP"] = "cleanup"; // Group is marked for cleanup
10
+ })(WebSocketStatus || (exports.WebSocketStatus = WebSocketStatus = {}));
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@nevuamarkets/poly-websockets",
3
+ "version": "0.0.1",
4
+ "description": "Plug-and-play Polymarket WebSocket price alerts",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "src"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "prepare": "npm run build",
14
+ "test": "vitest run"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/nevuamarkets/poly-websockets.git"
19
+ },
20
+ "keywords": [
21
+ "polymarket",
22
+ "websocket",
23
+ "price",
24
+ "market",
25
+ "alerts"
26
+ ],
27
+ "author": "Konstantinos Lekkas",
28
+ "license": "AGPL-3.0",
29
+ "bugs": {
30
+ "url": "https://github.com/nevuamarkets/poly-websockets/issues"
31
+ },
32
+ "homepage": "https://github.com/nevuamarkets/poly-websockets#readme",
33
+ "dependencies": {
34
+ "async-mutex": "^0.5.0",
35
+ "bottleneck": "^2.19.5",
36
+ "lodash": "^4.17.21",
37
+ "uuid": "^11.1.0",
38
+ "winston": "^3.17.0",
39
+ "ws": "^8.18.2"
40
+ },
41
+ "devDependencies": {
42
+ "@types/lodash": "^4.17.17",
43
+ "@types/ms": "^2.1.0",
44
+ "@types/ws": "^8.18.1",
45
+ "typescript": "^5.4.2",
46
+ "vitest": "^3.0.7",
47
+ "@types/node": "^22.13.11"
48
+ }
49
+ }