tardis-dev 16.4.2 → 16.5.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.
@@ -0,0 +1,195 @@
1
+ import { BookChange, BookTicker, Trade } from '../types.ts'
2
+ import { asNonZeroNumberOrUndefined } from '../handy.ts'
3
+ import { Mapper } from './mapper.ts'
4
+
5
+ type PolymarketBookChangeMapperMessage = PolymarketClobBookMessage | PolymarketClobBookMessage[] | PolymarketClobPriceChangeMessage
6
+ export class PolymarketBookChangeMapper implements Mapper<'polymarket', BookChange> {
7
+ canHandle(message: PolymarketNativeMessage): message is PolymarketBookChangeMapperMessage {
8
+ if (Array.isArray(message)) {
9
+ return message.length > 0 && message.every(isPolymarketClobBookMessage)
10
+ }
11
+ return isPolymarketClobPriceChangeMessage(message) || isPolymarketClobBookMessage(message)
12
+ }
13
+
14
+ getFilters(symbols?: string[]) {
15
+ return [
16
+ { channel: 'book' as const, symbols },
17
+ { channel: 'price_change' as const, symbols }
18
+ ]
19
+ }
20
+
21
+ *map(message: PolymarketNativeMessage, localTimestamp: Date): IterableIterator<BookChange> {
22
+ if (Array.isArray(message)) {
23
+ for (const bookMsg of message) {
24
+ yield this.mapBookSnapshot(bookMsg, localTimestamp)
25
+ }
26
+ return
27
+ }
28
+
29
+ if (isPolymarketClobPriceChangeMessage(message)) {
30
+ const timestamp = new Date(Number(message.timestamp))
31
+ const changes = message.price_changes
32
+
33
+ for (let i = 0; i < changes.length; i++) {
34
+ const change = changes[i]
35
+ const level = this.mapLevel(change)
36
+
37
+ yield {
38
+ type: 'book_change',
39
+ symbol: change.asset_id,
40
+ exchange: 'polymarket',
41
+ isSnapshot: false,
42
+ bids: change.side === 'BUY' ? [level] : [],
43
+ asks: change.side === 'SELL' ? [level] : [],
44
+ timestamp,
45
+ localTimestamp
46
+ }
47
+ }
48
+
49
+ return
50
+ }
51
+
52
+ if (isPolymarketClobBookMessage(message)) {
53
+ yield this.mapBookSnapshot(message, localTimestamp)
54
+ return
55
+ }
56
+ }
57
+
58
+ private mapBookSnapshot(message: PolymarketClobBookMessage, localTimestamp: Date): BookChange {
59
+ return {
60
+ type: 'book_change',
61
+ symbol: message.asset_id,
62
+ exchange: 'polymarket',
63
+ isSnapshot: true,
64
+ bids: message.bids.map(this.mapLevel.bind(this)),
65
+ asks: message.asks.map(this.mapLevel.bind(this)),
66
+ timestamp: new Date(Number(message.timestamp)),
67
+ localTimestamp
68
+ }
69
+ }
70
+
71
+ private mapLevel(level: Pick<PolymarketClobBookLevel, 'price' | 'size'>) {
72
+ return {
73
+ price: Number(level.price),
74
+ amount: Number(level.size)
75
+ }
76
+ }
77
+ }
78
+
79
+ export class PolymarketTradesMapper implements Mapper<'polymarket', Trade> {
80
+ canHandle(message: any): message is PolymarketClobLastTradePriceMessage {
81
+ return message.event_type === 'last_trade_price'
82
+ }
83
+
84
+ getFilters(symbols?: string[]) {
85
+ return [{ channel: 'last_trade_price' as const, symbols }]
86
+ }
87
+
88
+ *map(message: PolymarketClobLastTradePriceMessage, localTimestamp: Date): IterableIterator<Trade> {
89
+ yield {
90
+ type: 'trade',
91
+ symbol: message.asset_id,
92
+ exchange: 'polymarket',
93
+ id: message.transaction_hash,
94
+ price: Number(message.price),
95
+ amount: Number(message.size),
96
+ side: message.side.toLowerCase() as Lowercase<PolymarketClobTradeSide>,
97
+ timestamp: new Date(Number(message.timestamp)),
98
+ localTimestamp
99
+ }
100
+ }
101
+ }
102
+
103
+ export class PolymarketBookTickerMapper implements Mapper<'polymarket', BookTicker> {
104
+ canHandle(message: any): message is PolymarketClobBestBidAskMessage {
105
+ return message.event_type === 'best_bid_ask'
106
+ }
107
+
108
+ getFilters(symbols?: string[]) {
109
+ return [{ channel: 'best_bid_ask' as const, symbols }]
110
+ }
111
+
112
+ *map(message: PolymarketClobBestBidAskMessage, localTimestamp: Date): IterableIterator<BookTicker> {
113
+ yield {
114
+ type: 'book_ticker',
115
+ symbol: message.asset_id,
116
+ exchange: 'polymarket',
117
+ bidPrice: asNonZeroNumberOrUndefined(message.best_bid),
118
+ bidAmount: undefined,
119
+ askPrice: asNonZeroNumberOrUndefined(message.best_ask),
120
+ askAmount: undefined,
121
+ timestamp: new Date(Number(message.timestamp)),
122
+ localTimestamp
123
+ }
124
+ }
125
+ }
126
+
127
+ export type PolymarketNativeMessage =
128
+ | PolymarketClobBookMessage
129
+ | PolymarketClobBookMessage[]
130
+ | PolymarketClobPriceChangeMessage
131
+ | PolymarketClobLastTradePriceMessage
132
+ | PolymarketClobBestBidAskMessage
133
+
134
+ type PolymarketClobEventType = 'book' | 'price_change' | 'last_trade_price' | 'best_bid_ask'
135
+
136
+ type PolymarketClobMessage<T extends PolymarketClobEventType = PolymarketClobEventType> = {
137
+ event_type: T
138
+ market: string
139
+ }
140
+
141
+ function isPolymarketClobBookMessage(message: any): message is PolymarketClobBookMessage {
142
+ return message?.event_type === 'book'
143
+ }
144
+ type PolymarketClobBookMessage = PolymarketClobMessage<'book'> & {
145
+ asset_id: string
146
+ timestamp: string
147
+ hash: string
148
+ bids: PolymarketClobBookLevel[]
149
+ asks: PolymarketClobBookLevel[]
150
+ tick_size?: string
151
+ last_trade_price?: string
152
+ }
153
+
154
+ type PolymarketClobBookLevel = {
155
+ price: string
156
+ size: string
157
+ }
158
+
159
+ function isPolymarketClobPriceChangeMessage(message: any): message is PolymarketClobPriceChangeMessage {
160
+ return message?.event_type === 'price_change'
161
+ }
162
+ type PolymarketClobPriceChangeMessage = PolymarketClobMessage<'price_change'> & {
163
+ timestamp: string
164
+ price_changes: PolymarketClobPriceChange[]
165
+ }
166
+
167
+ type PolymarketClobPriceChange = {
168
+ asset_id: string
169
+ price: string
170
+ size: string
171
+ side: PolymarketClobTradeSide
172
+ hash: string
173
+ best_bid: string
174
+ best_ask: string
175
+ }
176
+
177
+ type PolymarketClobLastTradePriceMessage = PolymarketClobMessage<'last_trade_price'> & {
178
+ asset_id: string
179
+ fee_rate_bps: string
180
+ price: string
181
+ side: PolymarketClobTradeSide
182
+ size: string
183
+ timestamp: string
184
+ transaction_hash: string
185
+ }
186
+
187
+ type PolymarketClobTradeSide = 'BUY' | 'SELL'
188
+
189
+ type PolymarketClobBestBidAskMessage = PolymarketClobMessage<'best_bid_ask'> & {
190
+ asset_id: string
191
+ best_bid: string
192
+ best_ask: string
193
+ spread: string
194
+ timestamp: string
195
+ }
@@ -54,6 +54,7 @@ import { CoinbaseInternationalRealTimeFeed } from './coinbaseinternational.ts'
54
54
  import { HyperliquidRealTimeFeed } from './hyperliquid.ts'
