@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.
- package/LICENSE +661 -0
- package/README.md +251 -0
- package/dist/WSSubscriptionManager.d.ts +18 -0
- package/dist/WSSubscriptionManager.js +174 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +21 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.js +34 -0
- package/dist/modules/GroupRegistry.d.ts +85 -0
- package/dist/modules/GroupRegistry.js +261 -0
- package/dist/modules/GroupSocket.d.ts +22 -0
- package/dist/modules/GroupSocket.js +311 -0
- package/dist/modules/OrderBookCache.d.ts +49 -0
- package/dist/modules/OrderBookCache.js +173 -0
- package/dist/types/PolymarketWebSocket.d.ts +242 -0
- package/dist/types/PolymarketWebSocket.js +50 -0
- package/dist/types/WebSocketSubscriptions.d.ts +19 -0
- package/dist/types/WebSocketSubscriptions.js +10 -0
- package/package.json +49 -0
- package/src/WSSubscriptionManager.ts +201 -0
- package/src/index.ts +3 -0
- package/src/logger.ts +37 -0
- package/src/modules/GroupRegistry.ts +274 -0
- package/src/modules/GroupSocket.ts +338 -0
- package/src/modules/OrderBookCache.ts +208 -0
- package/src/types/PolymarketWebSocket.ts +280 -0
- package/src/types/WebSocketSubscriptions.ts +26 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import Bottleneck from 'bottleneck';
|
|
3
|
+
import { logger } from '../logger';
|
|
4
|
+
import { WebSocketGroup, WebSocketStatus } from '../types/WebSocketSubscriptions';
|
|
5
|
+
import { BookEntry, OrderBookCache } from './OrderBookCache';
|
|
6
|
+
import {
|
|
7
|
+
BookEvent,
|
|
8
|
+
isBookEvent,
|
|
9
|
+
isLastTradePriceEvent,
|
|
10
|
+
isPriceChangeEvent,
|
|
11
|
+
isTickSizeChangeEvent,
|
|
12
|
+
LastTradePriceEvent,
|
|
13
|
+
PriceChangeEvent,
|
|
14
|
+
TickSizeChangeEvent,
|
|
15
|
+
PolymarketWSEvent,
|
|
16
|
+
WebSocketHandlers,
|
|
17
|
+
PolymarketPriceUpdateEvent,
|
|
18
|
+
} from '../types/PolymarketWebSocket';
|
|
19
|
+
import _ from 'lodash';
|
|
20
|
+
import ms from 'ms';
|
|
21
|
+
import { randomInt } from 'crypto';
|
|
22
|
+
|
|
23
|
+
const CLOB_WSS_URL = 'wss://ws-subscriptions-clob.polymarket.com/ws/market';
|
|
24
|
+
|
|
25
|
+
export class GroupSocket {
|
|
26
|
+
private pingInterval?: NodeJS.Timeout;
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
private group: WebSocketGroup,
|
|
30
|
+
private limiter: Bottleneck,
|
|
31
|
+
private bookCache: OrderBookCache,
|
|
32
|
+
private handlers: WebSocketHandlers,
|
|
33
|
+
) {}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Establish the websocket connection using the provided Bottleneck limiter.
|
|
37
|
+
*
|
|
38
|
+
*/
|
|
39
|
+
public async connect(): Promise<void> {
|
|
40
|
+
if (this.group.assetIds.size === 0) {
|
|
41
|
+
this.group.status = WebSocketStatus.CLEANUP;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
logger.info({
|
|
47
|
+
message: 'Connecting to CLOB WebSocket',
|
|
48
|
+
groupId: this.group.groupId,
|
|
49
|
+
assetIdsLength: this.group.assetIds.size,
|
|
50
|
+
});
|
|
51
|
+
this.group.wsClient = await this.limiter.schedule({ priority: 0 }, async () => {
|
|
52
|
+
const ws = new WebSocket(CLOB_WSS_URL);
|
|
53
|
+
/*
|
|
54
|
+
This handler will be replaced by the handlers in setupEventHandlers
|
|
55
|
+
*/
|
|
56
|
+
ws.on('error', (err) => {
|
|
57
|
+
logger.warn({
|
|
58
|
+
message: 'Error connecting to CLOB WebSocket',
|
|
59
|
+
error: err,
|
|
60
|
+
groupId: this.group.groupId,
|
|
61
|
+
assetIdsLength: this.group.assetIds.size,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
return ws;
|
|
65
|
+
});
|
|
66
|
+
} catch (err) {
|
|
67
|
+
this.group.status = WebSocketStatus.DEAD;
|
|
68
|
+
throw err; // caller responsible for error handler
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.setupEventHandlers();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private setupEventHandlers() {
|
|
75
|
+
const group = this.group;
|
|
76
|
+
const handlers = this.handlers;
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
Define handlers within this scope to capture 'this' context
|
|
80
|
+
*/
|
|
81
|
+
const handleOpen = async () => {
|
|
82
|
+
if (group.assetIds.size === 0) {
|
|
83
|
+
group.status = WebSocketStatus.CLEANUP;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
group.status = WebSocketStatus.ALIVE;
|
|
88
|
+
|
|
89
|
+
group.wsClient!.send(JSON.stringify({ assets_ids: Array.from(group.assetIds), type: 'market' }));
|
|
90
|
+
await handlers.onWSOpen?.(group.groupId, Array.from(group.assetIds));
|
|
91
|
+
|
|
92
|
+
this.pingInterval = setInterval(() => {
|
|
93
|
+
if (group.assetIds.size === 0) {
|
|
94
|
+
clearInterval(this.pingInterval);
|
|
95
|
+
group.status = WebSocketStatus.CLEANUP;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!group.wsClient) {
|
|
100
|
+
clearInterval(this.pingInterval);
|
|
101
|
+
group.status = WebSocketStatus.DEAD;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
group.wsClient.ping();
|
|
105
|
+
}, randomInt(ms('15s'), ms('25s')));
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const handleMessage = async (data: Buffer) => {
|
|
109
|
+
let events: PolymarketWSEvent[] = [];
|
|
110
|
+
try {
|
|
111
|
+
const parsedData: any = JSON.parse(data.toString());
|
|
112
|
+
events = Array.isArray(parsedData) ? parsedData : [parsedData];
|
|
113
|
+
} catch (err) {
|
|
114
|
+
await handlers.onError?.(new Error(`Not JSON: ${data.toString()}`));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
events = _.filter(events, (event: PolymarketWSEvent) => _.size(event.asset_id) > 0);
|
|
119
|
+
|
|
120
|
+
const bookEvents: BookEvent[] = [];
|
|
121
|
+
const lastTradeEvents: LastTradePriceEvent[] = [];
|
|
122
|
+
const tickEvents: TickSizeChangeEvent[] = [];
|
|
123
|
+
const priceChangeEvents: PriceChangeEvent[] = [];
|
|
124
|
+
|
|
125
|
+
for (const event of events) {
|
|
126
|
+
/*
|
|
127
|
+
Skip events for asset ids that are not in the group to ensure that
|
|
128
|
+
we don't get stale events for assets that were removed.
|
|
129
|
+
*/
|
|
130
|
+
if (!group.assetIds.has(event.asset_id)) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (isBookEvent(event)) {
|
|
135
|
+
bookEvents.push(event);
|
|
136
|
+
} else if (isLastTradePriceEvent(event)) {
|
|
137
|
+
lastTradeEvents.push(event);
|
|
138
|
+
} else if (isTickSizeChangeEvent(event)) {
|
|
139
|
+
tickEvents.push(event);
|
|
140
|
+
} else if (isPriceChangeEvent(event)) {
|
|
141
|
+
priceChangeEvents.push(event);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
await handlers.onError?.(new Error(`Unknown event: ${JSON.stringify(event)}`));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await this.handleBookEvents(bookEvents);
|
|
149
|
+
await this.handleTickEvents(tickEvents);
|
|
150
|
+
await this.handlePriceChangeEvents(priceChangeEvents);
|
|
151
|
+
await this.handleLastTradeEvents(lastTradeEvents);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const handlePong = () => {
|
|
155
|
+
group.groupId;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const handleError = (err: Error) => {
|
|
159
|
+
group.status = WebSocketStatus.DEAD;
|
|
160
|
+
clearInterval(this.pingInterval);
|
|
161
|
+
handlers.onError?.(new Error(`WebSocket error for group ${group.groupId}: ${err.message}`));
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const handleClose = async (code: number, reason: Buffer) => {
|
|
165
|
+
group.status = WebSocketStatus.DEAD;
|
|
166
|
+
clearInterval(this.pingInterval);
|
|
167
|
+
await handlers.onWSClose?.(group.groupId, code, reason.toString());
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (group.wsClient) {
|
|
171
|
+
// Remove any existing handlers
|
|
172
|
+
group.wsClient.removeAllListeners();
|
|
173
|
+
|
|
174
|
+
// Add the handlers
|
|
175
|
+
group.wsClient.on('open', handleOpen);
|
|
176
|
+
group.wsClient.on('message', handleMessage);
|
|
177
|
+
group.wsClient.on('pong', handlePong);
|
|
178
|
+
group.wsClient.on('error', handleError);
|
|
179
|
+
group.wsClient.on('close', handleClose);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (group.assetIds.size === 0) {
|
|
183
|
+
group.status = WebSocketStatus.CLEANUP;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!group.wsClient) {
|
|
188
|
+
group.status = WebSocketStatus.DEAD;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private async handleBookEvents(bookEvents: BookEvent[]): Promise<void> {
|
|
194
|
+
if (bookEvents.length) {
|
|
195
|
+
for (const event of bookEvents) {
|
|
196
|
+
this.bookCache.replaceBook(event);
|
|
197
|
+
}
|
|
198
|
+
await this.handlers.onBook?.(bookEvents);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private async handleTickEvents(tickEvents: TickSizeChangeEvent[]): Promise<void> {
|
|
203
|
+
if (tickEvents.length) {
|
|
204
|
+
await this.handlers.onTickSizeChange?.(tickEvents);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async handlePriceChangeEvents(priceChangeEvents: PriceChangeEvent[]): Promise<void> {
|
|
209
|
+
if (priceChangeEvents.length) {
|
|
210
|
+
await this.handlers.onPriceChange?.(priceChangeEvents);
|
|
211
|
+
|
|
212
|
+
for (const event of priceChangeEvents) {
|
|
213
|
+
try {
|
|
214
|
+
this.bookCache.upsertPriceChange(event);
|
|
215
|
+
} catch (err) {
|
|
216
|
+
logger.warn({
|
|
217
|
+
message: `Skipping price_change: book not found for asset`,
|
|
218
|
+
asset_id: event.asset_id,
|
|
219
|
+
event: event,
|
|
220
|
+
error: err
|
|
221
|
+
});
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let spreadOver10Cents: boolean;
|
|
226
|
+
try {
|
|
227
|
+
spreadOver10Cents = this.bookCache.spreadOver(event.asset_id, 0.1);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
logger.warn({
|
|
230
|
+
message: 'Skipping price_change: error calculating spread',
|
|
231
|
+
asset_id: event.asset_id,
|
|
232
|
+
event: event,
|
|
233
|
+
error: err
|
|
234
|
+
});
|
|
235
|
+
await this.handlers.onError?.(err as Error);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!spreadOver10Cents) {
|
|
240
|
+
let newPrice: string;
|
|
241
|
+
try {
|
|
242
|
+
newPrice = this.bookCache.midpoint(event.asset_id);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
logger.warn({
|
|
245
|
+
message: 'Skipping price_change: error calculating midpoint',
|
|
246
|
+
asset_id: event.asset_id,
|
|
247
|
+
event: event,
|
|
248
|
+
error: err
|
|
249
|
+
});
|
|
250
|
+
await this.handlers.onError?.(err as Error);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const bookEntry: BookEntry | null = this.bookCache.getBookEntry(event.asset_id);
|
|
255
|
+
if (!bookEntry) {
|
|
256
|
+
logger.warn({
|
|
257
|
+
message: 'Skipping price_change: book not found for asset',
|
|
258
|
+
asset_id: event.asset_id,
|
|
259
|
+
event: event,
|
|
260
|
+
});
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (newPrice !== bookEntry.price) {
|
|
265
|
+
bookEntry.price = newPrice;
|
|
266
|
+
const priceUpdateEvent: PolymarketPriceUpdateEvent = {
|
|
267
|
+
asset_id: event.asset_id,
|
|
268
|
+
event_type: 'price_update',
|
|
269
|
+
triggeringEvent: event,
|
|
270
|
+
timestamp: event.timestamp,
|
|
271
|
+
book: { bids: bookEntry.bids, asks: bookEntry.asks },
|
|
272
|
+
price: newPrice,
|
|
273
|
+
midpoint: bookEntry.midpoint || '',
|
|
274
|
+
spread: bookEntry.spread || '',
|
|
275
|
+
};
|
|
276
|
+
await this.handlers.onPolymarketPriceUpdate?.([priceUpdateEvent]);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private async handleLastTradeEvents(lastTradeEvents: LastTradePriceEvent[]): Promise<void> {
|
|
284
|
+
if (lastTradeEvents.length) {
|
|
285
|
+
/*
|
|
286
|
+
Note: There is no need to edit the book here. According to the docs, a separate
|
|
287
|
+
book event is sent when a trade affects the book.
|
|
288
|
+
|
|
289
|
+
See: https://docs.polymarket.com/developers/CLOB/websocket/market-channel#book-message
|
|
290
|
+
*/
|
|
291
|
+
await this.handlers.onLastTradePrice?.(lastTradeEvents);
|
|
292
|
+
|
|
293
|
+
for (const event of lastTradeEvents) {
|
|
294
|
+
let spreadOver10Cents: boolean;
|
|
295
|
+
try {
|
|
296
|
+
spreadOver10Cents = this.bookCache.spreadOver(event.asset_id, 0.1);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
logger.warn({
|
|
299
|
+
message: 'Skipping last_trade_price: error calculating spread',
|
|
300
|
+
asset_id: event.asset_id,
|
|
301
|
+
event: event,
|
|
302
|
+
error: err
|
|
303
|
+
});
|
|
304
|
+
await this.handlers.onError?.(err as Error);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (spreadOver10Cents) {
|
|
308
|
+
// Ensure no trailing zeros
|
|
309
|
+
const newPrice = parseFloat(event.price).toString();
|
|
310
|
+
|
|
311
|
+
const bookEntry: BookEntry | null = this.bookCache.getBookEntry(event.asset_id);
|
|
312
|
+
if (!bookEntry) {
|
|
313
|
+
logger.warn({
|
|
314
|
+
message: 'Skipping last_trade_price: book not found for asset',
|
|
315
|
+
asset_id: event.asset_id,
|
|
316
|
+
event: event,
|
|
317
|
+
});
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (newPrice !== bookEntry.price) {
|
|
321
|
+
bookEntry.price = newPrice;
|
|
322
|
+
const priceUpdateEvent: PolymarketPriceUpdateEvent = {
|
|
323
|
+
asset_id: event.asset_id,
|
|
324
|
+
event_type: 'price_update',
|
|
325
|
+
triggeringEvent: event,
|
|
326
|
+
timestamp: event.timestamp,
|
|
327
|
+
book: { bids: bookEntry.bids, asks: bookEntry.asks },
|
|
328
|
+
price: newPrice,
|
|
329
|
+
midpoint: bookEntry.midpoint || '',
|
|
330
|
+
spread: bookEntry.spread || '',
|
|
331
|
+
};
|
|
332
|
+
await this.handlers.onPolymarketPriceUpdate?.([priceUpdateEvent]);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import {
|
|
3
|
+
BookEvent,
|
|
4
|
+
PriceChangeEvent,
|
|
5
|
+
PriceLevel,
|
|
6
|
+
} from '../types/PolymarketWebSocket';
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* Shared book cache store – exported so legacy code paths can keep using it
|
|
10
|
+
* until the refactor is complete.
|
|
11
|
+
*/
|
|
12
|
+
export interface BookEntry {
|
|
13
|
+
bids: PriceLevel[];
|
|
14
|
+
asks: PriceLevel[];
|
|
15
|
+
price: string | null;
|
|
16
|
+
midpoint: string | null;
|
|
17
|
+
spread: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export
|
|
21
|
+
|
|
22
|
+
function sortDescendingInPlace(bookSide: PriceLevel[]): void {
|
|
23
|
+
bookSide.sort((a, b) => parseFloat(b.price) - parseFloat(a.price));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sortAscendingInPlace(bookSide: PriceLevel[]): void {
|
|
27
|
+
bookSide.sort((a, b) => parseFloat(a.price) - parseFloat(b.price));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class OrderBookCache {
|
|
31
|
+
private bookCache: {
|
|
32
|
+
[assetId: string]: BookEntry
|
|
33
|
+
} = {};
|
|
34
|
+
|
|
35
|
+
constructor() {}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Replace full book (after a `book` event)
|
|
39
|
+
*/
|
|
40
|
+
public replaceBook(event: BookEvent): void {
|
|
41
|
+
let lastPrice = null;
|
|
42
|
+
let lastMidpoint = null;
|
|
43
|
+
let lastSpread = null;
|
|
44
|
+
if (this.bookCache[event.asset_id]) {
|
|
45
|
+
lastPrice = this.bookCache[event.asset_id].price;
|
|
46
|
+
lastMidpoint = this.bookCache[event.asset_id].midpoint;
|
|
47
|
+
lastSpread = this.bookCache[event.asset_id].spread;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.bookCache[event.asset_id] = {
|
|
51
|
+
bids: [...event.bids],
|
|
52
|
+
asks: [...event.asks],
|
|
53
|
+
price: lastPrice,
|
|
54
|
+
midpoint: lastMidpoint,
|
|
55
|
+
spread: lastSpread,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/* Polymarket book events are currently sorted as such:
|
|
59
|
+
* - bids (buys) ascending
|
|
60
|
+
* - asks (sells) descending
|
|
61
|
+
*
|
|
62
|
+
* So we maintain this order in the cache.
|
|
63
|
+
*/
|
|
64
|
+
sortAscendingInPlace(this.bookCache[event.asset_id].bids);
|
|
65
|
+
sortDescendingInPlace(this.bookCache[event.asset_id].asks);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Update a cached book from a `price_change` event.
|
|
70
|
+
*
|
|
71
|
+
* Returns true if the book was updated.
|
|
72
|
+
* Throws if the book is not found.
|
|
73
|
+
*/
|
|
74
|
+
public upsertPriceChange(event: PriceChangeEvent): void {
|
|
75
|
+
const book = this.bookCache[event.asset_id];
|
|
76
|
+
if (!book) {
|
|
77
|
+
throw new Error(`Book not found for asset ${event.asset_id}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const change of event.changes) {
|
|
81
|
+
const { price, size, side } = change;
|
|
82
|
+
if (side === 'BUY') {
|
|
83
|
+
const i = book.bids.findIndex(bid => bid.price === price);
|
|
84
|
+
if (i !== -1) {
|
|
85
|
+
book.bids[i].size = size;
|
|
86
|
+
} else {
|
|
87
|
+
book.bids.push({ price, size });
|
|
88
|
+
|
|
89
|
+
// Ensure the bids are sorted ascending
|
|
90
|
+
sortAscendingInPlace(book.bids);
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
const i = book.asks.findIndex(ask => ask.price === price);
|
|
94
|
+
if (i !== -1) {
|
|
95
|
+
book.asks[i].size = size;
|
|
96
|
+
} else {
|
|
97
|
+
book.asks.push({ price, size });
|
|
98
|
+
|
|
99
|
+
// Ensure the asks are sorted descending
|
|
100
|
+
sortDescendingInPlace(book.asks);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Return `true` if best-bid/best-ask spread exceeds `cents`.
|
|
108
|
+
*
|
|
109
|
+
* Side effect: updates the book's spread
|
|
110
|
+
*
|
|
111
|
+
* Throws if either side of the book is empty.
|
|
112
|
+
*/
|
|
113
|
+
public spreadOver(assetId: string, cents = 0.1): boolean {
|
|
114
|
+
const book = this.bookCache[assetId];
|
|
115
|
+
if (!book) throw new Error(`Book for ${assetId} not cached`);
|
|
116
|
+
if (book.asks.length === 0) throw new Error('No asks in book');
|
|
117
|
+
if (book.bids.length === 0) throw new Error('No bids in book');
|
|
118
|
+
|
|
119
|
+
/*
|
|
120
|
+
* Polymarket book events are currently sorted as such:
|
|
121
|
+
* - bids ascending
|
|
122
|
+
* - asks descending
|
|
123
|
+
*/
|
|
124
|
+
|
|
125
|
+
const highestBid = book.bids[book.bids.length - 1].price;
|
|
126
|
+
const lowestAsk = book.asks[book.asks.length - 1].price;
|
|
127
|
+
|
|
128
|
+
const highestBidNum = parseFloat(highestBid);
|
|
129
|
+
const lowestAskNum = parseFloat(lowestAsk);
|
|
130
|
+
|
|
131
|
+
const spread = lowestAskNum - highestBidNum;
|
|
132
|
+
|
|
133
|
+
if (isNaN(spread)) {
|
|
134
|
+
throw new Error(`Spread is NaN: lowestAsk '${lowestAsk}' highestBid '${highestBid}'`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/*
|
|
138
|
+
* Update spead, 3 precision decimal places, trim trailing zeros
|
|
139
|
+
*/
|
|
140
|
+
book.spread = parseFloat(spread.toFixed(3)).toString();
|
|
141
|
+
|
|
142
|
+
// Should be safe for 0.### - precision values
|
|
143
|
+
return spread > cents;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Calculate the midpoint of the book, rounded to 3dp, no trailing zeros
|
|
148
|
+
*
|
|
149
|
+
* Side effect: updates the book's midpoint
|
|
150
|
+
*
|
|
151
|
+
* Throws if
|
|
152
|
+
* - the book is not found or missing either bid or ask
|
|
153
|
+
* - the midpoint is NaN.
|
|
154
|
+
*/
|
|
155
|
+
public midpoint(assetId: string): string {
|
|
156
|
+
const book = this.bookCache[assetId];
|
|
157
|
+
if (!book) throw new Error(`Book for ${assetId} not cached`);
|
|
158
|
+
if (book.asks.length === 0) throw new Error('No asks in book');
|
|
159
|
+
if (book.bids.length === 0) throw new Error('No bids in book');
|
|
160
|
+
|
|
161
|
+
/*
|
|
162
|
+
* Polymarket book events are currently sorted as such:
|
|
163
|
+
* - bids ascending
|
|
164
|
+
* - asks descending
|
|
165
|
+
*/
|
|
166
|
+
const highestBid = book.bids[book.bids.length - 1].price;
|
|
167
|
+
const lowestAsk = book.asks[book.asks.length - 1].price;
|
|
168
|
+
|
|
169
|
+
const highestBidNum = parseFloat(highestBid);
|
|
170
|
+
const lowestAskNum = parseFloat(lowestAsk);
|
|
171
|
+
|
|
172
|
+
const midpoint = (highestBidNum + lowestAskNum) / 2;
|
|
173
|
+
|
|
174
|
+
if (isNaN(midpoint)) {
|
|
175
|
+
throw new Error(`Midpoint is NaN: lowestAsk '${lowestAsk}' highestBid '${highestBid}'`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/*
|
|
179
|
+
* Update midpoint, 3 precision decimal places, trim trailing zeros
|
|
180
|
+
*/
|
|
181
|
+
book.midpoint = parseFloat(midpoint.toFixed(3)).toString();
|
|
182
|
+
|
|
183
|
+
return parseFloat(midpoint.toFixed(3)).toString();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
public clear(assetId?: string): void {
|
|
187
|
+
if (assetId) {
|
|
188
|
+
delete this.bookCache[assetId];
|
|
189
|
+
} else {
|
|
190
|
+
for (const k of Object.keys(this.bookCache)) {
|
|
191
|
+
delete this.bookCache[k];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get a book entry by asset id.
|
|
198
|
+
*
|
|
199
|
+
* Return null if the book is not found.
|
|
200
|
+
*/
|
|
201
|
+
public getBookEntry(assetId: string): BookEntry | null {
|
|
202
|
+
if (!this.bookCache[assetId]) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
return this.bookCache[assetId];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
}
|