tardis-dev 13.11.0 → 13.13.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,322 @@
1
+ import { upperCaseSymbols } from '../handy'
2
+ import { BookChange, BookTicker, DerivativeTicker, Exchange, FilterForExchange, Liquidation, Trade } from '../types'
3
+ import { Mapper, PendingTickerInfoHelper } from './mapper'
4
+
5
+ export const wooxTradesMapper: Mapper<'woo-x', Trade> = {
6
+ canHandle(message: WooxTradeMessage) {
7
+ return message.topic !== undefined && message.topic.endsWith('@trade')
8
+ },
9
+
10
+ getFilters(symbols?: string[]) {
11
+ symbols = upperCaseSymbols(symbols)
12
+
13
+ return [
14
+ {
15
+ channel: 'trade',
16
+ symbols
17
+ }
18
+ ]
19
+ },
20
+
21
+ *map(message: WooxTradeMessage, localTimestamp: Date): IterableIterator<Trade> {
22
+ const timestamp = new Date(message.ts)
23
+
24
+ yield {
25
+ type: 'trade',
26
+ symbol: message.data.symbol,
27
+ exchange: 'woo-x',
28
+ id: undefined,
29
+ price: message.data.price,
30
+ amount: message.data.size,
31
+ side: message.data.side === 'SELL' ? 'sell' : 'buy',
32
+ timestamp,
33
+ localTimestamp: localTimestamp
34
+ }
35
+ }
36
+ }
37
+
38
+ export class WooxBookChangeMapper implements Mapper<'woo-x', BookChange> {
39
+ private readonly _symbolToDepthInfoMapping: { [key: string]: LocalDepthInfo } = {}
40
+
41
+ canHandle(message: WooxOrderbookMessage | WooxOrderbookupdateMessage) {
42
+ if ('id' in message) {
43
+ return message.id.endsWith('@orderbook')
44
+ }
45
+
46
+ if ('topic' in message) {
47
+ return message.topic.endsWith('@orderbookupdate')
48
+ }
49
+
50
+ return false
51
+ }
52
+
53
+ getFilters(symbols?: string[]) {
54
+ symbols = upperCaseSymbols(symbols)
55
+
56
+ return [
57
+ {
58
+ channel: 'orderbook',
59
+ symbols
60
+ },
61
+ {
62
+ channel: 'orderbookupdate',
63
+ symbols
64
+ }
65
+ ]
66
+ }
67
+
68
+ *map(message: WooxOrderbookMessage | WooxOrderbookupdateMessage, localTimestamp: Date): IterableIterator<BookChange> {
69
+ const symbol = message.data.symbol
70
+
71
+ if (this._symbolToDepthInfoMapping[symbol] === undefined) {
72
+ this._symbolToDepthInfoMapping[symbol] = {
73
+ bufferedUpdates: []
74
+ }
75
+ }
76
+
77
+ const symbolDepthInfo = this._symbolToDepthInfoMapping[symbol]
78
+ const snapshotAlreadyProcessed = symbolDepthInfo.snapshotProcessed
79
+
80
+ // first check if received message is snapshot and process it as such if it is
81
+ if ('id' in message && message.success) {
82
+ yield {
83
+ type: 'book_change',
84
+ symbol,
85
+ exchange: 'woo-x',
86
+ isSnapshot: true,
87
+ bids: message.data.bids.map(this._mapBookLevel),
88
+ asks: message.data.asks.map(this._mapBookLevel),
89
+ timestamp: new Date(message.data.ts),
90
+ localTimestamp
91
+ }
92
+
93
+ // mark given symbol depth info that has snapshot processed
94
+ symbolDepthInfo.lastUpdateTimestamp = message.data.ts
95
+ symbolDepthInfo.snapshotProcessed = true
96
+
97
+ // if there were any depth updates buffered, let's proccess those
98
+ for (const update of symbolDepthInfo.bufferedUpdates) {
99
+ const bookChange = this._mapBookDepthUpdate(update, localTimestamp, symbolDepthInfo, symbol)
100
+ if (bookChange !== undefined) {
101
+ yield bookChange
102
+ }
103
+ }
104
+ // remove all buffered updates
105
+ symbolDepthInfo.bufferedUpdates = []
106
+ } else if (snapshotAlreadyProcessed) {
107
+ // snapshot was already processed let's map the message as normal book_change
108
+ const bookChange = this._mapBookDepthUpdate(message as WooxOrderbookupdateMessage, localTimestamp, symbolDepthInfo, symbol)
109
+ if (bookChange !== undefined) {
110
+ yield bookChange
111
+ }
112
+ } else {
113
+ // if snapshot hasn't been yet processed and we've got depthUpdate message, let's buffer it for later processing
114
+ symbolDepthInfo.bufferedUpdates.push(message as WooxOrderbookupdateMessage)
115
+ }
116
+ }
117
+
118
+ private _mapBookDepthUpdate(
119
+ wooxBookUpdate: WooxOrderbookupdateMessage,
120
+ localTimestamp: Date,
121
+ depthInfo: LocalDepthInfo,
122
+ symbol: string
123
+ ): BookChange | undefined {
124
+ if (wooxBookUpdate.data.prevTs < depthInfo.lastUpdateTimestamp!) {
125
+ return
126
+ }
127
+
128
+ return {
129
+ type: 'book_change',
130
+ symbol,
131
+ exchange: 'woo-x',
132
+ isSnapshot: false,
133
+ bids: wooxBookUpdate.data.bids.map(this._mapBookLevel),
134
+ asks: wooxBookUpdate.data.asks.map(this._mapBookLevel),
135
+ timestamp: new Date(wooxBookUpdate.ts),
136
+ localTimestamp
137
+ }
138
+ }
139
+
140
+ private _mapBookLevel(level: [number, number]) {
141
+ const price = level[0]
142
+ const amount = level[1]
143
+
144
+ return { price, amount }
145
+ }
146
+ }
147
+
148
+ export class WooxDerivativeTickerMapper implements Mapper<'woo-x', DerivativeTicker> {
149
+ private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()
150
+ private readonly _indexPrices = new Map<string, number>()
151
+
152
+ canHandle(message: WooxTradeMessage | WooxEstFundingRate | WooxMarkPrice | WooxIndexPrice | WooxOpenInterest) {
153
+ if (message.topic === undefined) {
154
+ return false
155
+ }
156
+ const symbol = (message.data && message.data.symbol) || ''
157
+ const isPerp = symbol.startsWith('PERP_')
158
+
159
+ return (
160
+ (message.topic.endsWith('@trade') && isPerp) ||
161
+ (message.topic.endsWith('@markprice') && isPerp) ||
162
+ (message.topic.endsWith('@estfundingrate') && isPerp) ||
163
+ message.topic.endsWith('@indexprice') ||
164
+ (message.topic.endsWith('@openinterest') && isPerp)
165
+ )
166
+ }
167
+
168
+ getFilters(symbols?: string[]) {
169
+ symbols = upperCaseSymbols(symbols)
170
+ const spotSymbols = symbols !== undefined ? symbols.map((s) => s.replace('PERP_', 'SPOT_')) : []
171
+ return [
172
+ {
173
+ channel: 'trade',
174
+ symbols
175
+ },
176
+ {
177
+ channel: 'markprice',
178
+ symbols
179
+ },
180
+ {
181
+ channel: 'estfundingrate',
182
+ symbols
183
+ },
184
+ {
185
+ channel: 'openinterest',
186
+ symbols
187
+ },
188
+ {
189
+ channel: 'indexprice',
190
+ symbols: spotSymbols
191
+ }
192
+ ]
193
+ }
194
+
195
+ *map(message: any, localTimestamp: Date): IterableIterator<DerivativeTicker> {
196
+ if (message.topic.endsWith('@indexprice')) {
197
+ this._indexPrices.set(message.data.symbol.replace('SPOT_', 'PERP_'), message.data.price)
198
+ } else {
199
+ const symbol = message.data.symbol
200
+ const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(symbol, 'woo-x')
201
+
202
+ const lastIndexPrice = this._indexPrices.get(symbol)
203
+ if (lastIndexPrice !== undefined) {
204
+ pendingTickerInfo.updateIndexPrice(lastIndexPrice)
205
+ }
206
+
207
+ if (message.topic.endsWith('@markprice')) {
208
+ pendingTickerInfo.updateMarkPrice(message.data.price)
209
+ pendingTickerInfo.updateTimestamp(new Date(message.ts))
210
+ }
211
+
212
+ if (message.topic.endsWith('@trade')) {
213
+ pendingTickerInfo.updateLastPrice(message.data.price)
214
+ pendingTickerInfo.updateTimestamp(new Date(message.ts))
215
+ }
216
+
217
+ if (message.topic.endsWith('@estfundingrate')) {
218
+ pendingTickerInfo.updateFundingRate(message.data.fundingRate)
219
+ pendingTickerInfo.updateFundingTimestamp(new Date(message.data.fundingTs))
220
+ pendingTickerInfo.updateTimestamp(new Date(message.ts))
221
+ }
222
+
223
+ if (message.topic.endsWith('@openinterest')) {
224
+ pendingTickerInfo.updateOpenInterest(message.data.openInterest)
225
+ pendingTickerInfo.updateTimestamp(new Date(message.ts))
226
+ }
227
+
228
+ if (pendingTickerInfo.hasChanged()) {
229
+ yield pendingTickerInfo.getSnapshot(localTimestamp)
230
+ }
231
+ }
232
+ }
233
+ }
234
+
235
+ export class WooxBookTickerMapper implements Mapper<'woo-x', BookTicker> {
236
+ canHandle(message: WooxTradeMessage) {
237
+ return message.topic !== undefined && message.topic.endsWith('@bbo')
238
+ }
239
+
240
+ getFilters(symbols?: string[]) {
241
+ symbols = upperCaseSymbols(symbols)
242
+
243
+ return [
244
+ {
245
+ channel: 'bbo',
246
+ symbols
247
+ }
248
+ ]
249
+ }
250
+
251
+ *map(wooxBBOMessage: WooxBBOMessage, localTimestamp: Date) {
252
+ const wooxBookTicker = wooxBBOMessage.data
253
+
254
+ const ticker: BookTicker = {
255
+ type: 'book_ticker',
256
+ symbol: wooxBookTicker.symbol,
257
+ exchange: 'woo-x',
258
+ askAmount: wooxBookTicker.askSize !== undefined ? wooxBookTicker.askSize : undefined,
259
+ askPrice: wooxBookTicker.ask !== undefined ? wooxBookTicker.ask : undefined,
260
+
261
+ bidPrice: wooxBookTicker.bid !== undefined ? wooxBookTicker.bid : undefined,
262
+ bidAmount: wooxBookTicker.bidSize !== undefined ? wooxBookTicker.bidSize : undefined,
263
+ timestamp: new Date(wooxBBOMessage.ts),
264
+ localTimestamp: localTimestamp
265
+ }
266
+
267
+ yield ticker
268
+ }
269
+ }
270
+
271
+ type LocalDepthInfo = {
272
+ bufferedUpdates: WooxOrderbookupdateMessage[]
273
+ snapshotProcessed?: boolean
274
+ lastUpdateTimestamp?: number
275
+ }
276
+
277
+ type WooxTradeMessage = {
278
+ topic: 'PERP_GALA_USDT@trade'
279
+ ts: 1674431999995
280
+ data: { symbol: 'PERP_GALA_USDT'; price: 0.048756; size: 4109; side: 'SELL'; source: 0 }
281
+ }
282
+
283
+ type WooxOrderbookupdateMessage = {
284
+ topic: 'PERP_BTC_USDT@orderbookupdate'
285
+ ts: 1674432000020
286
+ data: {
287
+ symbol: 'PERP_BTC_USDT'
288
+ prevTs: 1674432000030
289
+ asks: [[22712.7, 15.4675]]
290
+ bids: [[22708.0, 4.503]]
291
+ }
292
+ }
293
+
294
+ type WooxOrderbookMessage = {
295
+ id: 'PERP_BTC_USDT@orderbook'
296
+ event: 'request'
297
+ success: true
298
+ ts: 1674432000034
299
+ data: {
300
+ symbol: 'PERP_BTC_USDT'
301
+ ts: 1674432000020
302
+ asks: [[22712.7, 15.4675], [26772.1, 0.248]]
303
+ bids: [[22708.0, 4.503], [18555.0, 0.002]]
304
+ }
305
+ }
306
+
307
+ type WooxBBOMessage = {
308
+ topic: 'SPOT_ETH_USDT@bbo'
309
+ ts: 1674431999996
310
+ data: { symbol: 'SPOT_ETH_USDT'; ask: 1627.61; askSize: 38.3755; bid: 1627.26; bidSize: 20.424926 }
311
+ }
312
+
313
+ type WooxMarkPrice = { topic: 'PERP_BTC_USDT@markprice'; ts: 1674432000007; data: { symbol: 'PERP_BTC_USDT'; price: 22711.11 } }
314
+
315
+ type WooxEstFundingRate = {
316
+ topic: 'PERP_BTC_USDT@estfundingrate'
317
+ ts: 1674432059002
318
+ data: { symbol: 'PERP_BTC_USDT'; fundingRate: 0.00000782; fundingTs: 1674435600005 }
319
+ }
320
+
321
+ type WooxIndexPrice = { topic: 'SPOT_BTC_USDT@indexprice'; ts: 1674432000024; data: { symbol: 'SPOT_BTC_USDT'; price: 22708.44 } }
322
+ type WooxOpenInterest = { topic: 'PERP_BTC_USDT@openinterest'; ts: 1674432013624; data: { symbol: 'PERP_BTC_USDT'; openInterest: 83.2241 } }
@@ -45,6 +45,7 @@ import { BybitSpotRealTimeFeed } from './bybitspot'
45
45
  import { CryptoComRealTimeFeed } from './cryptocom'