55
55
  import { LighterRealTimeFeed } from './lighter.ts'
56
56
  import { BullishRealTimeFeed } from './bullish.ts'
57
+ import { PolymarketRealTimeFeed } from './polymarket.ts'
57
58
 
58
59
  export * from './realtimefeed.ts'
59
60
 
@@ -118,7 +119,8 @@ const realTimeFeedsMap: {
118
119
  'coinbase-international': CoinbaseInternationalRealTimeFeed,
119
120
  hyperliquid: HyperliquidRealTimeFeed,
120
121
  lighter: LighterRealTimeFeed,
121
- bullish: BullishRealTimeFeed
122
+ bullish: BullishRealTimeFeed,
123
+ polymarket: PolymarketRealTimeFeed
122
124
  }
123
125
 
124
126
  export function getRealTimeFeedFactory(exchange: Exchange): RealTimeFeed {
@@ -0,0 +1,112 @@
1
+ import { Filter } from '../types.ts'
2
+ import { MultiConnectionRealTimeFeedBase, RealTimeFeedBase, RealTimeFeedIterable } from './realtimefeed.ts'
3
+
4
+ export class PolymarketRealTimeFeed extends MultiConnectionRealTimeFeedBase {
5
+ private readonly clobChannels = new Set([
6
+ 'book',
7
+ 'price_change',
8
+ 'last_trade_price',
9
+ 'best_bid_ask',
10
+ 'tick_size_change',
11
+ 'new_market',
12
+ 'market_resolved'
13
+ ])
14
+ private readonly sportsChannel = 'sport_result'
15
+
16
+ protected *_getRealTimeFeeds(
17
+ exchange: string,
18
+ filters: Filter<string>[],
19
+ timeoutIntervalMS?: number,
20
+ onError?: (error: Error) => void
21
+ ): IterableIterator<RealTimeFeedIterable> {
22
+ const clobFilters: Filter<string>[] = []
23
+ const sportsFilters: Filter<string>[] = []
24
+
25
+ for (const filter of filters) {
26
+ if (this.clobChannels.has(filter.channel)) {
27
+ clobFilters.push(filter)
28
+ } else if (filter.channel === this.sportsChannel) {
29
+ sportsFilters.push(filter)
30
+ } else {
31
+ throw new Error(`PolymarketRealTimeFeed unsupported channel ${filter.channel}`)
32
+ }
33
+ }
34
+
35
+ if (clobFilters.length > 0) {
36
+ if (clobFilters.every((filter) => filter.symbols === undefined || filter.symbols.length === 0)) {
37
+ throw new Error('PolymarketRealTimeFeed requires explicitly specified symbols when subscribing to CLOB live feed')
38
+ }
39
+
40
+ yield new PolymarketClobRealTimeFeed(exchange, clobFilters, timeoutIntervalMS, onError)
41
+ }
42
+ if (sportsFilters.length > 0) {
43
+ yield new PolymarketSportsRealTimeFeed(exchange, sportsFilters, timeoutIntervalMS, onError)
44
+ }
45
+ }
46
+ }
47
+
48
+ export class PolymarketClobRealTimeFeed extends RealTimeFeedBase {
49
+ protected wssURL = 'wss://ws-subscriptions-clob.polymarket.com/ws/market'
50
+ private readonly pongMessage = Buffer.from('{"__pong__":true}')
51
+
52
+ protected decompress = (msg: Buffer): Buffer => {
53
+ if (msg.toString() === 'PONG') {
54
+ return this.pongMessage
55
+ }
56
+ return msg
57
+ }
58
+
59
+ protected messageIsHeartbeat(msg: any) {
60
+ return msg.__pong__ === true
61
+ }
62
+
63
+ protected sendCustomPing = () => {
64
+ this.sendRaw('PING')
65
+ }
66
+
67
+ protected mapToSubscribeMessages(filters: Filter<string>[]): any[] {
68
+ return [
69
+ {
70
+ type: 'market',
71
+ assets_ids: [...new Set(filters.flatMap((f) => f.symbols ?? []))],
72
+ initial_dump: true,
73
+ level: 2,
74
+ custom_feature_enabled: true
75
+ }
76
+ ]
77
+ }
78
+
79
+ protected messageIsError(message: any): boolean {
80
+ return typeof message.error === 'string'
81
+ }
82
+ }
83
+
84
+ export class PolymarketSportsRealTimeFeed extends RealTimeFeedBase {
85
+ protected wssURL = 'wss://sports-api.polymarket.com/ws'
86
+ private readonly serverPingMessage = Buffer.from('{"__server_ping__":true}')
87
+
88
+ protected decompress = (msg: Buffer): Buffer => {
89
+ if (msg.toString() === 'ping') {
90
+ return this.serverPingMessage
91
+ }
92
+ return msg
93
+ }
94
+
95
+ protected messageIsHeartbeat(msg: any) {
96
+ return msg.__server_ping__ === true
97
+ }
98
+
99
+ protected onMessage(msg: any) {
100
+ if (msg.__server_ping__ === true) {
101
+ this.sendRaw('pong')
102
+ }
103
+ }
104
+
105
+ protected mapToSubscribeMessages(_filters: Filter<string>[]): any[] {
106
+ return []
107
+ }
108
+
109
+ protected messageIsError(message: any): boolean {
110
+ return typeof message.error === 'string'
111
+ }
112
+ }
@@ -219,6 +219,16 @@ export abstract class RealTimeFeedBase implements RealTimeFeedIterable {
219
219
  this._ws.send(JSON.stringify(msg))
220
220
  }
221
221
 
222
+ protected sendRaw(msg: string | Buffer) {
223
+ if (this._ws === undefined) {
224
+ return
225
+ }
226
+ if (this._ws.readyState !== WebSocket.OPEN) {
227
+ return
228
+ }
229
+ this._ws.send(msg)
230
+ }
231
+
222
232
  protected abstract mapToSubscribeMessages(filters: Filter<string>[]): any[]
223
233
 
224
234
  protected abstract messageIsError(message: any): boolean