@icgio/icg-exchanges 1.40.45 → 1.40.46

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/README.md CHANGED
@@ -89,11 +89,11 @@ Each exchange class extends `ExchangeBase` and implements:
89
89
 
90
90
  The WebSocket base class (`exchange-ws.js`) uses three layers of detection to identify and recover from dead or degraded connections:
91
91
 
92
- ### Layer 1: Depth staleness (`ts_dict` check)
92
+ ### Layer 1: Depth staleness (`market_ts_by_pair` check)
93
93
 
94
- Runs every 10 seconds. For each subscribed pair, checks if `ts_dict[pair].received_ts` is older than `market_stale_time` (default 120s). If stale, disconnects and auto-reconnects after 5s.
94
+ Runs every 10 seconds. For each subscribed pair, checks if `market_ts_by_pair[pair].ts_ms_local_received` is older than `market_stale_time` (default 120s). If stale, disconnects and auto-reconnects after 5s.
95
95
 
96
- `ts_dict` is updated when:
96
+ `market_ts_by_pair` is updated when:
97
97
 
98
98
  - A depth message is received (all exchanges)
99
99
  - A server-initiated ping is received (lbank only — since lbank stops sending depth updates for low-liquidity pairs when the orderbook hasn't changed, the server ping acts as a liveness signal)
@@ -112,8 +112,9 @@ module.exports = class Ascendex extends ExchangeBase {
112
112
  }),
113
113
  )
114
114
  })
115
- this.ws.market.on_message((data) => {
115
+ this.ws.market.on_message((data, hr_recv) => {
116
116
  data = JSON.parse(data)
117
+ const hr_parsed = process.hrtime()
117
118
  if (data.op === 'pong') {
118
119
  this.ws.market.last_pong = new Date()
119
120
  } else {
@@ -126,7 +127,7 @@ module.exports = class Ascendex extends ExchangeBase {
126
127
  for (let bid of data.data.bids) {
127
128
  this.ws.market.bids_dict[pair].insert(bid[0], bid[1])
128
129
  }
129
- this.ws.market.ts_dict[pair] = { received_ts: Date.now() }
130
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ hr_recv, hr_parsed })
130
131
  const asks_s = this.ws.market.asks_dict[pair].entries()
131
132
  const bids_s = this.ws.market.bids_dict[pair].entries()
132
133
  if (asks_s.length > 0 && bids_s.length > 0) {
@@ -152,7 +153,7 @@ module.exports = class Ascendex extends ExchangeBase {
152
153
  this.ws.market.bids_dict[pair].insert(parseFloat(bid[0]), parseFloat(bid[1]))
153
154
  }
154
155
  }
155
- this.ws.market.ts_dict[pair] = { received_ts: Date.now() }
156
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ hr_recv, hr_parsed })
156
157
  const asks_u = this.ws.market.asks_dict[pair].entries()
157
158
  const bids_u = this.ws.market.bids_dict[pair].entries()
158
159
  if (asks_u.length > 0 && bids_u.length > 0) {
@@ -109,8 +109,9 @@ module.exports = class Biconomy extends ExchangeBase {
109
109
  }),
110
110
  )
111
111
  })
112
- this.ws.market.on_message((data) => {
112
+ this.ws.market.on_message((data, hr_recv) => {
113
113
  data = JSON.parse(data)
114
+ const hr_parsed = process.hrtime()
114
115
  if (data && data.method === 'depth.update') {
115
116
  let pair = post_process_pair(data.params[2])
116
117
  this.ws.market.pair_status[pair] = 'opened'
@@ -147,7 +148,7 @@ module.exports = class Biconomy extends ExchangeBase {
147
148
  }
148
149
  }
149
150
  }
150
- this.ws.market.ts_dict[pair] = { received_ts: Date.now() }
151
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ hr_recv, hr_parsed })
151
152
  const asks_e = this.ws.market.asks_dict[pair].entries()
152
153
  const bids_e = this.ws.market.bids_dict[pair].entries()
153
154
  if (asks_e.length > 0 && bids_e.length > 0) {
@@ -173,8 +174,13 @@ module.exports = class Biconomy extends ExchangeBase {
173
174
  this.ws.market.last_pong = new Date()
174
175
  const now = Date.now()
175
176
  pair_list.forEach((pair) => {
176
- if (this.ws.market.ts_dict[pair]) {
177
- this.ws.market.ts_dict[pair].received_ts = now
177
+ if (this.ws.market.market_ts_by_pair[pair]) {
178
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({
179
+ ...this.ws.market.market_ts_by_pair[pair],
180
+ ts_ms_local_received: now,
181
+ hr_recv,
182
+ hr_parsed,
183
+ })
178
184
  }
179
185
  })
180
186
  } else {
@@ -73,8 +73,9 @@ module.exports = class Binance extends ExchangeBase {
73
73
  ;((this.rate_last_id = {}), (this.first_rate = {}))
74
74
  this.ws.market_socket = new websocket('wss://stream.binance.com:9443/stream?streams=' + streams.slice(0, -1))
75
75
  this.ws.market.on_open(() => {})
76
- this.ws.market.on_message((data) => {
76
+ this.ws.market.on_message((data, hr_recv) => {
77
77
  data = JSON.parse(data).data
78
+ const hr_parsed = process.hrtime()
78
79
  if (data.e === 'depthUpdate') {
79
80
  let pair = post_process_pair(data.s, true)
80
81
  this.ws.market.pair_status[pair] = 'opened'
@@ -103,7 +104,7 @@ module.exports = class Binance extends ExchangeBase {
103
104
  this.ws.market.bids_dict[pair].insert(parseFloat(bid[0]), parseFloat(bid[1]))
104
105
  }
105
106
  }
106
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: data.E }
107
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: data.E, hr_recv, hr_parsed })
107
108
  } else if (this.first_rate[pair] && data.U > lastu[pair] + 1) {
108
109
  this.first_rate[pair] = false
109
110
  }
@@ -134,7 +135,7 @@ module.exports = class Binance extends ExchangeBase {
134
135
  if (options.depth === false) {
135
136
  this.ws.market.asks_dict[pair] = [[parseFloat(data.a), parseFloat(data.A)]]
136
137
  this.ws.market.bids_dict[pair] = [[parseFloat(data.b), parseFloat(data.B)]]
137
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: data.E || Date.now() }
138
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: data.E || Date.now(), hr_recv, hr_parsed })
138
139
  this.ws.market.pair_status[pair] = 'opened'
139
140
  }
140
141
  this.ws.bbo_observable.notify()
@@ -136,20 +136,21 @@ module.exports = class Bingx extends ExchangeBase {
136
136
  this.ws.market.bids_dict[pair] = utils.ordered_dict(false)
137
137
  })
138
138
  })
