@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/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { WSSubscriptionManager, WebSocketHandlers } from './WSSubscriptionManager';
2
+ export * from './types/PolymarketWebSocket';
3
+ export * from './types/WebSocketSubscriptions';
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
+ }