tardis-dev 16.5.0 → 16.6.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.
Files changed (273) hide show
  1. package/dist/consts.d.ts +3 -1
  2. package/dist/consts.d.ts.map +1 -1
  3. package/dist/consts.js +21 -1
  4. package/dist/consts.js.map +1 -1
  5. package/dist/handy.d.ts.map +1 -1
  6. package/dist/handy.js +6 -3
  7. package/dist/handy.js.map +1 -1
  8. package/dist/mappers/ascendex.d.ts +6 -99
  9. package/dist/mappers/ascendex.d.ts.map +1 -1
  10. package/dist/mappers/ascendex.js +13 -4
  11. package/dist/mappers/ascendex.js.map +1 -1
  12. package/dist/mappers/binance.d.ts +27 -154
  13. package/dist/mappers/binance.d.ts.map +1 -1
  14. package/dist/mappers/binance.js +49 -15
  15. package/dist/mappers/binance.js.map +1 -1
  16. package/dist/mappers/binancedex.d.ts +7 -4
  17. package/dist/mappers/binancedex.d.ts.map +1 -1
  18. package/dist/mappers/binancedex.js +11 -3
  19. package/dist/mappers/binancedex.js.map +1 -1
  20. package/dist/mappers/binanceeuropeanoptions.d.ts +6 -216
  21. package/dist/mappers/binanceeuropeanoptions.d.ts.map +1 -1
  22. package/dist/mappers/binanceeuropeanoptions.js +26 -7
  23. package/dist/mappers/binanceeuropeanoptions.js.map +1 -1
  24. package/dist/mappers/bitfinex.d.ts +15 -100
  25. package/dist/mappers/bitfinex.d.ts.map +1 -1
  26. package/dist/mappers/bitfinex.js +20 -5
  27. package/dist/mappers/bitfinex.js.map +1 -1
  28. package/dist/mappers/bitflyer.d.ts +5 -41
  29. package/dist/mappers/bitflyer.d.ts.map +1 -1
  30. package/dist/mappers/bitflyer.js +11 -3
  31. package/dist/mappers/bitflyer.js.map +1 -1
  32. package/dist/mappers/bitget.d.ts +12 -269
  33. package/dist/mappers/bitget.d.ts.map +1 -1
  34. package/dist/mappers/bitget.js +46 -9
  35. package/dist/mappers/bitget.js.map +1 -1
  36. package/dist/mappers/bitmex.d.ts +8 -56
  37. package/dist/mappers/bitmex.d.ts.map +1 -1
  38. package/dist/mappers/bitmex.js +15 -5
  39. package/dist/mappers/bitmex.js.map +1 -1
  40. package/dist/mappers/bitnomial.d.ts +4 -44
  41. package/dist/mappers/bitnomial.d.ts.map +1 -1
  42. package/dist/mappers/bitnomial.js +9 -2
  43. package/dist/mappers/bitnomial.js.map +1 -1
  44. package/dist/mappers/bitstamp.d.ts +4 -44
  45. package/dist/mappers/bitstamp.d.ts.map +1 -1
  46. package/dist/mappers/bitstamp.js +9 -2
  47. package/dist/mappers/bitstamp.js.map +1 -1
  48. package/dist/mappers/blockchaincom.d.ts +4 -63
  49. package/dist/mappers/blockchaincom.d.ts.map +1 -1
  50. package/dist/mappers/blockchaincom.js +9 -2
  51. package/dist/mappers/blockchaincom.js.map +1 -1
  52. package/dist/mappers/bullish.d.ts +8 -158
  53. package/dist/mappers/bullish.d.ts.map +1 -1
  54. package/dist/mappers/bullish.js +19 -7
  55. package/dist/mappers/bullish.js.map +1 -1
  56. package/dist/mappers/bybit.d.ts +61 -407
  57. package/dist/mappers/bybit.d.ts.map +1 -1
  58. package/dist/mappers/bybit.js +60 -15
  59. package/dist/mappers/bybit.js.map +1 -1
  60. package/dist/mappers/bybitspot.d.ts +1 -1
  61. package/dist/mappers/coinbase.d.ts +6 -39
  62. package/dist/mappers/coinbase.d.ts.map +1 -1
  63. package/dist/mappers/coinbase.js +11 -3
  64. package/dist/mappers/coinbase.js.map +1 -1
  65. package/dist/mappers/coinbaseinternational.d.ts +7 -78
  66. package/dist/mappers/coinbaseinternational.d.ts.map +1 -1
  67. package/dist/mappers/coinbaseinternational.js +13 -4
  68. package/dist/mappers/coinbaseinternational.js.map +1 -1
  69. package/dist/mappers/coinflex.d.ts +6 -24
  70. package/dist/mappers/coinflex.d.ts.map +1 -1
  71. package/dist/mappers/coinflex.js +11 -3
  72. package/dist/mappers/coinflex.js.map +1 -1
  73. package/dist/mappers/cryptocom.d.ts +22 -328
  74. package/dist/mappers/cryptocom.d.ts.map +1 -1
  75. package/dist/mappers/cryptocom.js +13 -4
  76. package/dist/mappers/cryptocom.js.map +1 -1
  77. package/dist/mappers/cryptofacilities.d.ts +8 -96
  78. package/dist/mappers/cryptofacilities.d.ts.map +1 -1
  79. package/dist/mappers/cryptofacilities.js +24 -7
  80. package/dist/mappers/cryptofacilities.js.map +1 -1
  81. package/dist/mappers/delta.d.ts +7 -114
  82. package/dist/mappers/delta.d.ts.map +1 -1
  83. package/dist/mappers/delta.js +30 -10
  84. package/dist/mappers/delta.js.map +1 -1
  85. package/dist/mappers/deribit.d.ts +8 -66
  86. package/dist/mappers/deribit.d.ts.map +1 -1
  87. package/dist/mappers/deribit.js +17 -6
  88. package/dist/mappers/deribit.js.map +1 -1
  89. package/dist/mappers/dydx.d.ts +5 -146
  90. package/dist/mappers/dydx.d.ts.map +1 -1
  91. package/dist/mappers/dydx.js +11 -3
  92. package/dist/mappers/dydx.js.map +1 -1
  93. package/dist/mappers/dydxv4.d.ts +6 -173
  94. package/dist/mappers/dydxv4.d.ts.map +1 -1
  95. package/dist/mappers/dydxv4.js +13 -4
  96. package/dist/mappers/dydxv4.js.map +1 -1
  97. package/dist/mappers/ftx.d.ts +12 -107
  98. package/dist/mappers/ftx.d.ts.map +1 -1
  99. package/dist/mappers/ftx.js +21 -6
  100. package/dist/mappers/ftx.js.map +1 -1
  101. package/dist/mappers/gateio.d.ts +6 -191
  102. package/dist/mappers/gateio.d.ts.map +1 -1
  103. package/dist/mappers/gateio.js +28 -8
  104. package/dist/mappers/gateio.js.map +1 -1
  105. package/dist/mappers/gateiofutures.d.ts +7 -122
  106. package/dist/mappers/gateiofutures.d.ts.map +1 -1
  107. package/dist/mappers/gateiofutures.js +13 -4
  108. package/dist/mappers/gateiofutures.js.map +1 -1
  109. package/dist/mappers/gemini.d.ts +6 -3
  110. package/dist/mappers/gemini.d.ts.map +1 -1
  111. package/dist/mappers/gemini.js +9 -2
  112. package/dist/mappers/gemini.js.map +1 -1
  113. package/dist/mappers/hitbtc.d.ts +6 -3
  114. package/dist/mappers/hitbtc.d.ts.map +1 -1
  115. package/dist/mappers/hitbtc.js +9 -2
  116. package/dist/mappers/hitbtc.js.map +1 -1
  117. package/dist/mappers/huobi.d.ts +118 -289
  118. package/dist/mappers/huobi.d.ts.map +1 -1
  119. package/dist/mappers/huobi.js +45 -7
  120. package/dist/mappers/huobi.js.map +1 -1
  121. package/dist/mappers/hyperliquid.d.ts +6 -86
  122. package/dist/mappers/hyperliquid.d.ts.map +1 -1
  123. package/dist/mappers/hyperliquid.js +13 -4
  124. package/dist/mappers/hyperliquid.js.map +1 -1
  125. package/dist/mappers/index.d.ts +784 -272
  126. package/dist/mappers/index.d.ts.map +1 -1
  127. package/dist/mappers/index.js +64 -432
  128. package/dist/mappers/index.js.map +1 -1
  129. package/dist/mappers/kraken.d.ts +7 -4
  130. package/dist/mappers/kraken.d.ts.map +1 -1
  131. package/dist/mappers/kraken.js +11 -3
  132. package/dist/mappers/kraken.js.map +1 -1
  133. package/dist/mappers/kucoin.d.ts +6 -122
  134. package/dist/mappers/kucoin.d.ts.map +1 -1
  135. package/dist/mappers/kucoin.js +13 -5
  136. package/dist/mappers/kucoin.js.map +1 -1
  137. package/dist/mappers/kucoinfutures.d.ts +6 -207
  138. package/dist/mappers/kucoinfutures.d.ts.map +1 -1
  139. package/dist/mappers/kucoinfutures.js +15 -6
  140. package/dist/mappers/kucoinfutures.js.map +1 -1
  141. package/dist/mappers/lighter.d.ts +8 -156
  142. package/dist/mappers/lighter.d.ts.map +1 -1
  143. package/dist/mappers/lighter.js +15 -5
  144. package/dist/mappers/lighter.js.map +1 -1
  145. package/dist/mappers/mapper.d.ts +3 -1
  146. package/dist/mappers/mapper.d.ts.map +1 -1
  147. package/dist/mappers/mapper.js.map +1 -1
  148. package/dist/mappers/mexc.d.ts +122 -0
  149. package/dist/mappers/mexc.d.ts.map +1 -0
  150. package/dist/mappers/mexc.js +205 -0
  151. package/dist/mappers/mexc.js.map +1 -0
  152. package/dist/mappers/mexcfutures.d.ts +137 -0
  153. package/dist/mappers/mexcfutures.d.ts.map +1 -0
  154. package/dist/mappers/mexcfutures.js +225 -0
  155. package/dist/mappers/mexcfutures.js.map +1 -0
  156. package/dist/mappers/okex.d.ts +36 -473
  157. package/dist/mappers/okex.d.ts.map +1 -1
  158. package/dist/mappers/okex.js +143 -37
  159. package/dist/mappers/okex.js.map +1 -1
  160. package/dist/mappers/phemex.d.ts +5 -82
  161. package/dist/mappers/phemex.d.ts.map +1 -1
  162. package/dist/mappers/phemex.js +22 -4
  163. package/dist/mappers/phemex.js.map +1 -1
  164. package/dist/mappers/poloniex.d.ts +5 -90
  165. package/dist/mappers/poloniex.d.ts.map +1 -1
  166. package/dist/mappers/poloniex.js +18 -4
  167. package/dist/mappers/poloniex.js.map +1 -1
  168. package/dist/mappers/polymarket.d.ts +7 -31
  169. package/dist/mappers/polymarket.d.ts.map +1 -1
  170. package/dist/mappers/polymarket.js +11 -3
  171. package/dist/mappers/polymarket.js.map +1 -1
  172. package/dist/mappers/registry.d.ts +41 -0
  173. package/dist/mappers/registry.d.ts.map +1 -0
  174. package/dist/mappers/registry.js +111 -0
  175. package/dist/mappers/registry.js.map +1 -0
  176. package/dist/mappers/serum.d.ts +16 -89
  177. package/dist/mappers/serum.d.ts.map +1 -1
  178. package/dist/mappers/serum.js +21 -3
  179. package/dist/mappers/serum.js.map +1 -1
  180. package/dist/mappers/upbit.d.ts +5 -47
  181. package/dist/mappers/upbit.d.ts.map +1 -1
  182. package/dist/mappers/upbit.js +9 -2
  183. package/dist/mappers/upbit.js.map +1 -1
  184. package/dist/mappers/woox.d.ts +6 -107
  185. package/dist/mappers/woox.d.ts.map +1 -1
  186. package/dist/mappers/woox.js +13 -4
  187. package/dist/mappers/woox.js.map +1 -1
  188. package/dist/okexsymbols.d.ts +3 -0
  189. package/dist/okexsymbols.d.ts.map +1 -0
  190. package/dist/okexsymbols.js +10 -0
  191. package/dist/okexsymbols.js.map +1 -0
  192. package/dist/realtimefeeds/hyperliquid.d.ts.map +1 -1
  193. package/dist/realtimefeeds/hyperliquid.js +3 -1
  194. package/dist/realtimefeeds/hyperliquid.js.map +1 -1
  195. package/dist/realtimefeeds/index.d.ts.map +1 -1
  196. package/dist/realtimefeeds/index.js +4 -0
  197. package/dist/realtimefeeds/index.js.map +1 -1
  198. package/dist/realtimefeeds/mexc.d.ts +35 -0
  199. package/dist/realtimefeeds/mexc.d.ts.map +1 -0
  200. package/dist/realtimefeeds/mexc.js +261 -0
  201. package/dist/realtimefeeds/mexc.js.map +1 -0
  202. package/dist/realtimefeeds/mexcfutures.d.ts +25 -0
  203. package/dist/realtimefeeds/mexcfutures.d.ts.map +1 -0
  204. package/dist/realtimefeeds/mexcfutures.js +203 -0
  205. package/dist/realtimefeeds/mexcfutures.js.map +1 -0
  206. package/dist/realtimefeeds/okex.d.ts +1 -0
  207. package/dist/realtimefeeds/okex.d.ts.map +1 -1
  208. package/dist/realtimefeeds/okex.js +7 -5
  209. package/dist/realtimefeeds/okex.js.map +1 -1
  210. package/dist/realtimefeeds/realtimefeed.d.ts +1 -0
  211. package/dist/realtimefeeds/realtimefeed.d.ts.map +1 -1
  212. package/dist/realtimefeeds/realtimefeed.js +4 -1
  213. package/dist/realtimefeeds/realtimefeed.js.map +1 -1
  214. package/dist/replay.d.ts.map +1 -1
  215. package/dist/replay.js +62 -13
  216. package/dist/replay.js.map +1 -1
  217. package/example.js +133 -31
  218. package/package.json +2 -1
  219. package/src/consts.ts +22 -1
  220. package/src/handy.ts +6 -3
  221. package/src/mappers/ascendex.ts +14 -4
  222. package/src/mappers/binance.ts +61 -21
  223. package/src/mappers/binancedex.ts +12 -3
  224. package/src/mappers/binanceeuropeanoptions.ts +28 -7
  225. package/src/mappers/bitfinex.ts +21 -5
  226. package/src/mappers/bitflyer.ts +12 -3
  227. package/src/mappers/bitget.ts +48 -9
  228. package/src/mappers/bitmex.ts +16 -5
  229. package/src/mappers/bitnomial.ts +10 -2
  230. package/src/mappers/bitstamp.ts +10 -2
  231. package/src/mappers/blockchaincom.ts +10 -2
  232. package/src/mappers/bullish.ts +21 -8
  233. package/src/mappers/bybit.ts +70 -15
  234. package/src/mappers/coinbase.ts +12 -3
  235. package/src/mappers/coinbaseinternational.ts +14 -4
  236. package/src/mappers/coinflex.ts +12 -3
  237. package/src/mappers/cryptocom.ts +14 -4
  238. package/src/mappers/cryptofacilities.ts +30 -6
  239. package/src/mappers/delta.ts +41 -7
  240. package/src/mappers/deribit.ts +18 -6
  241. package/src/mappers/dydx.ts +12 -3
  242. package/src/mappers/dydxv4.ts +14 -4
  243. package/src/mappers/ftx.ts +22 -6
  244. package/src/mappers/gateio.ts +34 -8
  245. package/src/mappers/gateiofutures.ts +14 -4
  246. package/src/mappers/gemini.ts +10 -2
  247. package/src/mappers/hitbtc.ts +10 -2
  248. package/src/mappers/huobi.ts +47 -10
  249. package/src/mappers/hyperliquid.ts +14 -4
  250. package/src/mappers/index.ts +133 -679
  251. package/src/mappers/kraken.ts +12 -3
  252. package/src/mappers/kucoin.ts +19 -5
  253. package/src/mappers/kucoinfutures.ts +20 -5
  254. package/src/mappers/lighter.ts +16 -5
  255. package/src/mappers/mapper.ts +3 -1
  256. package/src/mappers/mexc.ts +324 -0
  257. package/src/mappers/mexcfutures.ts +352 -0
  258. package/src/mappers/okex.ts +178 -37
  259. package/src/mappers/phemex.ts +25 -4
  260. package/src/mappers/poloniex.ts +20 -4
  261. package/src/mappers/polymarket.ts +12 -3
  262. package/src/mappers/registry.ts +203 -0
  263. package/src/mappers/serum.ts +22 -3
  264. package/src/mappers/upbit.ts +10 -2
  265. package/src/mappers/woox.ts +14 -4
  266. package/src/okexsymbols.ts +10 -0
  267. package/src/realtimefeeds/hyperliquid.ts +4 -1
  268. package/src/realtimefeeds/index.ts +4 -0
  269. package/src/realtimefeeds/mexc.ts +327 -0
  270. package/src/realtimefeeds/mexcfutures.ts +269 -0
  271. package/src/realtimefeeds/okex.ts +7 -5
  272. package/src/realtimefeeds/realtimefeed.ts +5 -1
  273. package/src/replay.ts +73 -15