139
- this.ws.market.on_message((data) => {
139
+ this.ws.market.on_message((data, hr_recv) => {
140
140
  zlib.gunzip(data, (err, decompressedData) => {
141
141
  if (err) {
142
142
  console.error('Error decompressing data:', err)
143
143
  } else {
144
144
  let data = decompressedData.toString('utf8')
145
145
  data = JSON.parse(data)
146
+ const hr_parsed = process.hrtime()
146
147
  if (data && _.includes(data.dataType, 'depth')) {
147
148
  let parts = data.dataType.split('@')
148
149
  let pair = post_process_pair(parts[0])
149
150
  this.ws.market.pair_status[pair] = 'opened'
150
151
  this.ws.market.asks_dict[pair] = data.data.asks.map((ask) => [parseFloat(ask[0]), parseFloat(ask[1])]).reverse()
151
152
  this.ws.market.bids_dict[pair] = data.data.bids.map((bid) => [parseFloat(bid[0]), parseFloat(bid[1])])
152
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: data.timestamp }
153
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: data.timestamp, hr_recv, hr_parsed })
153
154
  const asks = this.ws.market.asks_dict[pair]
154
155
  const bids = this.ws.market.bids_dict[pair]
155
156
  if (asks && asks.length > 0 && bids && bids.length > 0) {
@@ -147,18 +147,19 @@ module.exports = class Bitget extends ExchangeBase {
147
147
  })
148
148
  this.ws.market.ping('ping')
149
149
  })
150
- this.ws.market.on_message((data) => {
150
+ this.ws.market.on_message((data, hr_recv) => {
151
151
  if (data.toString() === 'pong') {
152
152
  this.ws.market.last_pong = new Date()
153
153
  } else {
154
154
  data = JSON.parse(data)
155
+ const hr_parsed = process.hrtime()
155
156
  let book = data.data
156
157
  if (data && data.arg && data.arg.channel === depth_val && Array.isArray(book) && book) {
157
158
  let pair = data.arg.instId
158
159
  this.ws.market.pair_status[pair] = 'opened'
159
160
  this.ws.market.asks_dict[pair] = book[0].asks.map((ask) => [parseFloat(ask[0]), parseFloat(ask[1])])
160
161
  this.ws.market.bids_dict[pair] = book[0].bids.map((bid) => [parseFloat(bid[0]), parseFloat(bid[1])])
161
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parseInt(book[0].ts) }
162
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: parseInt(book[0].ts), hr_recv, hr_parsed })
162
163
  } else if (data && data.arg && data.arg.channel === ticker_val && Array.isArray(book) && book) {
163
164
  let pair = data.arg.instId
164
165
  let t = book[0]
@@ -175,7 +176,7 @@ module.exports = class Bitget extends ExchangeBase {
175
176
  if (options.depth === false) {
176
177
  this.ws.market.asks_dict[pair] = [[ask_1_price, ask_1_amount]]
177
178
  this.ws.market.bids_dict[pair] = [[bid_1_price, bid_1_amount]]
178
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parseInt(t.ts || data.ts) || Date.now() }
179
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: parseInt(t.ts || data.ts) || Date.now(), hr_recv, hr_parsed })
179
180
  this.ws.market.pair_status[pair] = 'opened'
180
181
  }
181
182
  this.ws.bbo_observable.notify()
@@ -165,11 +165,12 @@ module.exports = class Bitmart extends ExchangeBase {
165
165
  })
166
166
  this.ws.market.ping('ping')
167
167
  })
168
- this.ws.market.on_message((data) => {
168
+ this.ws.market.on_message((data, hr_recv) => {
169
169
  if (data.toString() === 'pong') {
170
170
  this.ws.market.last_pong = new Date()
171
171
  } else {
172
172
  data = JSON.parse(data)
173
+ const hr_parsed = process.hrtime()
173
174
  let book = data.data
174
175
  // Get the pair from the message's symbol field
175
176
  const msg_symbol = data.symbol
@@ -178,7 +179,7 @@ module.exports = class Bitmart extends ExchangeBase {
178
179
  this.ws.market.pair_status[target_pair] = 'opened'
179
180
  this.ws.market.asks_dict[target_pair] = book.sells.map((ask) => [parseFloat(ask.price), parseFloat(ask.amount)])
180
181
  this.ws.market.bids_dict[target_pair] = book.buys.map((bid) => [parseFloat(bid.price), parseFloat(bid.amount)])
181
- this.ws.market.ts_dict[target_pair] = { received_ts: Date.now(), event_ts: parseInt(data.timeStamp) || undefined }
182
+ this.ws.market.market_ts_by_pair[target_pair] = this.ws.market.build_ts({ ts_ms_venue_event: parseInt(data.timeStamp) || undefined, hr_recv, hr_parsed })
182
183
  const asks = this.ws.market.asks_dict[target_pair]
183
184
  const bids = this.ws.market.bids_dict[target_pair]
184
185
  if (asks && asks.length > 0 && bids && bids.length > 0) {
@@ -122,20 +122,21 @@ module.exports = class Bitrue extends ExchangeBase {
122
122
  }),
123
123
  )
124
124
  })
125
- this.ws.market.on_message((data) => {
125
+ this.ws.market.on_message((data, hr_recv) => {
126
126
  zlib.gunzip(data, (err, decompressedData) => {
127
127
  if (err) {
128
128
  console.error('Error decompressing data:', err)
129
129
  } else {
130
130
  let data = decompressedData.toString('utf8')
131
131
  data = JSON.parse(data)
132
+ const hr_parsed = process.hrtime()
132
133
  if (data && _.includes(data.channel, 'depth_step0') && data.tick) {
133
134
  let parts = data.channel.split('_')
134
135
  let pair = post_process_pair(parts[1])
135
136
  this.ws.market.pair_status[pair] = 'opened'
136
137
  this.ws.market.asks_dict[pair] = data.tick.asks.map((ask) => [parseFloat(ask[0]), parseFloat(ask[1])])
137
138
  this.ws.market.bids_dict[pair] = data.tick.buys.map((bid) => [parseFloat(bid[0]), parseFloat(bid[1])])
138
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: data.ts }
139
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: data.ts, hr_recv, hr_parsed })
139
140
  const asks = this.ws.market.asks_dict[pair]
140
141
  const bids = this.ws.market.bids_dict[pair]
141
142
  if (asks && asks.length > 0 && bids && bids.length > 0) {
@@ -165,8 +166,13 @@ module.exports = class Bitrue extends ExchangeBase {
165
166
  // Server ping proves connection is alive — reset stale timer for all opened pairs
166
167
  const now = Date.now()
167
168
  pair_list.forEach((pair) => {
168
- if (this.ws.market.ts_dict[pair]) {
169
- this.ws.market.ts_dict[pair].received_ts = now
169
+ if (this.ws.market.market_ts_by_pair[pair]) {
170
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({
171
+ ...this.ws.market.market_ts_by_pair[pair],
172
+ ts_ms_local_received: now,
173
+ hr_recv,
174
+ hr_parsed,
175
+ })
170
176
  }
171
177
  })
172
178
  } else {
@@ -150,9 +150,10 @@ module.exports = class Blofin extends ExchangeBase {
150
150
  }),
151
151
  )
152
152
  })
153
- this.ws.market.on_message((data) => {
153
+ this.ws.market.on_message((data, hr_recv) => {
154
154
  try {
155
155
  data = JSON.parse(data)
156
+ const hr_parsed = process.hrtime()
156
157
  if (data.channel === 'DEPTH') {
157
158
  const action = data.data_type
158
159
  const pair = post_process_pair(data.symbol)
@@ -168,7 +169,7 @@ module.exports = class Blofin extends ExchangeBase {
168
169
  const [price, size] = bid
169
170
  this.ws.market.bids_dict[pair].insert(parseFloat(price), parseFloat(size))
170
171
  }
171
- this.ws.market.ts_dict[pair] = { received_ts: Date.now() }
172
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ hr_recv, hr_parsed })
172
173
  {
173
174
  const _asks = this.ws.market.asks_dict[pair].entries()
174
175
  const _bids = this.ws.market.bids_dict[pair].entries()
@@ -197,7 +198,7 @@ module.exports = class Blofin extends ExchangeBase {
197
198
  this.ws.market.bids_dict[pair].insert(parseFloat(price), parseFloat(size))
198
199
  }
199
200
  }
200
- this.ws.market.ts_dict[pair] = { received_ts: Date.now() }
201
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ hr_recv, hr_parsed })
201
202
  {
202
203
  const _asks = this.ws.market.asks_dict[pair].entries()
203
204
  const _bids = this.ws.market.bids_dict[pair].entries()
@@ -96,11 +96,12 @@ module.exports = class Btse extends ExchangeBase {
96
96
  })
97
97
  this.ws.market.ping('ping')
98
98
  })
99
- this.ws.market.on_message((data) => {
99
+ this.ws.market.on_message((data, hr_recv) => {
100
100
  if (data.toString() === 'pong') {
101
101
  this.ws.market.last_pong = new Date()
102
102
  } else {
103
103
  data = JSON.parse(data)
104
+ const hr_parsed = process.hrtime()
104
105
  if (data && data.topic && data.topic.includes('snapshot:' || 'update:') && data.data) {
105
106
  let pair = post_process_pair(data.data.symbol)
106
107
  let current_timestamp = new Date().getTime()
@@ -132,7 +133,7 @@ module.exports = class Btse extends ExchangeBase {
132
133
  }
133
134
  }
134
135
  }
135
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: book.timestamp }
136
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: book.timestamp, hr_recv, hr_parsed })
136
137
  {
137
138
  const _asks = this.ws.market.asks_dict[pair].entries()
138
139
  const _bids = this.ws.market.bids_dict[pair].entries()
@@ -90,9 +90,10 @@ module.exports = class Bybit extends ExchangeBase {
90
90
  })
91
91
  this.ws.market.ping(JSON.stringify({ op: 'ping' }))
92
92
  })
93
- this.ws.market.on_message((data) => {
93
+ this.ws.market.on_message((data, hr_recv) => {
94
94
  try {
95
95
  data = JSON.parse(data)
96
+ const hr_parsed = process.hrtime()
96
97
  if (data.success && data.ret_msg === 'pong') {
97
98
  this.ws.market.last_pong = new Date()
98
99
  } else {
@@ -131,7 +132,7 @@ module.exports = class Bybit extends ExchangeBase {
131
132
  }
132
133
  }
133
134
  }
134
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: data.ts }
135
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: data.ts, hr_recv, hr_parsed })
135
136
  // Populate BBO from orderbook data
136
137
  if (options.bbo !== false && data.data.a && data.data.a.length > 0 && data.data.b && data.data.b.length > 0) {
137
138
  const best_ask = data.data.a[0]
@@ -229,10 +229,14 @@ class CoinbaseFixMd {
229
229
  }
230
230
 
231
231
  _handle_md(fields) {
232
+ const hr_recv = process.hrtime()
232
233
  let entry = parse_md_entry(fields)
233
234
  if (!entry) return
234
- entry.event_ts = parse_event_ts_ms(entry.time)
235
- entry.received_ts = Date.now()
235
+ const hr_parsed = process.hrtime()
236
+ entry.ts_ms_venue_event = parse_event_ts_ms(entry.time)
237
+ entry.ts_ms_local_received = Date.now()
238
+ entry.hr_recv = hr_recv
239
+ entry.hr_parsed = hr_parsed
236
240
  this.on_event(entry)
237
241
  }
238
242
 
@@ -240,10 +244,14 @@ class CoinbaseFixMd {
240
244
  let entries = []
241
245
  let now = Date.now()
242
246
  for (let fields of batch) {
247
+ const hr_recv = process.hrtime()
243
248
  let entry = parse_md_entry(fields)
244
249
  if (!entry) continue
245
- entry.event_ts = parse_event_ts_ms(entry.time)
246
- entry.received_ts = now
250
+ const hr_parsed = process.hrtime()
251
+ entry.ts_ms_venue_event = parse_event_ts_ms(entry.time)
252
+ entry.ts_ms_local_received = now
253
+ entry.hr_recv = hr_recv
254
+ entry.hr_parsed = hr_parsed
247
255
  entries.push(entry)
248
256
  }
249
257
  if (entries.length) this.on_batch(entries)
@@ -219,13 +219,14 @@ module.exports = class Coinbase extends ExchangeBase {
219
219
  }))
220
220
  console.log('coinbase ws-direct: subscribed channels:', channels.join(', '))
221
221
  })
222
- this.ws.market.on_message((data) => {
222
+ this.ws.market.on_message((data, hr_recv) => {
223
223
  try {
224
224
  data = JSON.parse(data)
225
225
  } catch (err) {
226
226
  console.error('coinbase', 'ws-direct', 'JSON parse error:', err.message)
227
227
  return
228
228
  }
229
+ const hr_parsed = process.hrtime()
229
230
  if (data.type === 'snapshot') {
230
231
  const pair = post_process_pair(data.product_id)
231
232
  if (!this.ws.market.asks_dict[pair]) return
@@ -252,7 +253,7 @@ module.exports = class Coinbase extends ExchangeBase {
252
253
  if (price >= lo && price <= hi) this.ws.market.asks_dict[pair].insert(price, qty)
253
254
  }
254
255
  this.ws.market.pair_status[pair] = 'opened'
255
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: Date.now() }
256
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: Date.now(), hr_recv, hr_parsed })
256
257
  const top_bid = this.ws.market.bids_dict[pair].top()
257
258
  const top_ask = this.ws.market.asks_dict[pair].top()
258
259
  if (top_bid != null && top_ask != null) {
@@ -283,7 +284,7 @@ module.exports = class Coinbase extends ExchangeBase {
283
284
  }
284
285
  }
285
286
  }
286
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parse_event_ts_ms(data.time) || Date.now() }
287
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: parse_event_ts_ms(data.time) || Date.now(), hr_recv, hr_parsed })
287
288
  {
288
289
  const top_bid = this.ws.market.bids_dict[pair].top()
289
290
  const top_ask = this.ws.market.asks_dict[pair].top()
@@ -306,7 +307,7 @@ module.exports = class Coinbase extends ExchangeBase {
306
307
  if (options.depth === false) {
307
308
  this.ws.market.asks_dict[pair] = [[best_ask, best_ask_qty]]
308
309
  this.ws.market.bids_dict[pair] = [[best_bid, best_bid_qty]]
309
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parse_event_ts_ms(data.time) || Date.now() }
310
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: parse_event_ts_ms(data.time) || Date.now(), hr_recv, hr_parsed })
310
311
  this.ws.market.pair_status[pair] = 'opened'
311
312
  } else {
312
313
  const bids = this.ws.market.bids_dict[pair]
@@ -323,7 +324,7 @@ module.exports = class Coinbase extends ExchangeBase {
323
324
  for (const k of prune_bids) bids.del(k)
324
325
  const prune_asks = asks.keys().filter((k) => k > hi)
325
326
  for (const k of prune_asks) asks.del(k)
326
- if (bbo_changed) this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parse_event_ts_ms(data.time) || Date.now() }
327
+ if (bbo_changed) this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: parse_event_ts_ms(data.time) || Date.now(), hr_recv, hr_parsed })
327
328
  }
328
329
  if (bbo_changed) this.ws.bbo_observable.notify()
329
330
  } else if (data.type === 'match' || data.type === 'last_match') {
@@ -406,13 +407,14 @@ module.exports = class Coinbase extends ExchangeBase {
406
407
  this.ws.market.send(JSON.stringify({ type: 'subscribe', product_ids, channel: 'market_trades' }))
407
408
  }
408
409
  })