46
46
  import { KucoinRealTimeFeed } from './kucoin'
47
47
  import { BitnomialRealTimeFeed } from './bitnomial'
48
+ import { WooxRealTimeFeed } from './woox'
48
49
 
49
50
  export * from './realtimefeed'
50
51
 
@@ -98,7 +99,8 @@ const realTimeFeedsMap: {
98
99
  'crypto-com': CryptoComRealTimeFeed,
99
100
  'crypto-com-derivatives': CryptoComRealTimeFeed,
100
101
  kucoin: KucoinRealTimeFeed,
101
- bitnomial: BitnomialRealTimeFeed
102
+ bitnomial: BitnomialRealTimeFeed,
103
+ 'woo-x': WooxRealTimeFeed
102
104
  }
103
105
 
104
106
  export function getRealTimeFeedFactory(exchange: Exchange): RealTimeFeed {
@@ -0,0 +1,70 @@
1
+ import { wait } from '../handy'
2
+ import { Filter } from '../types'
3
+ import { RealTimeFeedBase } from './realtimefeed'
4
+
5
+ export class WooxRealTimeFeed extends RealTimeFeedBase {
6
+ protected wssURL = 'wss://wss.woo.org/ws/stream/OqdphuyCtYWxwzhxyLLjOWNdFP7sQt8RPWzmb5xY'
7
+
8
+ protected mapToSubscribeMessages(filters: Filter<string>[]): any[] {
9
+ return filters
10
+ .filter((filter) => filter.channel !== 'orderbook')
11
+ .map((filter) => {
12
+ if (!filter.symbols || filter.symbols.length === 0) {
13
+ throw new Error('WooxRealTimeFeed requires explicitly specified symbols when subscribing to live feed')
14
+ }
15
+
16
+ return filter.symbols.map((symbol) => {
17
+ return {
18
+ id: `${symbol}@${filter.channel}`,
19
+ topic: `${symbol}@${filter.channel}`,
20
+ event: 'subscribe'
21
+ }
22
+ })
23
+ })
24
+ .flatMap((f) => f)
25
+ }
26
+
27
+ protected async provideManualSnapshots(filters: Filter<string>[], shouldCancel: () => boolean) {
28
+ const orderbookFilter = filters.find((f) => f.channel === 'orderbook')
29
+ if (!orderbookFilter) {
30
+ return
31
+ }
32
+
33
+ await wait(200)
34
+
35
+ for (let symbol of orderbookFilter.symbols!) {
36
+ if (shouldCancel()) {
37
+ return
38
+ }
39
+
40
+ this.send({
41
+ id: `${symbol}@orderbook`,
42
+ event: 'request',
43
+ params: {
44
+ type: 'orderbook',
45
+ symbol
46
+ }
47
+ })
48
+
49
+ await wait(1)
50
+ }
51
+
52
+ this.debug('sent orderbook requests for: %s', orderbookFilter.symbols)
53
+ }
54
+
55
+ protected messageIsError(message: any): boolean {
56
+ return message.success === false || message.errorMsg !== undefined
57
+ }
58
+
59
+ protected messageIsHeartbeat(message: any): boolean {
60
+ return message.event === 'ping'
61
+ }
62
+
63
+ protected onMessage(msg: any) {
64
+ if (msg.event === 'ping') {
65
+ this.send({
66
+ event: 'ping'
67
+ })
68
+ }
69
+ }
70
+ }