@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/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.WSSubscriptionManager = void 0;
|
|
18
|
+
var WSSubscriptionManager_1 = require("./WSSubscriptionManager");
|
|
19
|
+
Object.defineProperty(exports, "WSSubscriptionManager", { enumerable: true, get: function () { return WSSubscriptionManager_1.WSSubscriptionManager; } });
|
|
20
|
+
__exportStar(require("./types/PolymarketWebSocket"), exports);
|
|
21
|
+
__exportStar(require("./types/WebSocketSubscriptions"), exports);
|
package/dist/logger.d.ts
ADDED
package/dist/logger.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.logger = void 0;
|
|
7
|
+
const winston_1 = __importDefault(require("winston"));
|
|
8
|
+
// Override with LOG_LEVEL environment variable (e.g., LOG_LEVEL=info npm start)
|
|
9
|
+
exports.logger = winston_1.default.createLogger({
|
|
10
|
+
level: process.env.LOG_LEVEL || 'warn',
|
|
11
|
+
format: winston_1.default.format.combine(winston_1.default.format.timestamp(), winston_1.default.format.errors({ stack: true }), winston_1.default.format.colorize(), winston_1.default.format.printf(({ level, message, timestamp, ...rest }) => {
|
|
12
|
+
// Ensure consistent order: timestamp, level, message, then rest of fields
|
|
13
|
+
const restString = Object.keys(rest)
|
|
14
|
+
.filter(key => key !== 'service') // Exclude service since we add it in defaultMeta
|
|
15
|
+
.sort()
|
|
16
|
+
.map(key => `${key}: ${JSON.stringify(rest[key])}`)
|
|
17
|
+
.join(', ');
|
|
18
|
+
return `${timestamp} ${level}: ${message}${restString ? ` (${restString})` : ''}`;
|
|
19
|
+
})),
|
|
20
|
+
defaultMeta: { service: 'poly-websockets' },
|
|
21
|
+
transports: [
|
|
22
|
+
new winston_1.default.transports.Console({
|
|
23
|
+
format: winston_1.default.format.combine(winston_1.default.format.colorize({
|
|
24
|
+
all: true,
|
|
25
|
+
colors: {
|
|
26
|
+
error: 'red',
|
|
27
|
+
warn: 'yellow',
|
|
28
|
+
info: 'cyan',
|
|
29
|
+
debug: 'green'
|
|
30
|
+
}
|
|
31
|
+
}))
|
|
32
|
+
})
|
|
33
|
+
]
|
|
34
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { BookEvent, PriceChangeEvent, PriceLevel } from '../types/PolymarketWebSocket';
|
|
2
|
+
export interface BookEntry {
|
|
3
|
+
bids: PriceLevel[];
|
|
4
|
+
asks: PriceLevel[];
|
|
5
|
+
price: string | null;
|
|
6
|
+
midpoint: string | null;
|
|
7
|
+
spread: string | null;
|
|
8
|
+
}
|
|
9
|
+
export declare class OrderBookCache {
|
|
10
|
+
private bookCache;
|
|
11
|
+
constructor();
|
|
12
|
+
/**
|
|
13
|
+
* Replace full book (after a `book` event)
|
|
14
|
+
* @param event new orderbook event
|
|
15
|
+
*/
|
|
16
|
+
replaceBook(event: BookEvent): void;
|
|
17
|
+
/**
|
|
18
|
+
* Update a cached book from a `price_change` event.
|
|
19
|
+
*
|
|
20
|
+
* @param event PriceChangeEvent
|
|
21
|
+
* @returns true if the book was updated.
|
|
22
|
+
* @throws if the book is not found.
|
|
23
|
+
*/
|
|
24
|
+
upsertPriceChange(event: PriceChangeEvent): void;
|
|
25
|
+
/**
|
|
26
|
+
* Side effect: updates the book's spread
|
|
27
|
+
*
|
|
28
|
+
* @returns `true` if best-bid/best-ask spread exceeds `cents`.
|
|
29
|
+
* @throws if either side of the book is empty.
|
|
30
|
+
*/
|
|
31
|
+
spreadOver(assetId: string, cents?: number): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Calculate the midpoint of the book, rounded to 3dp, no trailing zeros
|
|
34
|
+
*
|
|
35
|
+
* Side effect: updates the book's midpoint
|
|
36
|
+
*
|
|
37
|
+
* Throws if
|
|
38
|
+
* - the book is not found or missing either bid or ask
|
|
39
|
+
* - the midpoint is NaN.
|
|
40
|
+
*/
|
|
41
|
+
midpoint(assetId: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Removes a specific market from the orderbook if assetId is provided
|
|
44
|
+
* otherwise clears all orderbook
|
|
45
|
+
* @param assetId tokenId of a market
|
|
46
|
+
*/
|
|
47
|
+
clear(assetId?: string): void;
|
|
48
|
+
/**
|
|
49
|
+
* Get a book entry by asset id.
|
|
50
|
+
*
|
|
51
|
+
* @returns book entry if found, otherwise null
|
|
52
|
+
*/
|
|
53
|
+
getBookEntry(assetId: string): BookEntry | null;
|
|
54
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OrderBookCache = void 0;
|
|
4
|
+
function sortDescendingInPlace(bookSide) {
|
|
5
|
+
bookSide.sort((a, b) => parseFloat(b.price) - parseFloat(a.price));
|
|
6
|
+
}
|
|
7
|
+
function sortAscendingInPlace(bookSide) {
|
|
8
|
+
bookSide.sort((a, b) => parseFloat(a.price) - parseFloat(b.price));
|
|
9
|
+
}
|
|
10
|
+
class OrderBookCache {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.bookCache = {};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Replace full book (after a `book` event)
|
|
16
|
+
* @param event new orderbook 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
|
+
* @param event PriceChangeEvent
|
|
47
|
+
* @returns true if the book was updated.
|
|
48
|
+
* @throws if the book is not found.
|
|
49
|
+
*/
|
|
50
|
+
upsertPriceChange(event) {
|
|
51
|
+
// Iterate through price_changes array
|
|
52
|
+
for (const priceChange of event.price_changes) {
|
|
53
|
+
const book = this.bookCache[priceChange.asset_id];
|
|
54
|
+
if (!book) {
|
|
55
|
+
throw new Error(`Book not found for asset ${priceChange.asset_id}`);
|
|
56
|
+
}
|
|
57
|
+
const { price, size, side } = priceChange;
|
|
58
|
+
const sizeNum = parseFloat(size);
|
|
59
|
+
if (side === 'BUY') {
|
|
60
|
+
const i = book.bids.findIndex(bid => bid.price === price);
|
|
61
|
+
if (i !== -1) {
|
|
62
|
+
// Remove entry if size is zero or effectively zero
|
|
63
|
+
if (sizeNum === 0 || size === '0') {
|
|
64
|
+
book.bids.splice(i, 1);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
book.bids[i].size = size;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else if (sizeNum > 0) {
|
|
71
|
+
// Only add if size is non-zero
|
|
72
|
+
book.bids.push({ price, size });
|
|
73
|
+
// Ensure the bids are sorted ascending
|
|
74
|
+
sortAscendingInPlace(book.bids);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
const i = book.asks.findIndex(ask => ask.price === price);
|
|
79
|
+
if (i !== -1) {
|
|
80
|
+
// Remove entry if size is zero or effectively zero
|
|
81
|
+
if (sizeNum === 0 || size === '0') {
|
|
82
|
+
book.asks.splice(i, 1);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
book.asks[i].size = size;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (sizeNum > 0) {
|
|
89
|
+
// Only add if size is non-zero
|
|
90
|
+
book.asks.push({ price, size });
|
|
91
|
+
// Ensure the asks are sorted descending
|
|
92
|
+
sortDescendingInPlace(book.asks);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Side effect: updates the book's spread
|
|
99
|
+
*
|
|
100
|
+
* @returns `true` if best-bid/best-ask spread exceeds `cents`.
|
|
101
|
+
* @throws if either side of the book is empty.
|
|
102
|
+
*/
|
|
103
|
+
spreadOver(assetId, cents = 0.1) {
|
|
104
|
+
const book = this.bookCache[assetId];
|
|
105
|
+
if (!book)
|
|
106
|
+
throw new Error(`Book for ${assetId} not cached`);
|
|
107
|
+
if (book.asks.length === 0)
|
|
108
|
+
throw new Error(`No asks in book for ${assetId}`);
|
|
109
|
+
if (book.bids.length === 0)
|
|
110
|
+
throw new Error(`No bids in book for ${assetId}`);
|
|
111
|
+
/*
|
|
112
|
+
* Polymarket book events are currently sorted as such:
|
|
113
|
+
* - bids ascending
|
|
114
|
+
* - asks descending
|
|
115
|
+
*/
|
|
116
|
+
const highestBid = book.bids[book.bids.length - 1].price;
|
|
117
|
+
const lowestAsk = book.asks[book.asks.length - 1].price;
|
|
118
|
+
const highestBidNum = parseFloat(highestBid);
|
|
119
|
+
const lowestAskNum = parseFloat(lowestAsk);
|
|
120
|
+
const spread = lowestAskNum - highestBidNum;
|
|
121
|
+
if (isNaN(spread)) {
|
|
122
|
+
throw new Error(`Spread is NaN: lowestAsk '${lowestAsk}' highestBid '${highestBid}'`);
|
|
123
|
+
}
|
|
124
|
+
/*
|
|
125
|
+
* Update spead, 3 precision decimal places, trim trailing zeros
|
|
126
|
+
*/
|
|
127
|
+
book.spread = parseFloat(spread.toFixed(3)).toString();
|
|
128
|
+
// Should be safe for 0.### - precision values
|
|
129
|
+
return spread > cents;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Calculate the midpoint of the book, rounded to 3dp, no trailing zeros
|
|
133
|
+
*
|
|
134
|
+
* Side effect: updates the book's midpoint
|
|
135
|
+
*
|
|
136
|
+
* Throws if
|
|
137
|
+
* - the book is not found or missing either bid or ask
|
|
138
|
+
* - the midpoint is NaN.
|
|
139
|
+
*/
|
|
140
|
+
midpoint(assetId) {
|
|
141
|
+
const book = this.bookCache[assetId];
|
|
142
|
+
if (!book)
|
|
143
|
+
throw new Error(`Book for ${assetId} not cached`);
|
|
144
|
+
if (book.asks.length === 0)
|
|
145
|
+
throw new Error(`No asks in book for ${assetId}`);
|
|
146
|
+
if (book.bids.length === 0)
|
|
147
|
+
throw new Error(`No bids in book for ${assetId}`);
|
|
148
|
+
/*
|
|
149
|
+
* Polymarket book events are currently sorted as such:
|
|
150
|
+
* - bids ascending
|
|
151
|
+
* - asks descending
|
|
152
|
+
*/
|
|
153
|
+
const highestBid = book.bids[book.bids.length - 1].price;
|
|
154
|
+
const lowestAsk = book.asks[book.asks.length - 1].price;
|
|
155
|
+
const highestBidNum = parseFloat(highestBid);
|
|
156
|
+
const lowestAskNum = parseFloat(lowestAsk);
|
|
157
|
+
const midpoint = (highestBidNum + lowestAskNum) / 2;
|
|
158
|
+
if (isNaN(midpoint)) {
|
|
159
|
+
throw new Error(`Midpoint is NaN: lowestAsk '${lowestAsk}' highestBid '${highestBid}'`);
|
|
160
|
+
}
|
|
161
|
+
/*
|
|
162
|
+
* Update midpoint, 3 precision decimal places, trim trailing zeros
|
|
163
|
+
*/
|
|
164
|
+
book.midpoint = parseFloat(midpoint.toFixed(3)).toString();
|
|
165
|
+
return parseFloat(midpoint.toFixed(3)).toString();
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Removes a specific market from the orderbook if assetId is provided
|
|
169
|
+
* otherwise clears all orderbook
|
|
170
|
+
* @param assetId tokenId of a market
|
|
171
|
+
*/
|
|
172
|
+
clear(assetId) {
|
|
173
|
+
if (assetId) {
|
|
174
|
+
delete this.bookCache[assetId];
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
for (const k of Object.keys(this.bookCache)) {
|
|
178
|
+
delete this.bookCache[k];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Get a book entry by asset id.
|
|
184
|
+
*
|
|
185
|
+
* @returns book entry if found, otherwise null
|
|
186
|
+
*/
|
|
187
|
+
getBookEntry(assetId) {
|
|
188
|
+
if (!this.bookCache[assetId]) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return this.bookCache[assetId];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
exports.OrderBookCache = OrderBookCache;
|