409
- this.ws.market.on_message((data) => {
410
+ this.ws.market.on_message((data, hr_recv) => {
410
411
  try {
411
412
  data = JSON.parse(data)
412
413
  } catch (err) {
413
414
  console.error('coinbase', 'websocket', 'JSON parse error:', err.message)
414
415
  return
415
416
  }
417
+ const hr_parsed = process.hrtime()
416
418
  const seq = data.sequence_num
417
419
  if (seq !== undefined) {
418
420
  const gap = (this._ws_last_seq !== undefined && seq > this._ws_last_seq + 1) ? seq - this._ws_last_seq - 1 : 0
@@ -483,10 +485,11 @@ module.exports = class Coinbase extends ExchangeBase {
483
485
  }
484
486
  }
485
487
  }
486
- this.ws.market.ts_dict[pair] = {
487
- received_ts: Date.now(),
488
- event_ts: parse_event_ts_ms(data.timestamp) || Date.now(),
489
- }
488
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({
489
+ ts_ms_venue_event: parse_event_ts_ms(data.timestamp) || Date.now(),
490
+ hr_recv,
491
+ hr_parsed,
492
+ })
490
493
  {
491
494
  const top_bid = this.ws.market.bids_dict[pair].top()
492
495
  const top_ask = this.ws.market.asks_dict[pair].top()
@@ -532,7 +535,11 @@ module.exports = class Coinbase extends ExchangeBase {
532
535
  if (options.depth === false) {
533
536
  this.ws.market.asks_dict[pair] = [[best_ask, best_ask_qty]]
534
537
  this.ws.market.bids_dict[pair] = [[best_bid, best_bid_qty]]
535
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parse_event_ts_ms(data.timestamp) || Date.now() }
538
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({
539
+ ts_ms_venue_event: parse_event_ts_ms(data.timestamp) || Date.now(),
540
+ hr_recv,
541
+ hr_parsed,
542
+ })
536
543
  this.ws.market.pair_status[pair] = 'opened'
537
544
  } else {
538
545
  const bids = this.ws.market.bids_dict[pair]
@@ -550,7 +557,11 @@ module.exports = class Coinbase extends ExchangeBase {
550
557
  for (const k of prune_bids) bids.del(k)
551
558
  const prune_asks = asks.keys().filter((k) => k > hi)
552
559
  for (const k of prune_asks) asks.del(k)
553
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parse_event_ts_ms(data.timestamp) || Date.now() }
560
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({
561
+ ts_ms_venue_event: parse_event_ts_ms(data.timestamp) || Date.now(),
562
+ hr_recv,
563
+ hr_parsed,
564
+ })
554
565
  }
555
566
  this.ws.bbo_observable.notify()
556
567
  }
@@ -674,7 +685,7 @@ module.exports = class Coinbase extends ExchangeBase {
674
685
  const top_ask = asks.top()
675
686
  if (top_bid != null && top_ask != null) {
676
687
  this.ws.market.tickers[pair] = { bid: top_bid, bid_size: bids.val(top_bid), ask: top_ask, ask_size: asks.val(top_ask) }
677
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: Date.now() }
688
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: Date.now() })
678
689
  }
679
690
 
680
691
  // Replay buffered events
@@ -754,8 +765,13 @@ module.exports = class Coinbase extends ExchangeBase {
754
765
  const pair = this._fix_md_pair_by_symbol[entry.symbol] || post_process_pair(entry.symbol)
755
766
  if (!this.ws.market.asks_dict[pair]) continue
756
767
 
757
- this.ws.market.last_message_time = entry.received_ts || Date.now()
758
- this.ws.market.ts_dict[pair] = { received_ts: entry.received_ts || Date.now(), event_ts: entry.event_ts || Date.now() }
768
+ this.ws.market.last_message_time = entry.ts_ms_local_received || Date.now()
769
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({
770
+ ts_ms_local_received: entry.ts_ms_local_received || Date.now(),
771
+ ts_ms_venue_event: entry.ts_ms_venue_event || Date.now(),
772
+ hr_recv: entry.hr_recv,
773
+ hr_parsed: entry.hr_parsed,
774
+ })
759
775
 
760
776
  const snap_seq = this._fix_md_snapshot_seq[pair]
761
777
  if (snap_seq === null) {
@@ -795,11 +811,16 @@ module.exports = class Coinbase extends ExchangeBase {
795
811
  price: entry.price,
796
812
  amount: entry.size,
797
813
  total: entry.price * entry.size,
798
- close_time: entry.event_ts ? new Date(entry.event_ts) : new Date(),
814
+ close_time: entry.ts_ms_venue_event ? new Date(entry.ts_ms_venue_event) : new Date(),
799
815
  }
800
816
  this.ws.market.market_history_dict[pair].unshift(trade)
801
817
  this.ws.market.market_history_dict[pair] = this.ws.market.market_history_dict[pair].filter((tr) => tr.close_time > new Date() - this.ws.market.market_history_store_time)
802
- this.ws.market.ts_dict[pair] = { received_ts: entry.received_ts || Date.now(), event_ts: entry.event_ts || Date.now() }
818
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({
819
+ ts_ms_local_received: entry.ts_ms_local_received || Date.now(),
820
+ ts_ms_venue_event: entry.ts_ms_venue_event || Date.now(),
821
+ hr_recv: entry.hr_recv,
822
+ hr_parsed: entry.hr_parsed,
823
+ })
803
824
 
804
825
  // Don't modify the book aggregate on trades — let CHANGE/DELETE events manage it.
805
826
  // TRADE + CHANGE would double-subtract for partial fills. Instead:
@@ -135,14 +135,15 @@ module.exports = class Coinstore extends ExchangeBase {
135
135
  false,
136
136
  )
137
137
  })
138
- this.ws.market.on_message((data) => {
138
+ this.ws.market.on_message((data, hr_recv) => {
139
139
  data = JSON.parse(data)
140
+ const hr_parsed = process.hrtime()
140
141
  if (data && data.T === 'depth') {
141
142
  let pair = post_process_pair(data.symbol)
142
143
  this.ws.market.pair_status[pair] = 'opened'
143
144
  this.ws.market.asks_dict[pair] = data.a.map((ask) => [parseFloat(ask[0]), parseFloat(ask[1])])
144
145
  this.ws.market.bids_dict[pair] = data.b.map((bid) => [parseFloat(bid[0]), parseFloat(bid[1])])
145
- this.ws.market.ts_dict[pair] = { received_ts: Date.now() }
146
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ hr_recv, hr_parsed })
146
147
  const asks = this.ws.market.asks_dict[pair]
147
148
  const bids = this.ws.market.bids_dict[pair]
148
149
  if (asks && asks.length > 0 && bids && bids.length > 0) {
@@ -109,10 +109,12 @@ module.exports = class Cryptocom extends ExchangeBase {
109
109
  )
110
110
  }
111
111
  })
112
- this.ws.market.on_message((data) => {
112
+ this.ws.market.on_message((data, hr_recv) => {
113
113
  let parsed
114
+ let hr_parsed
114
115
  try {
115
116
  parsed = JSON.parse(data)
117
+ hr_parsed = process.hrtime()
116
118
  } catch (error) {
117
119
  return
118
120
  }
@@ -130,7 +132,7 @@ module.exports = class Cryptocom extends ExchangeBase {
130
132
  let pair = post_process_pair(result.instrument_name, true)
131
133
  if (result.channel === 'book') {
132
134
  this.ws.market.pair_status[pair] = 'opened'
133
- this.ws.market.ts_dict[pair] = { received_ts: Date.now() }
135
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ hr_recv, hr_parsed })
134
136
  let { asks, bids } = result.data[0]
135
137
  this.ws.market.asks_dict[pair] = asks.map((e) => [parseFloat(e[0]), parseFloat(e[1])])
136
138
  this.ws.market.bids_dict[pair] = bids.map((e) => [parseFloat(e[0]), parseFloat(e[1])])
@@ -19,6 +19,27 @@ function trim_orderbook_response(res, depth = 1) {
19
19
  return { ...res, body: trim_orderbook_body(res.body, depth) }
20
20
  }
21
21
 
22
+ function is_finite_number(value) {
23
+ return value != null && Number.isFinite(Number(value))
24
+ }
25
+
26
+ function normalize_market_ts(raw_market_ts) {
27
+ if (!raw_market_ts || typeof raw_market_ts !== 'object') {
28
+ return undefined
29
+ }
30
+ const market_ts = {
31
+ ts_ms_venue_event: is_finite_number(raw_market_ts.ts_ms_venue_event) ? Number(raw_market_ts.ts_ms_venue_event) : undefined,
32
+ ts_ms_venue_send: is_finite_number(raw_market_ts.ts_ms_venue_send) ? Number(raw_market_ts.ts_ms_venue_send) : undefined,
33
+ ts_ms_local_received: is_finite_number(raw_market_ts.ts_ms_local_received) ? Number(raw_market_ts.ts_ms_local_received) : undefined,
34
+ hr_recv: Array.isArray(raw_market_ts.hr_recv) ? raw_market_ts.hr_recv : undefined,
35
+ hr_parsed: Array.isArray(raw_market_ts.hr_parsed) ? raw_market_ts.hr_parsed : undefined,
36
+ }
37
+ if (Object.values(market_ts).some((value) => value != null)) {
38
+ return market_ts
39
+ }
40
+ return undefined
41
+ }
42
+
22
43
  const DEFAULT_HISTORY_MIN_WINDOW_MS = 60 * 1000
23
44
 
24
45
  function sleep(ms) {
@@ -229,13 +250,15 @@ class ExchangeBase {
229
250
  }
230
251
 
231
252
  snapshot_market_ts(pair) {
232
- const ticker_ts = _.get(this, ['ws', 'market', 'tickers', pair, '_ts'])
233
- if (ticker_ts && typeof ticker_ts === 'object') {
234
- return { ...ticker_ts }
253
+ const ticker_market_ts = _.get(this, ['ws', 'market', 'tickers', pair, 'market_ts'])
254
+ const normalized_ticker_market_ts = normalize_market_ts(ticker_market_ts)
255
+ if (normalized_ticker_market_ts) {
256
+ return { ...normalized_ticker_market_ts }
235
257
  }
236
- const ts = _.get(this, ['ws', 'market', 'ts_dict', pair])
237
- if (ts && typeof ts === 'object') {
238
- return { ...ts }
258
+ const market_ts = _.get(this, ['ws', 'market', 'market_ts_by_pair', pair])
259
+ const normalized_market_ts = normalize_market_ts(market_ts)
260
+ if (normalized_market_ts) {
261
+ return { ...normalized_market_ts }
239
262
  }
240
263
  return undefined
241
264
  }
@@ -252,7 +275,7 @@ class ExchangeBase {
252
275
  ...res,
253
276
  body: {
254
277
  ...res.body,
255
- _ts: res.body._ts && typeof res.body._ts === 'object' ? { ...res.body._ts } : market_ts,
278
+ market_ts: normalize_market_ts(res.body.market_ts) || market_ts,
256
279
  },
257
280
  }
258
281
  }
@@ -612,12 +635,12 @@ class ExchangeBase {
612
635
  bbo_ws(pair, cb) {
613
636
  const t = this.ws.market.tickers[pair]
614
637
  if (t && t.bid > 0 && t.ask > 0) {
615
- cb({ success: true, body: { asks: [[t.ask, t.ask_size]], bids: [[t.bid, t.bid_size]], _ts: t._ts || this.snapshot_market_ts(pair) } })
638
+ cb({ success: true, body: { asks: [[t.ask, t.ask_size]], bids: [[t.bid, t.bid_size]], market_ts: normalize_market_ts(t.market_ts) || this.snapshot_market_ts(pair) } })
616
639
  } else {
617
640
  // Fallback: derive from full orderbook
618
641
  this.rate_ws(pair, (res) => {
619
642
  if (res.success && res.body.bids.length > 0 && res.body.asks.length > 0) {
620
- cb({ success: true, body: { asks: [res.body.asks[0]], bids: [res.body.bids[0]], _ts: res.body._ts || this.snapshot_market_ts(pair) } })
643
+ cb({ success: true, body: { asks: [res.body.asks[0]], bids: [res.body.bids[0]], market_ts: normalize_market_ts(res.body.market_ts) || this.snapshot_market_ts(pair) } })
621
644
  } else {
622
645
  cb(res)
623
646
  }
@@ -23,9 +23,16 @@ module.exports = class ExchangeWs {
23
23
  pair_list: [],
24
24
  asks_dict: {},
25
25
  bids_dict: {},
26
- ts_dict: {},
26
+ market_ts_by_pair: {},
27
27
  market_history_dict: {},
28
28
  tickers: {},
29
+ build_ts: (market_ts = {}) => ({
30
+ ts_ms_local_received: market_ts.ts_ms_local_received != null ? market_ts.ts_ms_local_received : Date.now(),
31
+ ...(market_ts.ts_ms_venue_event != null ? { ts_ms_venue_event: market_ts.ts_ms_venue_event } : {}),
32
+ ...(market_ts.ts_ms_venue_send != null ? { ts_ms_venue_send: market_ts.ts_ms_venue_send } : {}),
33
+ ...(Array.isArray(market_ts.hr_recv) ? { hr_recv: market_ts.hr_recv } : {}),
34
+ ...(Array.isArray(market_ts.hr_parsed) ? { hr_parsed: market_ts.hr_parsed } : {}),
35
+ }),
29
36
  start_base: () => {
30
37
  console.log('start_base empty')
31
38
  },
@@ -37,7 +44,7 @@ module.exports = class ExchangeWs {
37
44
  this.market.pair_list = []
38
45
  this.market.asks_dict = {}
39
46
  this.market.bids_dict = {}
40
- this.market.ts_dict = {}
47
+ this.market.market_ts_by_pair = {}
41
48
  this.market.market_history_dict = {}
42
49
  this.market.tickers = {}
43
50
  this.market.last_ping = undefined
@@ -142,15 +149,15 @@ module.exports = class ExchangeWs {
142
149
  if (this.market.status !== 'opened') return
143
150
  const now = Date.now()
144
151
  const stale_time = this.market.market_stale_time
145
- // Check ts_dict (depth-specific staleness, for exchanges with server-initiated pings)
146
- const has_ts = Object.keys(this.market.ts_dict).length > 0
152
+ // Check market_ts_by_pair (depth-specific staleness, for exchanges with server-initiated pings)
153
+ const has_ts = Object.keys(this.market.market_ts_by_pair).length > 0
147
154
  if (this.market.pair_stale_check_enabled && has_ts) {
148
155
  const stale_pair = this.market.pair_list.find((pair) => {
149
- const ts = _.get(this.market.ts_dict, [pair, 'received_ts'])
156
+ const ts = _.get(this.market.market_ts_by_pair, [pair, 'ts_ms_local_received'])
150
157
  return ts && now - ts > stale_time
151
158
  })
152
159
  if (stale_pair) {
153
- const ts = _.get(this.market.ts_dict, [stale_pair, 'received_ts'])
160
+ const ts = _.get(this.market.market_ts_by_pair, [stale_pair, 'ts_ms_local_received'])
154
161
  console.error('market websocket stale depth data, reconnecting: %s %s (%s)', this.name, stale_pair, ts ? Math.round((now - ts) / 1000) + 's' : 'no data')
155
162
  this.market.end()
156
163
  return
@@ -160,12 +167,12 @@ module.exports = class ExchangeWs {
160
167
  // (e.g. Coinbase WS buffering — connection alive, heartbeats flowing, but data is minutes old)
161
168
  if (has_ts) {
162
169
  const stale_event_pair = this.market.pair_list.find((pair) => {
163
- const ts = _.get(this.market.ts_dict, [pair])
164
- return ts && ts.event_ts && ts.received_ts && ts.received_ts - ts.event_ts > stale_time
170
+ const ts = _.get(this.market.market_ts_by_pair, [pair])
171
+ return ts && ts.ts_ms_venue_event && ts.ts_ms_local_received && ts.ts_ms_local_received - ts.ts_ms_venue_event > stale_time
165
172
  })
166
173
  if (stale_event_pair) {
167
- const ts = _.get(this.market.ts_dict, [stale_event_pair])
168
- console.error('market websocket stale event data, reconnecting: %s %s (ws_delay: %ds)', this.name, stale_event_pair, Math.round((ts.received_ts - ts.event_ts) / 1000))
174
+ const ts = _.get(this.market.market_ts_by_pair, [stale_event_pair])
175
+ console.error('market websocket stale event data, reconnecting: %s %s (ws_delay: %ds)', this.name, stale_event_pair, Math.round((ts.ts_ms_local_received - ts.ts_ms_venue_event) / 1000))
169
176
  this.market.end()
170
177
  return
171
178
  }
@@ -219,7 +226,8 @@ module.exports = class ExchangeWs {
219
226
  let message_handler = (...args) => {
220
227
  if (boundSocket !== this.market_socket) return
221
228
  this.market.last_message_time = Date.now()
222
- listener(...args)
229
+ const hr_recv = process.hrtime()
230
+ listener(args[0], hr_recv, ...args.slice(1))
223
231
  this.market_observable.notify()
224
232
  }
225
233
  if (this.market_socket.serviceHandlers) {
@@ -159,8 +159,9 @@ module.exports = class Gate extends ExchangeBase {
159
159
  }),
160
160
  )
161
161
  const last_update_dict = {}
162
- this.ws.market.on_message((data) => {
162
+ this.ws.market.on_message((data, hr_recv) => {
163
163
  data = JSON.parse(data)
164
+ const hr_parsed = process.hrtime()
164
165
  if (data && data.channel === 'spot.pong') {
165
166
  this.ws.market.last_pong = new Date()
166
167
  } else if (data && data.channel === 'spot.order_book' && data.result && data.result.s && data.result.bids && data.result.asks) {
@@ -171,10 +172,7 @@ module.exports = class Gate extends ExchangeBase {
171
172
  this.ws.market.pair_status[pair] = 'opened'
172
173
  this.ws.market.asks_dict[pair] = asks.map((ask) => [parseFloat(ask[0]), parseFloat(ask[1])])
173
174
  this.ws.market.bids_dict[pair] = bids.map((bid) => [parseFloat(bid[0]), parseFloat(bid[1])])
174
- this.ws.market.ts_dict[pair] = {
175
- received_ts: Date.now(),
176
- event_ts: parseInt(t),
177
- }
175
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: parseInt(t), hr_recv, hr_parsed })
178
176
  last_update_dict[pair] = lastUpdateId
179
177
  }
180
178
  } else if (data && data.channel === 'spot.book_ticker' && data.result && data.result.s) {
@@ -189,7 +187,7 @@ module.exports = class Gate extends ExchangeBase {
189
187
  if (options.depth === false) {
190
188
  this.ws.market.asks_dict[pair] = [[parseFloat(ask_1_price), parseFloat(ask_1_amount)]]
191
189
  this.ws.market.bids_dict[pair] = [[parseFloat(bid_1_price), parseFloat(bid_1_amount)]]
192
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parseInt(data.result.t) || Date.now() }
190
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: parseInt(data.result.t) || Date.now(), hr_recv, hr_parsed })
193
191
  this.ws.market.pair_status[pair] = 'opened'
194
192
  } else {
195
193
  if (!this.ws.market.asks_dict[pair] || !this.ws.market.bids_dict[pair]) {
@@ -108,9 +108,10 @@ module.exports = class Hashkey extends ExchangeBase {
108
108
  }),
109
109
  )
110
110
  })
111
- this.ws.market.on_message((data) => {
111
+ this.ws.market.on_message((data, hr_recv) => {
112
112
  try {
113
113
  data = JSON.parse(data)
114
+ const hr_parsed = process.hrtime()
114
115
  if (data.pong) {
115
116
  this.ws.market.last_pong = new Date()
116
117
  } else {
@@ -120,7 +121,12 @@ module.exports = class Hashkey extends ExchangeBase {
120
121
  const { a, b } = data.data
121
122
  this.ws.market.asks_dict[pair] = a.map((ask) => [parseFloat(ask[0]), parseFloat(ask[1])])
122
123
  this.ws.market.bids_dict[pair] = b.map((bid) => [parseFloat(bid[0]), parseFloat(bid[1])])
123
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parseFloat(data.data.t) || undefined, send_ts: parseFloat(data.sendTime) || undefined }
124
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({
125
+ ts_ms_venue_event: parseFloat(data.data.t) || undefined,
126
+ ts_ms_venue_send: parseFloat(data.sendTime) || undefined,
127
+ hr_recv,
128
+ hr_parsed,
129
+ })
124
130
  const asks = this.ws.market.asks_dict[pair]
125
131
  const bids = this.ws.market.bids_dict[pair]
126
132
  if (asks && asks.length > 0 && bids && bids.length > 0) {
@@ -110,8 +110,9 @@ module.exports = class Hashkeyglobal extends ExchangeBase {
110
110
  })
111
111
  this.ws.market.ping('ping')
112
112
  })
113
- this.ws.market.on_message((data) => {
113
+ this.ws.market.on_message((data, hr_recv) => {
114
114
  data = JSON.parse(data)
115
+ const hr_parsed = process.hrtime()
115
116
  if (data.pong) {
116
117
  this.ws.market.last_pong = new Date()
117
118
  } else {
@@ -121,7 +122,7 @@ module.exports = class Hashkeyglobal extends ExchangeBase {
121
122
  this.ws.market.pair_status[pair] = 'opened'
122
123
  this.ws.market.asks_dict[pair] = book[0].a.map((ask) => [parseFloat(ask[0]), parseFloat(ask[1])])
123
124
  this.ws.market.bids_dict[pair] = book[0].b.map((bid) => [parseFloat(bid[0]), parseFloat(bid[1])])
124
- this.ws.market.ts_dict[pair] = { received_ts: Date.now() }
125
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ hr_recv, hr_parsed })
125
126
  const asks = this.ws.market.asks_dict[pair]
126
127
  const bids = this.ws.market.bids_dict[pair]
127
128
  if (asks && asks.length > 0 && bids && bids.length > 0) {
@@ -142,11 +142,12 @@ module.exports = class Hkbitex extends ExchangeBase {
142
142
  }, 60 * 1000)
143
143
  this.ws.market.ping('ping')
144
144
  })
145
- this.ws.market.on_message((data) => {
145
+ this.ws.market.on_message((data, hr_recv) => {
146
146
  if (data.toString() === 'pong') {
147
147
  this.ws.market.last_pong = new Date()
148
148
  } else {
149
149
  data = JSON.parse(data)
150
+ const hr_parsed = process.hrtime()
150
151
  if (data && data.data && data.data.e === 8) {
151
152
  let book = data.data
152
153
  let pair = post_process_pair(book.s)
@@ -157,7 +158,7 @@ module.exports = class Hkbitex extends ExchangeBase {
157
158
  if (book.d && book.d.b) {
158
159
  this.ws.market.bids_dict[pair] = book.d.b.map((bid) => [parseFloat(bid[0]), parseFloat(bid[1])])
159
160
  }
160
- this.ws.market.ts_dict[pair] = { received_ts: Date.now() }
161
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ hr_recv, hr_parsed })
161
162
  const asks = this.ws.market.asks_dict[pair]
162
163
  const bids = this.ws.market.bids_dict[pair]
163
164
  if (asks && asks.length > 0 && bids && bids.length > 0) {
@@ -120,12 +120,13 @@ module.exports = class Htx extends ExchangeBase {
120
120
  let depth_re = new RegExp('market.(.+).depth.step0'),
121
121
  bbo_re = new RegExp('market.(.+).bbo'),
122
122
  trade_re = new RegExp('market.(.+).trade.detail')
123
- this.ws.market.on_message((data) => {
123
+ this.ws.market.on_message((data, hr_recv) => {
124
124
  data = parse_body(pako.inflate(new Uint8Array(data), { to: 'string' }))
125
+ const hr_parsed = process.hrtime()
125
126
  if (data.ch && depth_re.exec(data.ch)) {
126
127
  let pair = post_process_pair(depth_re.exec(data.ch)[1])
127
128
  this.ws.market.pair_status[pair] = 'opened'
128
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: data.ts }
129
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: data.ts, hr_recv, hr_parsed })
129
130
  this.ws.market.asks_dict[pair] = data.tick.asks
130
131
  this.ws.market.bids_dict[pair] = data.tick.bids
131
132
  } else if (data.ch && bbo_re.exec(data.ch)) {
@@ -140,7 +141,7 @@ module.exports = class Htx extends ExchangeBase {
140
141
  if (options.depth === false) {
141
142
  this.ws.market.asks_dict[pair] = [[parseFloat(ask_1_price), parseFloat(ask_1_amount)]]
142
143
  this.ws.market.bids_dict[pair] = [[parseFloat(bid_1_price), parseFloat(bid_1_amount)]]
143
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: data.ts }
144
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: data.ts, hr_recv, hr_parsed })
144
145
  this.ws.market.pair_status[pair] = 'opened'
145
146
  } else {
146
147
  if (!Array.isArray(this.ws.market.asks_dict[pair]) || !Array.isArray(this.ws.market.bids_dict[pair])) {
@@ -121,8 +121,9 @@ module.exports = class Kucoin extends ExchangeBase {
121
121
  type: 'ping',
122
122
  }),
123
123
  )
124
- this.ws.market.on_message((data) => {
124
+ this.ws.market.on_message((data, hr_recv) => {
125
125
  data = JSON.parse(data)
126
+ const hr_parsed = process.hrtime()
126
127
  let book = data.data
127
128
  if (data.type === 'pong') {
128
129
  this.ws.market.last_pong = new Date()
@@ -132,7 +133,7 @@ module.exports = class Kucoin extends ExchangeBase {
132
133
  this.ws.market.pair_status[pair] = 'opened'
133
134
  this.ws.market.asks_dict[pair] = book.asks.map((ask) => [parseFloat(ask[0]), parseFloat(ask[1])])
134
135
  this.ws.market.bids_dict[pair] = book.bids.map((bid) => [parseFloat(bid[0]), parseFloat(bid[1])])
135
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: book.timestamp }
136
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: book.timestamp, hr_recv, hr_parsed })
136
137
  } else if (data && data.type === 'message' && data.topic && data.subject === 'level1' && book && book.timestamp && book.bids && book.asks) {
137
138
  let val = data.topic.split(':')
138
139
  let pair = post_process_pair(val[1])
@@ -148,7 +149,7 @@ module.exports = class Kucoin extends ExchangeBase {
148
149
  if (options.depth === false) {
149
150
  this.ws.market.asks_dict[pair] = [[parseFloat(ask_1_price), parseFloat(ask_1_amount)]]
150
151
  this.ws.market.bids_dict[pair] = [[parseFloat(bid_1_price), parseFloat(bid_1_amount)]]
151
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: book.timestamp }
152
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: book.timestamp, hr_recv, hr_parsed })
152
153
  this.ws.market.pair_status[pair] = 'opened'
153
154
  } else {
154
155
  if (!Array.isArray(this.ws.market.asks_dict[pair]) || !Array.isArray(this.ws.market.bids_dict[pair])) {
@@ -157,8 +157,9 @@ module.exports = class Lbank extends ExchangeBase {
157
157
  })
158
158
  this.ws.market.ping(JSON.stringify({ action: 'ping', ping: 'keepalive' }))
159
159
  })
160
- this.ws.market.on_message((data) => {
160
+ this.ws.market.on_message((data, hr_recv) => {
161
161
  data = JSON.parse(data)
162
+ const hr_parsed = process.hrtime()
162
163
  if (data.action === 'ping') {
163
164
  this.ws.market.send(
164
165
  JSON.stringify({
@@ -170,8 +171,13 @@ module.exports = class Lbank extends ExchangeBase {
170
171
  this.ws.market.last_pong = new Date()
171
172
  const now = Date.now()
172
173
  pair_list.forEach((pair) => {
173
- if (this.ws.market.ts_dict[pair]) {
174
- this.ws.market.ts_dict[pair].received_ts = now
174
+ if (this.ws.market.market_ts_by_pair[pair]) {
175
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({
176
+ ...this.ws.market.market_ts_by_pair[pair],
177
+ ts_ms_local_received: now,
178
+ hr_recv,
179
+ hr_parsed,
180
+ })
175
181
  }
176
182
  })
177
183
  } else if (data.action === 'pong') {
@@ -181,7 +187,7 @@ module.exports = class Lbank extends ExchangeBase {
181
187
  this.ws.market.pair_status[pair] = 'opened'
182
188
  this.ws.market.asks_dict[pair] = data.depth.asks.map((ask) => [parseFloat(ask[0]), parseFloat(ask[1])])
183
189
  this.ws.market.bids_dict[pair] = data.depth.bids.map((bid) => [parseFloat(bid[0]), parseFloat(bid[1])])
184
- this.ws.market.ts_dict[pair] = { received_ts: Date.now() }
190
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ hr_recv, hr_parsed })
185
191
  const asks = this.ws.market.asks_dict[pair]
186
192
  const bids = this.ws.market.bids_dict[pair]
187
193
  if (asks && asks.length > 0 && bids && bids.length > 0) {
@@ -131,7 +131,7 @@ module.exports = class Mexc extends ExchangeBase {
131
131
  return false
132
132
  }
133
133
  }
134
- this.ws.market.on_message((data) => {
134
+ this.ws.market.on_message((data, hr_recv) => {
135
135
  if (is_json(data)) {
136
136
  try {
137
137
  const json = JSON.parse(data.toString())
@@ -152,6 +152,7 @@ module.exports = class Mexc extends ExchangeBase {
152
152
  try {
153
153
  const message = PushDataV3ApiWrapper.decode(data)
154
154
  const obj = PushDataV3ApiWrapper.toObject(message, { enums: String, longs: String })
155
+ const hr_parsed = process.hrtime()
155
156
  const pair = post_process_pair(obj.symbol)
156
157
  const channel = obj.channel
157
158
  if (channel.startsWith('spot@public.limit.depth.v3.api.pb@')) {
@@ -159,7 +160,7 @@ module.exports = class Mexc extends ExchangeBase {
159
160
  this.ws.market.pair_status[pair] = 'opened'
160
161
  this.ws.market.asks_dict[pair] = asks.map((ask) => [parseFloat(ask.price), parseFloat(ask.quantity)])
161
162
  this.ws.market.bids_dict[pair] = bids.map((bid) => [parseFloat(bid.price), parseFloat(bid.quantity)])
162
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parseInt(obj.sendTime) || undefined }
163
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: parseInt(obj.sendTime) || undefined, hr_recv, hr_parsed })
163
164
  // Derive BBO from depth data
164
165
  if (options.bbo !== false && this.ws.market.asks_dict[pair].length > 0 && this.ws.market.bids_dict[pair].length > 0) {
165
166
  this.ws.market.tickers[pair] = {
@@ -217,13 +217,14 @@ module.exports = class Okx extends ExchangeBase {
217
217
  this.ws.market.ping('ping')
218
218
  start_market_history_socket()
219
219
  })
220
- this.ws.market.on_message((data) => {
220
+ this.ws.market.on_message((data, hr_recv) => {
221
221
  if (data.toString() === 'pong') {
222
222
  this.ws.market.last_pong = new Date()
223
223
  }
224
224
  if (data.toString() !== 'pong') {
225
225
  try {
226
226
  data = JSON.parse(data)
227
+ const hr_parsed = process.hrtime()
227
228
  if (data && data.arg && data.arg.channel === 'books' && data.data && data.data[0]) {
228
229
  const action = data.action
229
230
  const pair = post_process_pair(data.arg.instId, true)
@@ -262,7 +263,7 @@ module.exports = class Okx extends ExchangeBase {
262
263
  } else {
263
264
  }
264
265
  }
265
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parseInt(data.data[0].ts) }
266
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: parseInt(data.data[0].ts), hr_recv, hr_parsed })
266
267
  } else if (data && data.data && data.arg && (data.arg.channel === 'trades' || data.arg.channel === 'trades-all')) {
267
268
  for (let datum of data.data) {
268
269
  let pair = post_process_pair(datum.instId)
@@ -299,7 +300,7 @@ module.exports = class Okx extends ExchangeBase {
299
300
  if (options.depth === false) {
300
301
  this.ws.market.asks_dict[pair] = [[askPrice, askSize]]
301
302
  this.ws.market.bids_dict[pair] = [[bidPrice, bidSize]]
302
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parseInt(bbo.ts) }
303
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: parseInt(bbo.ts), hr_recv, hr_parsed })
303
304
  this.ws.market.pair_status[pair] = 'opened'
304
305
  }
305
306
  this.ws.bbo_observable.notify()
@@ -773,8 +773,9 @@ module.exports = class Phemex extends ExchangeBase {
773
773
  }),
774
774
  )
775
775
  })
776
- this.ws.market.on_message((data) => {
776
+ this.ws.market.on_message((data, hr_recv) => {
777
777
  data = JSON.parse(data)
778
+ const hr_parsed = process.hrtime()
778
779
  if (data && data.book && data.type === 'snapshot') {
779
780
  let pair = post_process_pair(data.symbol)
780
781
  this.ws.market.pair_status[pair] = 'opened'
@@ -784,7 +785,11 @@ module.exports = class Phemex extends ExchangeBase {
784
785
  for (let bid of data.book.bids) {
785
786
  this.ws.market.bids_dict[pair].insert(bid[0], bid[1])
786
787
  }
787
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: data.timestamp ? Math.floor(data.timestamp / 1e6) : undefined }
788
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({
789
+ ts_ms_venue_event: data.timestamp ? Math.floor(data.timestamp / 1e6) : undefined,
790
+ hr_recv,
791
+ hr_parsed,
792
+ })
788
793
  {
789
794
  const _asks = this.ws.market.asks_dict[pair].entries()
790
795
  const _bids = this.ws.market.bids_dict[pair].entries()
@@ -812,7 +817,11 @@ module.exports = class Phemex extends ExchangeBase {
812
817
  this.ws.market.bids_dict[pair].insert(parseFloat(bid[0]), parseFloat(bid[1]))
813
818
  }
814
819
  }
815
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: data.timestamp ? Math.floor(data.timestamp / 1e6) : undefined }
820
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({
821
+ ts_ms_venue_event: data.timestamp ? Math.floor(data.timestamp / 1e6) : undefined,
822
+ hr_recv,
823
+ hr_parsed,
824
+ })
816
825
  {
817
826
  const _asks = this.ws.market.asks_dict[pair].entries()
818
827
  const _bids = this.ws.market.bids_dict[pair].entries()
@@ -97,17 +97,18 @@ module.exports = class Upbit extends ExchangeBase {
97
97
  let message = JSON.stringify([JSON.stringify(sub_channels).toString()])
98
98
  this.ws.market.send(message)
99
99
  })
100
- this.ws.market.on_message((data) => {
100
+ this.ws.market.on_message((data, hr_recv) => {
101
101
  if (data.startsWith('a')) {
102
102
  data = _.trim(data, 'a')
103
103
  data = JSON.parse(JSON.parse(data)[0])
104
104
  }
105
+ const hr_parsed = process.hrtime()
105
106
  if (data.type === 'crixOrderbook') {
106
107
  let pair = post_process_pair(_.split(data.code, '.')[2])
107
108
  this.ws.market.pair_status[pair] = 'opened'
108
109
  this.ws.market.asks_dict[pair] = data.orderbookUnits.map((o) => [parseFloat(o.askPrice), parseFloat(o.askSize)])
109
110
  this.ws.market.bids_dict[pair] = data.orderbookUnits.map((o) => [parseFloat(o.bidPrice), parseFloat(o.bidSize)])
110
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: data.timestamp }
111
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ ts_ms_venue_event: data.timestamp, hr_recv, hr_parsed })
111
112
  const asks = this.ws.market.asks_dict[pair]
112
113
  const bids = this.ws.market.bids_dict[pair]
113
114
  if (asks && asks.length > 0 && bids && bids.length > 0) {
@@ -122,12 +122,14 @@ module.exports = class Xt extends ExchangeBase {
122
122
  })
123
123
  this.ws.market.ping('ping')
124
124
  })
125
- this.ws.market.on_message((data) => {
125
+ this.ws.market.on_message((data, hr_recv) => {
126
126
  if (data.toString() === 'pong') {
127
127
  this.ws.market.last_pong = new Date()
128
128
  } else {
129
+ let hr_parsed
129
130
  try {
130
131
  data = JSON.parse(data)
132
+ hr_parsed = process.hrtime()
131
133
  } catch (err) {
132
134
  console.error('xt websocket', data.toString(), err)
133
135
  }
@@ -137,7 +139,7 @@ module.exports = class Xt extends ExchangeBase {
137
139
  this.ws.market.pair_status[pair] = 'opened'
138
140
  this.ws.market.asks_dict[pair] = data.data.a.map((ask) => [parseFloat(ask[0]), parseFloat(ask[1])])
139
141
  this.ws.market.bids_dict[pair] = data.data.b.map((bid) => [parseFloat(bid[0]), parseFloat(bid[1])])
140
- this.ws.market.ts_dict[pair] = { received_ts: Date.now() }
142
+ this.ws.market.market_ts_by_pair[pair] = this.ws.market.build_ts({ hr_recv, hr_parsed })
141
143
  const asks = this.ws.market.asks_dict[pair]
142
144
  const bids = this.ws.market.bids_dict[pair]
143
145
  if (asks && asks.length > 0 && bids && bids.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icgio/icg-exchanges",
3
- "version": "1.40.45",
3
+ "version": "1.40.46",
4
4
  "description": "icgio exchanges package",
5
5
  "main": "./exchanges.js",
6
6
  "scripts": {