@@ -0,0 +1,327 @@
1
+ import protobuf from 'protobufjs'
2
+ import { CircularBuffer, getJSON, wait } from '../handy.ts'
3
+ import type { MexcDepthSnapshotMessage } from '../mappers/mexc.ts'
4
+ import { Filter } from '../types.ts'
5
+ import { RealTimeFeedBase } from './realtimefeed.ts'
6
+
7
+ export class MexcRealTimeFeed extends RealTimeFeedBase {
8
+ private static readonly depthChannel = 'spot@public.aggre.depth.v3.api.pb@10ms'
9
+ private static readonly depthSnapshotChannel = 'spot@public.aggre.depth.snapshot.v3.api.pb@10ms'
10
+ private static readonly jsonObjectStart = '{'.charCodeAt(0)
11
+ private static pushDataV3ApiWrapper: protobuf.Type | undefined
12
+ /**
13
+ * MEXC protobuf docs at @see https://www.mexc.com/api-docs/spot-v3/websocket-market-streams/protocol-buffers-integration
14
+ * and schemas from @see https://github.com/mexcdevelop/websocket-proto, mirrored in tardis-recorder/datafeeds/src/mexc/proto.
15
+ */
16
+ private static readonly pushDataV3ApiWrapperSchema = `
17
+ syntax = "proto3";
18
+
19
+ message PushDataV3ApiWrapper {
20
+ string channel = 1;
21
+ oneof body {
22
+ PublicAggreDepthsV3Api publicAggreDepths = 313;
23
+ PublicAggreDealsV3Api publicAggreDeals = 314;
24
+ PublicAggreBookTickerV3Api publicAggreBookTicker = 315;
25
+ }
26
+
27
+ optional string symbol = 3;
28
+ optional string symbolId = 4;
29
+ optional int64 createTime = 5;
30
+ optional int64 sendTime = 6;
31
+ }
32
+
33
+ message PublicAggreDealsV3Api {
34
+ repeated PublicAggreDealsV3ApiItem deals = 1;
35
+ string eventType = 2;
36
+ }
37
+
38
+ message PublicAggreDealsV3ApiItem {
39
+ string price = 1;
40
+ string quantity = 2;
41
+ int32 tradeType = 3;
42
+ int64 time = 4;
43
+ string tradeId = 5;
44
+ }
45
+
46
+ message PublicAggreDepthsV3Api {
47
+ repeated PublicAggreDepthV3ApiItem asks = 1;
48
+ repeated PublicAggreDepthV3ApiItem bids = 2;
49
+ string eventType = 3;
50
+ string fromVersion = 4;
51
+ string toVersion = 5;
52
+ int64 lastOrderCreateTime = 6;
53
+ }
54
+
55
+ message PublicAggreDepthV3ApiItem {
56
+ string price = 1;
57
+ string quantity = 2;
58
+ }
59
+
60
+ message PublicAggreBookTickerV3Api {
61
+ string bidPrice = 1;
62
+ string bidQuantity = 2;
63
+ string askPrice = 3;
64
+ string askQuantity = 4;
65
+ string version = 5;
66
+ int64 lastOrderCreateTime = 6;
67
+ }
68
+ `
69
+
70
+ private readonly pendingDepthSnapshotSymbols = new Set<string>()
71
+ private readonly bufferedDepthUpdates = new Map<string, CircularBuffer<MexcDepthUpdateData>>()
72
+ protected readonly wssURL = 'wss://wbs-api.mexc.com/ws'
73
+ protected readonly httpURL: string = 'https://api.mexc.com'
74
+ private readonly channels = new Set([
75
+ 'spot@public.aggre.deals.v3.api.pb@10ms',
76
+ MexcRealTimeFeed.depthChannel,
77
+ MexcRealTimeFeed.depthSnapshotChannel,
78
+ 'spot@public.aggre.bookTicker.v3.api.pb@10ms'
79
+ ])
80
+
81
+ protected override mapToSubscribeMessages(filters: Filter<string>[]): any[] {
82
+ const filtersWithSymbols = filters.map<Required<Filter<string>>>((filter) => {
83
+ if (!this.channels.has(filter.channel)) {
84
+ throw new Error(`MexcRealTimeFeed unsupported channel ${filter.channel}`)
85
+ }
86
+
87
+ if (!filter.symbols || filter.symbols.length === 0) {
88
+ throw new Error('MexcRealTimeFeed requires explicitly specified symbols when subscribing to live feed')
89
+ }
90
+
91
+ return filter as Required<Filter<string>>
92
+ })
93
+
94
+ const depthSnapshotFilters = filtersWithSymbols.filter((filter) => filter.channel === MexcRealTimeFeed.depthSnapshotChannel)
95
+ this.validateDepthSnapshotFilters(filtersWithSymbols, depthSnapshotFilters)
96
+ this.resetDepthSnapshotTracking(depthSnapshotFilters)
97
+
98
+ return [
99
+ {
100
+ method: 'SUBSCRIPTION',
101
+ params: filtersWithSymbols.flatMap((filter) =>
102
+ filter.channel === MexcRealTimeFeed.depthSnapshotChannel
103
+ ? []
104
+ : filter.symbols.map((symbol) => `${filter.channel}@${symbol.toUpperCase()}`)
105
+ )
106
+ }
107
+ ]
108
+ }
109
+
110
+ protected override parseMessage(message: Buffer<ArrayBufferLike>): any {
111
+ const buffer = Buffer.isBuffer(message) ? message : Buffer.from(message)
112
+
113
+ if (buffer.length > 0 && buffer[0] === MexcRealTimeFeed.jsonObjectStart) {
114
+ return JSON.parse(buffer.toString())
115
+ }
116
+
117
+ const pushDataV3ApiWrapper = this.getPushDataV3ApiWrapper()
118
+ return pushDataV3ApiWrapper.toObject(pushDataV3ApiWrapper.decode(buffer), {
119
+ longs: String,
120
+ arrays: true
121
+ })
122
+ }
123
+
124
+ protected override messageIsError(message: any): boolean {
125
+ return message.code !== undefined && message.code !== 0
126
+ }
127
+
128
+ protected override messageIsHeartbeat(message: any) {
129
+ return message.msg === 'PONG'
130
+ }
131
+
132
+ protected override onMessage(message: any) {
133
+ if (
134
+ message.channel?.startsWith(MexcRealTimeFeed.depthChannel) !== true ||
135
+ message.symbol === undefined ||
136
+ this.pendingDepthSnapshotSymbols.has(message.symbol) === false
137
+ ) {
138
+ return
139
+ }
140
+
141
+ const firstVersion = Number(message.publicAggreDepths?.fromVersion)
142
+ const lastVersion = Number(message.publicAggreDepths?.toVersion)
143
+ if (Number.isFinite(firstVersion) === false || Number.isFinite(lastVersion) === false) {
144
+ return
145
+ }
146
+
147
+ const bufferedUpdates = this.bufferedDepthUpdates.get(message.symbol) ?? new CircularBuffer<MexcDepthUpdateData>(2000)
148
+ bufferedUpdates.append({ firstVersion, lastVersion })
149
+ this.bufferedDepthUpdates.set(message.symbol, bufferedUpdates)
150
+ }
151
+
152
+ protected override async provideManualSnapshots(filters: Filter<string>[], shouldCancel: () => boolean) {
153
+ const depthFilter = filters.find((filter) => filter.channel === MexcRealTimeFeed.depthSnapshotChannel)
154
+ if (depthFilter === undefined) {
155
+ return
156
+ }
157
+
158
+ this.debug('requesting manual snapshots for: %s', depthFilter.symbols!)
159
+
160
+ for (const symbol of depthFilter.symbols!) {
161
+ await this.provideManualSnapshot(symbol.toUpperCase(), shouldCancel)
162
+ }
163
+
164
+ this.debug('requested manual snapshots successfully for: %s ', depthFilter.symbols!)
165
+ }
166
+
167
+ protected sendCustomPing = () => {
168
+ this.send({ method: 'PING' })
169
+ }
170
+
171
+ private getPushDataV3ApiWrapper() {
172
+ MexcRealTimeFeed.pushDataV3ApiWrapper ??= protobuf
173
+ .parse(MexcRealTimeFeed.pushDataV3ApiWrapperSchema)
174
+ .root.lookupType('PushDataV3ApiWrapper')
175
+
176
+ return MexcRealTimeFeed.pushDataV3ApiWrapper
177
+ }
178
+
179
+ private resetDepthSnapshotTracking(filters: Required<Filter<string>>[]) {
180
+ this.pendingDepthSnapshotSymbols.clear()
181
+ this.bufferedDepthUpdates.clear()
182
+
183
+ for (const filter of filters) {
184
+ for (const symbol of filter.symbols) {
185
+ const upperCaseSymbol = symbol.toUpperCase()
186
+ this.pendingDepthSnapshotSymbols.add(upperCaseSymbol)
187
+ this.bufferedDepthUpdates.set(upperCaseSymbol, new CircularBuffer<MexcDepthUpdateData>(2000))
188
+ }
189
+ }
190
+ }
191
+
192
+ private validateDepthSnapshotFilters(filters: Required<Filter<string>>[], depthSnapshotFilters: Required<Filter<string>>[]) {
193
+ if (depthSnapshotFilters.length === 0) {
194
+ return
195
+ }
196
+
197
+ const depthSymbols = new Set(
198
+ filters
199
+ .filter((filter) => filter.channel === MexcRealTimeFeed.depthChannel)
200
+ .flatMap((filter) => filter.symbols.map((symbol) => symbol.toUpperCase()))
201
+ )
202
+
203
+ for (const filter of depthSnapshotFilters) {
204
+ for (const symbol of filter.symbols) {
205
+ if (depthSymbols.has(symbol.toUpperCase()) === false) {
206
+ throw new Error(
207
+ `MexcRealTimeFeed requires ${MexcRealTimeFeed.depthChannel} for every ${MexcRealTimeFeed.depthSnapshotChannel} symbol`
208
+ )
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ private async provideManualSnapshot(symbol: string, shouldCancel: () => boolean) {
215
+ const maxSnapshotRounds = 4
216
+ const maxSnapshotAttemptsPerRound = 3
217
+
218
+ for (let round = 0; round < maxSnapshotRounds; round++) {
219
+ for (let attempt = 1; attempt <= maxSnapshotAttemptsPerRound; attempt++) {
220
+ if (shouldCancel()) {
221
+ return
222
+ }
223
+
224
+ const { data } = await getJSON<MexcDepthSnapshotResponse>(`${this.httpURL}/api/v3/depth?symbol=${symbol}&limit=5000`)
225
+ if (this.snapshotResponseIsValid(data) === false) {
226
+ if (attempt < maxSnapshotAttemptsPerRound) {
227
+ await wait(attempt * 1000)
228
+ }
229
+ continue
230
+ }
231
+
232
+ const hasOverlap = await this.waitForSnapshotOverlap(symbol, data.lastUpdateId)
233
+
234
+ if (shouldCancel()) {
235
+ return
236
+ }
237
+
238
+ if (hasOverlap === false) {
239
+ this.trimBufferedUpdates(symbol)
240
+ if (attempt < maxSnapshotAttemptsPerRound) {
241
+ await wait(attempt * 1000)
242
+ }
243
+ continue
244
+ }
245
+
246
+ if (hasOverlap === true || attempt === maxSnapshotAttemptsPerRound) {
247
+ this.manualSnapshotsBuffer.push(this.createManualSnapshot(symbol, data))
248
+ this.pendingDepthSnapshotSymbols.delete(symbol)
249
+ this.bufferedDepthUpdates.delete(symbol)
250
+ return
251
+ }
252
+ }
253
+ }
254
+
255
+ throw new Error(`MexcRealTimeFeed could not align depth snapshot for ${symbol}`)
256
+ }
257
+
258
+ private async waitForSnapshotOverlap(symbol: string, lastUpdateId: number) {
259
+ let hasOverlap = this.validateSnapshotOverlap(symbol, lastUpdateId)
260
+ for (let attempt = 0; attempt < 60; attempt++) {
261
+ if (hasOverlap !== undefined) {
262
+ return hasOverlap
263
+ }
264
+
265
+ await wait(100)
266
+ hasOverlap = this.validateSnapshotOverlap(symbol, lastUpdateId)
267
+ }
268
+
269
+ return hasOverlap
270
+ }
271
+
272
+ private validateSnapshotOverlap(symbol: string, lastUpdateId: number) {
273
+ const bufferedUpdates = this.bufferedDepthUpdates.get(symbol)
274
+ for (const update of bufferedUpdates?.items() ?? []) {
275
+ if (update.lastVersion <= lastUpdateId) {
276
+ continue
277
+ }
278
+
279
+ return update.firstVersion <= lastUpdateId + 1 && update.lastVersion >= lastUpdateId + 1
280
+ }
281
+
282
+ return undefined
283
+ }
284
+
285
+ private trimBufferedUpdates(symbol: string) {
286
+ const bufferedUpdates = this.bufferedDepthUpdates.get(symbol)
287
+ if (bufferedUpdates === undefined || bufferedUpdates.count <= 100) {
288
+ return
289
+ }
290
+
291
+ const trimmed = new CircularBuffer<MexcDepthUpdateData>(2000)
292
+ for (const update of [...bufferedUpdates.items()].slice(-100)) {
293
+ trimmed.append(update)
294
+ }
295
+ this.bufferedDepthUpdates.set(symbol, trimmed)
296
+ }
297
+
298
+ private snapshotResponseIsValid(data: MexcDepthSnapshotResponse) {
299
+ return Number.isFinite(data.lastUpdateId) && Array.isArray(data.asks) && Array.isArray(data.bids)
300
+ }
301
+
302
+ private createManualSnapshot(symbol: string, data: MexcDepthSnapshotResponse): MexcDepthSnapshotMessage {
303
+ return {
304
+ channel: `${MexcRealTimeFeed.depthSnapshotChannel}@${symbol}`,
305
+ symbol,
306
+ generated: true,
307
+ publicAggreDepthsSnapshot: {
308
+ lastUpdateId: data.lastUpdateId,
309
+ asks: data.asks,
310
+ bids: data.bids,
311
+ timestamp: data.timestamp
312
+ }
313
+ }
314
+ }
315
+ }
316
+
317
+ type MexcDepthSnapshotResponse = {
318
+ lastUpdateId: number
319
+ bids: [string, string][]
320
+ asks: [string, string][]
321
+ timestamp: number
322
+ }
323
+
324
+ type MexcDepthUpdateData = {
325
+ firstVersion: number
326
+ lastVersion: number
327
+ }
@@ -0,0 +1,269 @@
1
+ import { CircularBuffer, getJSON, wait } from '../handy.ts'
2
+ import type { MexcFuturesDepthSnapshotData, MexcFuturesDepthSnapshotMessage } from '../mappers/mexcfutures.ts'
3
+ import { Filter } from '../types.ts'
4
+ import { RealTimeFeedBase } from './realtimefeed.ts'
5
+
6
+ export class MexcFuturesRealTimeFeed extends RealTimeFeedBase {
7
+ private readonly pendingDepthSnapshotSymbols = new Set<string>()
8
+ private readonly bufferedDepthUpdates = new Map<string, CircularBuffer<MexcFuturesDepthUpdateData>>()
9
+ protected readonly wssURL = 'wss://contract.mexc.com'
10
+ protected readonly httpURL: string = 'https://contract.mexc.com'
11
+ protected readonly channelToSubscriptionMethod = new Map([
12
+ ['push.deal', 'sub.deal'],
13
+ ['push.depth', 'sub.depth'],
14
+ ['push.ticker', 'sub.ticker'],
15
+ ['push.index.price', 'sub.index.price'],
16
+ ['push.fair.price', 'sub.fair.price'],
17
+ ['push.funding.rate', 'sub.funding.rate'],
18
+ ['push.contract', 'sub.contract']
19
+ ])
20
+
21
+ protected override sendCustomPing = () => {
22
+ this.send({ method: 'ping' })
23
+ }
24
+
25
+ protected override async getWebSocketUrl() {
26
+ return 'wss://contract.mexc.com/edge'
27
+ }
28
+
29
+ protected override mapToSubscribeMessages(filters: Filter<string>[]): any[] {
30
+ const filtersWithSymbols = filters.map<Required<Filter<string>>>((filter) => {
31
+ if (filter.channel === 'push.contract') {
32
+ return { ...filter, symbols: [] }
33
+ }
34
+
35
+ if (filter.channel !== 'push.depth.snapshot' && this.channelToSubscriptionMethod.has(filter.channel) === false) {
36
+ throw new Error(`Unsupported MEXC futures channel ${filter.channel}`)
37
+ }
38
+
39
+ if (!filter.symbols || filter.symbols.length === 0) {
40
+ throw new Error('MexcFuturesRealTimeFeed requires explicitly specified symbols when subscribing to live feed')
41
+ }
42
+
43
+ return filter as Required<Filter<string>>
44
+ })
45
+
46
+ const depthSnapshotFilters = filtersWithSymbols.filter((filter) => filter.channel === 'push.depth.snapshot')
47
+ this.validateDepthSnapshotFilters(filtersWithSymbols, depthSnapshotFilters)
48
+ this.resetDepthSnapshotTracking(depthSnapshotFilters)
49
+
50
+ return filtersWithSymbols.flatMap((filter) => {
51
+ if (filter.channel === 'push.depth.snapshot') {
52
+ return []
53
+ }
54
+
55
+ if (filter.channel === 'push.contract') {
56
+ return [{ method: this.channelToSubscriptionMethod.get(filter.channel) }]
57
+ }
58
+
59
+ return filter.symbols.map((symbol) => ({
60
+ method: this.channelToSubscriptionMethod.get(filter.channel),
61
+ param: { symbol },
62
+ gzip: false
63
+ }))
64
+ })
65
+ }
66
+
67
+ private resetDepthSnapshotTracking(filters: Required<Filter<string>>[]) {
68
+ this.pendingDepthSnapshotSymbols.clear()
69
+ this.bufferedDepthUpdates.clear()
70
+
71
+ for (const filter of filters) {
72
+ for (const symbol of filter.symbols) {
73
+ const upperCaseSymbol = symbol.toUpperCase()
74
+ this.pendingDepthSnapshotSymbols.add(upperCaseSymbol)
75
+ this.bufferedDepthUpdates.set(upperCaseSymbol, new CircularBuffer<MexcFuturesDepthUpdateData>(2000))
76
+ }
77
+ }
78
+ }
79
+
80
+ private validateDepthSnapshotFilters(filters: Required<Filter<string>>[], depthSnapshotFilters: Required<Filter<string>>[]) {
81
+ if (depthSnapshotFilters.length === 0) {
82
+ return
83
+ }
84
+
85
+ const depthSymbols = new Set(
86
+ filters.filter((filter) => filter.channel === 'push.depth').flatMap((filter) => filter.symbols.map((symbol) => symbol.toUpperCase()))
87
+ )
88
+
89
+ for (const filter of depthSnapshotFilters) {
90
+ for (const symbol of filter.symbols) {
91
+ if (depthSymbols.has(symbol.toUpperCase()) === false) {
92
+ throw new Error('MexcFuturesRealTimeFeed requires push.depth for every push.depth.snapshot symbol')
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ protected override onMessage(message: any) {
99
+ if (
100
+ message.channel !== 'push.depth' ||
101
+ message.symbol === undefined ||
102
+ this.pendingDepthSnapshotSymbols.has(message.symbol) === false
103
+ ) {
104
+ return
105
+ }
106
+
107
+ const firstVersion = Number(
108
+ message.data?.begin !== undefined && message.data.begin !== null && message.data.begin !== 0
109
+ ? message.data.begin
110
+ : message.data?.version
111
+ )
112
+ const lastVersion = Number(
113
+ message.data?.end !== undefined && message.data.end !== null && message.data.end !== 0 ? message.data.end : message.data?.version
114
+ )
115
+ if (Number.isFinite(firstVersion) === false || Number.isFinite(lastVersion) === false) {
116
+ return
117
+ }
118
+
119
+ const bufferedUpdates = this.bufferedDepthUpdates.get(message.symbol) ?? new CircularBuffer<MexcFuturesDepthUpdateData>(2000)
120
+ bufferedUpdates.append({ firstVersion, lastVersion })
121
+ this.bufferedDepthUpdates.set(message.symbol, bufferedUpdates)
122
+ }
123
+
124
+ protected override async provideManualSnapshots(filters: Filter<string>[], shouldCancel: () => boolean) {
125
+ const depthFilter = filters.find((filter) => filter.channel === 'push.depth.snapshot')
126
+ if (depthFilter === undefined) {
127
+ return
128
+ }
129
+
130
+ this.debug('requesting manual snapshots for: %s', depthFilter.symbols!)
131
+
132
+ for (const symbol of depthFilter.symbols!) {
133
+ await this.provideManualSnapshot(symbol.toUpperCase(), shouldCancel)
134
+ }
135
+
136
+ this.debug('requested manual snapshots successfully for: %s ', depthFilter.symbols!)
137
+ }
138
+
139
+ protected override messageIsError(message: any): boolean {
140
+ return message.channel === 'rs.error' || message.success === false || message.error !== undefined
141
+ }
142
+
143
+ protected override messageIsHeartbeat(message: any) {
144
+ return message.channel === 'pong'
145
+ }
146
+
147
+ private async provideManualSnapshot(symbol: string, shouldCancel: () => boolean) {
148
+ const maxSnapshotRounds = 4
149
+ const maxSnapshotAttemptsPerRound = 3
150
+
151
+ for (let round = 0; round < maxSnapshotRounds; round++) {
152
+ for (let attempt = 1; attempt <= maxSnapshotAttemptsPerRound; attempt++) {
153
+ if (shouldCancel()) {
154
+ return
155
+ }
156
+
157
+ const { data } = await getJSON<MexcFuturesDepthSnapshotResponse>(`${this.httpURL}/api/v1/contract/depth/${symbol}?limit=5000`)
158
+ if (this.snapshotResponseIsValid(data) === false) {
159
+ if (attempt < maxSnapshotAttemptsPerRound) {
160
+ await wait(attempt * 1000)
161
+ }
162
+ continue
163
+ }
164
+
165
+ const hasOverlap = await this.waitForSnapshotOverlap(symbol, data.data.version)
166
+
167
+ if (shouldCancel()) {
168
+ return
169
+ }
170
+
171
+ if (hasOverlap === false) {
172
+ this.trimBufferedUpdates(symbol)
173
+ if (attempt < maxSnapshotAttemptsPerRound) {
174
+ await wait(attempt * 1000)
175
+ }
176
+ continue
177
+ }
178
+
179
+ if (hasOverlap === true || attempt === maxSnapshotAttemptsPerRound) {
180
+ this.manualSnapshotsBuffer.push(this.createManualSnapshot(symbol, data.data))
181
+ this.pendingDepthSnapshotSymbols.delete(symbol)
182
+ this.bufferedDepthUpdates.delete(symbol)
183
+ return
184
+ }
185
+ }
186
+ }
187
+
188
+ throw new Error(`MexcFuturesRealTimeFeed could not align depth snapshot for ${symbol}`)
189
+ }
190
+
191
+ private async waitForSnapshotOverlap(symbol: string, snapshotVersion: number) {
192
+ let hasOverlap = this.validateSnapshotOverlap(symbol, snapshotVersion)
193
+ for (let attempt = 0; attempt < 60; attempt++) {
194
+ if (hasOverlap !== undefined) {
195
+ return hasOverlap
196
+ }
197
+
198
+ await wait(100)
199
+ hasOverlap = this.validateSnapshotOverlap(symbol, snapshotVersion)
200
+ }
201
+
202
+ return hasOverlap
203
+ }
204
+
205
+ private validateSnapshotOverlap(symbol: string, snapshotVersion: number) {
206
+ const bufferedUpdates = this.bufferedDepthUpdates.get(symbol)
207
+ for (const update of bufferedUpdates?.items() ?? []) {
208
+ if (update.lastVersion <= snapshotVersion) {
209
+ continue
210
+ }
211
+
212
+ return update.firstVersion <= snapshotVersion + 1 && update.lastVersion >= snapshotVersion + 1
213
+ }
214
+
215
+ return undefined
216
+ }
217
+
218
+ private trimBufferedUpdates(symbol: string) {
219
+ const bufferedUpdates = this.bufferedDepthUpdates.get(symbol)
220
+ if (bufferedUpdates === undefined || bufferedUpdates.count <= 100) {
221
+ return
222
+ }
223
+
224
+ const trimmed = new CircularBuffer<MexcFuturesDepthUpdateData>(2000)
225
+ for (const update of [...bufferedUpdates.items()].slice(-100)) {
226
+ trimmed.append(update)
227
+ }
228
+ this.bufferedDepthUpdates.set(symbol, trimmed)
229
+ }
230
+
231
+ private snapshotResponseIsValid(response: MexcFuturesDepthSnapshotResponse) {
232
+ return (
233
+ response.success === true &&
234
+ response.code === 0 &&
235
+ response.data !== undefined &&
236
+ Number.isFinite(response.data.version) &&
237
+ response.data.version >= 0 &&
238
+ Array.isArray(response.data.asks) &&
239
+ Array.isArray(response.data.bids)
240
+ )
241
+ }
242
+
243
+ private createManualSnapshot(symbol: string, data: MexcFuturesDepthSnapshotData): MexcFuturesDepthSnapshotMessage {
244
+ return {
245
+ channel: 'push.depth.snapshot',
246
+ symbol,
247
+ generated: true,
248
+ data: {
249
+ cts: data.cts,
250
+ asks: data.asks,
251
+ bids: data.bids,
252
+ timestamp: data.timestamp,
253
+ version: data.version
254
+ }
255
+ }
256
+ }
257
+ }
258
+
259
+ type MexcFuturesDepthSnapshotResponse = {
260
+ success: boolean
261
+ code: number
262
+ message?: string
263
+ data: MexcFuturesDepthSnapshotData
264
+ }
265
+
266
+ type MexcFuturesDepthUpdateData = {
267
+ firstVersion: number
268
+ lastVersion: number
269
+ }
@@ -1,5 +1,6 @@
1
1
  import crypto from 'crypto'
2
2
  import { wait } from '../handy.ts'
3
+ import { getOkexOptionsFamilyOrIndex, getOkexOptionsUnderlyingIndex } from '../okexsymbols.ts'
3
4
  import { Filter } from '../types.ts'
4
5
  import { MultiConnectionRealTimeFeedBase, RealTimeFeedBase } from './realtimefeed.ts'
5
6
 
@@ -140,7 +141,8 @@ class OkexOptionsSingleRealTimeFeed extends OkexSingleRealTimeFeed {
140
141
  super(wssURL, exchange, filters, timeoutIntervalMS, onError)
141
142
  }
142
143
 
143
- private _defaultIndexes = ['BTC-USD', 'ETH-USD']
144
+ private _defaultIndexes = ['BTC-USD', 'ETH-USD', 'SOL-USD']
145
+ private _defaultOptionSummaryInstrumentFamilies = ['BTC-USD', 'BTC-USD_UM', 'ETH-USD', 'ETH-USD_UM', 'SOL-USD_UM']
144
146
 
145
147
  private _channelRequiresIndexNotSymbol(channel: string) {
146
148
  if (channel === 'index-tickers' || channel === 'opt-summary') {
@@ -155,7 +157,7 @@ class OkexOptionsSingleRealTimeFeed extends OkexSingleRealTimeFeed {
155
157
  const channelRequiresIndexNotSymbol = this._channelRequiresIndexNotSymbol(filter.channel)
156
158
 
157
159
  if (symbols.length === 0 && channelRequiresIndexNotSymbol) {
158
- symbols = this._defaultIndexes
160
+ symbols = filter.channel === 'opt-summary' ? this._defaultOptionSummaryInstrumentFamilies : this._defaultIndexes
159
161
  }
160
162
 
161
163
  if (symbols.length === 0) {
@@ -165,8 +167,7 @@ class OkexOptionsSingleRealTimeFeed extends OkexSingleRealTimeFeed {
165
167
  return symbols.map((symbol) => {
166
168
  let finalSymbol = symbol
167
169
  if (channelRequiresIndexNotSymbol) {
168
- const symbolParts = symbol.split('-')
169
- finalSymbol = `${symbolParts[0]}-${symbolParts[1]}`
170
+ finalSymbol = filter.channel === 'opt-summary' ? getOkexOptionsFamilyOrIndex(symbol) : getOkexOptionsUnderlyingIndex(symbol)
170
171
  }
171
172
  return {
172
173
  channel: filter.channel,
@@ -176,11 +177,12 @@ class OkexOptionsSingleRealTimeFeed extends OkexSingleRealTimeFeed {
176
177
  })
177
178
  })
178
179
  .flatMap((s) => s)
180
+ const uniqueArgs = [...new Map(args.map((arg) => [`${arg.channel}:${arg.instId ?? ''}:${arg.instFamily ?? ''}`, arg])).values()]
179
181
 
180
182
  return [
181
183
  {
182
184
  op: 'subscribe',
183
- args: [...new Set(args)]
185
+ args: uniqueArgs
184
186
  }
185
187
  ]
186
188
  }
@@ -117,7 +117,7 @@ export abstract class RealTimeFeedBase implements RealTimeFeedIterable {
117
117
  message = message.toString().replace(/"id":([0-9]+),/g, '"id":"$1",') as any
118
118
  }
119
119
 
120
- const messageDeserialized = JSON.parse(message as any)
120
+ const messageDeserialized = this.parseMessage(message)
121
121
 
122
122
  if (this.messageIsError(messageDeserialized)) {
123
123
  if (this.isIgnoredError(messageDeserialized)) {
@@ -233,6 +233,10 @@ export abstract class RealTimeFeedBase implements RealTimeFeedIterable {
233
233
 
234
234
  protected abstract messageIsError(message: any): boolean
235
235
 
236
+ protected parseMessage(message: Buffer<ArrayBufferLike>): any {
237
+ return JSON.parse(message as any)
238
+ }
239
+
236
240
  protected sendCustomPing: (() => void) | undefined = undefined
237
241
 
238
242
  protected isIgnoredError(_message: any) {