@tentou-tech/poly-websockets 1.0.0
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 +212 -0
- package/dist/WSSubscriptionManager.d.ts +160 -0
- package/dist/WSSubscriptionManager.js +1020 -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/OrderBookCache.d.ts +54 -0
- package/dist/modules/OrderBookCache.js +194 -0
- package/dist/types/PolymarketWebSocket.d.ts +460 -0
- package/dist/types/PolymarketWebSocket.js +86 -0
- package/dist/types/WebSocketSubscriptions.d.ts +32 -0
- package/dist/types/WebSocketSubscriptions.js +12 -0
- package/package.json +54 -0
- package/src/WSSubscriptionManager.ts +1126 -0
- package/src/index.ts +3 -0
- package/src/logger.ts +36 -0
- package/src/modules/OrderBookCache.ts +227 -0
- package/src/types/PolymarketWebSocket.ts +538 -0
- package/src/types/WebSocketSubscriptions.ts +35 -0
package/src/index.ts
ADDED
package/src/logger.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import winston from 'winston';
|
|
2
|
+
|
|
3
|
+
// Override with LOG_LEVEL environment variable (e.g., LOG_LEVEL=info npm start)
|
|
4
|
+
export const logger = winston.createLogger({
|
|
5
|
+
level: process.env.LOG_LEVEL || 'warn',
|
|
6
|
+
format: winston.format.combine(
|
|
7
|
+
winston.format.timestamp(),
|
|
8
|
+
winston.format.errors({ stack: true }),
|
|
9
|
+
winston.format.colorize(),
|
|
10
|
+
winston.format.printf(({ level, message, timestamp, ...rest }) => {
|
|
11
|
+
// Ensure consistent order: timestamp, level, message, then rest of fields
|
|
12
|
+
const restString = Object.keys(rest)
|
|
13
|
+
.filter(key => key !== 'service') // Exclude service since we add it in defaultMeta
|
|
14
|
+
.sort()
|
|
15
|
+
.map(key => `${key}: ${JSON.stringify(rest[key])}`)
|
|
16
|
+
.join(', ');
|
|
17
|
+
return `${timestamp} ${level}: ${message}${restString ? ` (${restString})` : ''}`;
|
|
18
|
+
})
|
|
19
|
+
),
|
|
20
|
+
defaultMeta: { service: 'poly-websockets' },
|
|
21
|
+
transports: [
|
|
22
|
+
new winston.transports.Console({
|
|
23
|
+
format: winston.format.combine(
|
|
24
|
+
winston.format.colorize({
|
|
25
|
+
all: true,
|
|
26
|
+
colors: {
|
|
27
|
+
error: 'red',
|
|
28
|
+
warn: 'yellow',
|
|
29
|
+
info: 'cyan',
|
|
30
|
+
debug: 'green'
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
)
|
|
34
|
+
})
|
|
35
|
+
]
|
|
36
|
+
});
|
|
@@ -0,0 +1,227 @@
|
|
|
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
|
+
|
|
21
|
+
function sortDescendingInPlace(bookSide: PriceLevel[]): void {
|
|
22
|
+
bookSide.sort((a, b) => parseFloat(b.price) - parseFloat(a.price));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sortAscendingInPlace(bookSide: PriceLevel[]): void {
|
|
26
|
+
bookSide.sort((a, b) => parseFloat(a.price) - parseFloat(b.price));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class OrderBookCache {
|
|
30
|
+
private bookCache: {
|
|
31
|
+
[assetId: string]: BookEntry
|
|
32
|
+
} = {};
|
|
33
|
+
|
|
34
|
+
constructor() {}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Replace full book (after a `book` event)
|
|
38
|
+
* @param event new orderbook 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
|
+
* @param event PriceChangeEvent
|
|
72
|
+
* @returns true if the book was updated.
|
|
73
|
+
* @throws if the book is not found.
|
|
74
|
+
*/
|
|
75
|
+
public upsertPriceChange(event: PriceChangeEvent): void {
|
|
76
|
+
// Iterate through price_changes array
|
|
77
|
+
for (const priceChange of event.price_changes) {
|
|
78
|
+
const book = this.bookCache[priceChange.asset_id];
|
|
79
|
+
if (!book) {
|
|
80
|
+
throw new Error(`Book not found for asset ${priceChange.asset_id}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { price, size, side } = priceChange;
|
|
84
|
+
const sizeNum = parseFloat(size);
|
|
85
|
+
|
|
86
|
+
if (side === 'BUY') {
|
|
87
|
+
const i = book.bids.findIndex(bid => bid.price === price);
|
|
88
|
+
if (i !== -1) {
|
|
89
|
+
// Remove entry if size is zero or effectively zero
|
|
90
|
+
if (sizeNum === 0 || size === '0') {
|
|
91
|
+
book.bids.splice(i, 1);
|
|
92
|
+
} else {
|
|
93
|
+
book.bids[i].size = size;
|
|
94
|
+
}
|
|
95
|
+
} else if (sizeNum > 0) {
|
|
96
|
+
// Only add if size is non-zero
|
|
97
|
+
book.bids.push({ price, size });
|
|
98
|
+
|
|
99
|
+
// Ensure the bids are sorted ascending
|
|
100
|
+
sortAscendingInPlace(book.bids);
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
const i = book.asks.findIndex(ask => ask.price === price);
|
|
104
|
+
if (i !== -1) {
|
|
105
|
+
// Remove entry if size is zero or effectively zero
|
|
106
|
+
if (sizeNum === 0 || size === '0') {
|
|
107
|
+
book.asks.splice(i, 1);
|
|
108
|
+
} else {
|
|
109
|
+
book.asks[i].size = size;
|
|
110
|
+
}
|
|
111
|
+
} else if (sizeNum > 0) {
|
|
112
|
+
// Only add if size is non-zero
|
|
113
|
+
book.asks.push({ price, size });
|
|
114
|
+
|
|
115
|
+
// Ensure the asks are sorted descending
|
|
116
|
+
sortDescendingInPlace(book.asks);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Side effect: updates the book's spread
|
|
124
|
+
*
|
|
125
|
+
* @returns `true` if best-bid/best-ask spread exceeds `cents`.
|
|
126
|
+
* @throws if either side of the book is empty.
|
|
127
|
+
*/
|
|
128
|
+
public spreadOver(assetId: string, cents = 0.1): boolean {
|
|
129
|
+
const book = this.bookCache[assetId];
|
|
130
|
+
if (!book) throw new Error(`Book for ${assetId} not cached`);
|
|
131
|
+
if (book.asks.length === 0) throw new Error(`No asks in book for ${assetId}`);
|
|
132
|
+
if (book.bids.length === 0) throw new Error(`No bids in book for ${assetId}`);
|
|
133
|
+
|
|
134
|
+
/*
|
|
135
|
+
* Polymarket book events are currently sorted as such:
|
|
136
|
+
* - bids ascending
|
|
137
|
+
* - asks descending
|
|
138
|
+
*/
|
|
139
|
+
|
|
140
|
+
const highestBid = book.bids[book.bids.length - 1].price;
|
|
141
|
+
const lowestAsk = book.asks[book.asks.length - 1].price;
|
|
142
|
+
|
|
143
|
+
const highestBidNum = parseFloat(highestBid);
|
|
144
|
+
const lowestAskNum = parseFloat(lowestAsk);
|
|
145
|
+
|
|
146
|
+
const spread = lowestAskNum - highestBidNum;
|
|
147
|
+
|
|
148
|
+
if (isNaN(spread)) {
|
|
149
|
+
throw new Error(`Spread is NaN: lowestAsk '${lowestAsk}' highestBid '${highestBid}'`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/*
|
|
153
|
+
* Update spead, 3 precision decimal places, trim trailing zeros
|
|
154
|
+
*/
|
|
155
|
+
book.spread = parseFloat(spread.toFixed(3)).toString();
|
|
156
|
+
|
|
157
|
+
// Should be safe for 0.### - precision values
|
|
158
|
+
return spread > cents;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Calculate the midpoint of the book, rounded to 3dp, no trailing zeros
|
|
163
|
+
*
|
|
164
|
+
* Side effect: updates the book's midpoint
|
|
165
|
+
*
|
|
166
|
+
* Throws if
|
|
167
|
+
* - the book is not found or missing either bid or ask
|
|
168
|
+
* - the midpoint is NaN.
|
|
169
|
+
*/
|
|
170
|
+
public midpoint(assetId: string): string {
|
|
171
|
+
const book = this.bookCache[assetId];
|
|
172
|
+
if (!book) throw new Error(`Book for ${assetId} not cached`);
|
|
173
|
+
if (book.asks.length === 0) throw new Error(`No asks in book for ${assetId}`);
|
|
174
|
+
if (book.bids.length === 0) throw new Error(`No bids in book for ${assetId}`);
|
|
175
|
+
|
|
176
|
+
/*
|
|
177
|
+
* Polymarket book events are currently sorted as such:
|
|
178
|
+
* - bids ascending
|
|
179
|
+
* - asks descending
|
|
180
|
+
*/
|
|
181
|
+
const highestBid = book.bids[book.bids.length - 1].price;
|
|
182
|
+
const lowestAsk = book.asks[book.asks.length - 1].price;
|
|
183
|
+
|
|
184
|
+
const highestBidNum = parseFloat(highestBid);
|
|
185
|
+
const lowestAskNum = parseFloat(lowestAsk);
|
|
186
|
+
|
|
187
|
+
const midpoint = (highestBidNum + lowestAskNum) / 2;
|
|
188
|
+
|
|
189
|
+
if (isNaN(midpoint)) {
|
|
190
|
+
throw new Error(`Midpoint is NaN: lowestAsk '${lowestAsk}' highestBid '${highestBid}'`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/*
|
|
194
|
+
* Update midpoint, 3 precision decimal places, trim trailing zeros
|
|
195
|
+
*/
|
|
196
|
+
book.midpoint = parseFloat(midpoint.toFixed(3)).toString();
|
|
197
|
+
|
|
198
|
+
return parseFloat(midpoint.toFixed(3)).toString();
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Removes a specific market from the orderbook if assetId is provided
|
|
202
|
+
* otherwise clears all orderbook
|
|
203
|
+
* @param assetId tokenId of a market
|
|
204
|
+
*/
|
|
205
|
+
public clear(assetId?: string): void {
|
|
206
|
+
if (assetId) {
|
|
207
|
+
delete this.bookCache[assetId];
|
|
208
|
+
} else {
|
|
209
|
+
for (const k of Object.keys(this.bookCache)) {
|
|
210
|
+
delete this.bookCache[k];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get a book entry by asset id.
|
|
217
|
+
*
|
|
218
|
+
* @returns book entry if found, otherwise null
|
|
219
|
+
*/
|
|
220
|
+
public getBookEntry(assetId: string): BookEntry | null {
|
|
221
|
+
if (!this.bookCache[assetId]) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
return this.bookCache[assetId];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
}
|