@icgio/icg-exchanges 1.40.41 → 1.40.43

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 (45) hide show
  1. package/API_DOCS.md +6 -6
  2. package/lib/exchanges/ascendex.js +7 -3
  3. package/lib/exchanges/biconomy.js +65 -2
  4. package/lib/exchanges/binance.js +65 -3
  5. package/lib/exchanges/bingx.js +73 -2
  6. package/lib/exchanges/bitfinex.js +76 -0
  7. package/lib/exchanges/bitget.js +86 -3
  8. package/lib/exchanges/bithumb.js +3 -0
  9. package/lib/exchanges/bitkub.js +3 -0
  10. package/lib/exchanges/bitmart.js +3 -2
  11. package/lib/exchanges/bitmex.js +71 -1
  12. package/lib/exchanges/bitrue.js +59 -2
  13. package/lib/exchanges/bitstamp.js +71 -0
  14. package/lib/exchanges/blofin.js +76 -3
  15. package/lib/exchanges/btse.js +6 -2
  16. package/lib/exchanges/bybit.js +3 -2
  17. package/lib/exchanges/coinbase-fix.js +0 -1
  18. package/lib/exchanges/coinbase.js +113 -11
  19. package/lib/exchanges/coinstore.js +6 -2
  20. package/lib/exchanges/cryptocom.js +69 -2
  21. package/lib/exchanges/deepcoin.js +64 -0
  22. package/lib/exchanges/digifinex.js +62 -0
  23. package/lib/exchanges/exchange-base.js +167 -1
  24. package/lib/exchanges/exchange-fix.js +0 -2
  25. package/lib/exchanges/exchange-ws.js +2 -1
  26. package/lib/exchanges/fastex.js +3 -0
  27. package/lib/exchanges/gate.js +4 -2
  28. package/lib/exchanges/gemini.js +70 -0
  29. package/lib/exchanges/hashkey.js +70 -2
  30. package/lib/exchanges/hashkeyglobal.js +70 -2
  31. package/lib/exchanges/hitbtc.js +2 -1
  32. package/lib/exchanges/hkbitex.js +6 -2
  33. package/lib/exchanges/htx.js +78 -3
  34. package/lib/exchanges/indodax.js +3 -0
  35. package/lib/exchanges/kraken.js +63 -0
  36. package/lib/exchanges/kucoin.js +4 -3
  37. package/lib/exchanges/lbank.js +89 -53
  38. package/lib/exchanges/mexc.js +61 -2
  39. package/lib/exchanges/okx.js +4 -3
  40. package/lib/exchanges/phemex.js +4 -3
  41. package/lib/exchanges/swft.js +12 -9
  42. package/lib/exchanges/upbit.js +60 -2
  43. package/lib/exchanges/weex.js +3 -0
  44. package/lib/exchanges/xt.js +64 -2
  45. package/package.json +2 -2
@@ -141,8 +141,9 @@ module.exports = class Bitmex extends ExchangeBase {
141
141
  this.ws.market.bids_dict[pair] = utils.ordered_dict(false)
142
142
  }
143
143
  })
144
- this.ws.market.on_message((data) => {
144
+ this.ws.market.on_message((data, t_recv) => {
145
145
  data = JSON.parse(data)
146
+ const t_parsed = process.hrtime()
146
147
  if (data.table === 'orderBookL2') {
147
148
  if (data.action === 'partial') {
148
149
  if (!data.data[0]) {
@@ -685,6 +686,75 @@ module.exports = class Bitmex extends ExchangeBase {
685
686
  }
686
687
  this.private_limiter.process(get_trades_base, pair, timeframe_in_ms, cb)
687
688
  }
689
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
690
+ let [api_key, secret_key] = this.api_secret_key
691
+ let { fetch_split_windows, filter_history_in_range, sort_history_rows } = ExchangeBase.history_utils
692
+ let verb = 'GET'
693
+ let endpoint = '/api/v1/execution/tradeHistory'
694
+ let fetch_window = (window_start_ms, window_stop_ms) =>
695
+ new Promise((resolve, reject) => {
696
+ let params = {
697
+ count: 500,
698
+ symbol: pre_process_pair(pair),
699
+ startTime: new Date(window_start_ms).toISOString(),
700
+ endTime: new Date(window_stop_ms - 1).toISOString(),
701
+ }
702
+ let query = qs.stringify(params)
703
+ let nonce = Date.now() + 60 * 1000
704
+ let signature = get_signature_bitmex(verb, endpoint + '?' + query, nonce, null, secret_key)
705
+ let options = get_options(api_key, nonce, signature)
706
+ needle.get(base_url + endpoint + '?' + query, options, (err, res, body) => {
707
+ if (Array.isArray(body)) {
708
+ resolve(
709
+ body
710
+ .filter((trade) => trade.execType === 'Trade')
711
+ .map((trade) => {
712
+ let price = parseFloat(trade.lastPx || trade.avgPx || trade.price)
713
+ let amount = Math.abs(parseFloat(trade.homeNotional))
714
+ if (!Number.isFinite(amount)) {
715
+ let quantity = parseFloat(trade.lastQty || trade.cumQty || trade.foreignNotional)
716
+ amount = Number.isFinite(quantity) && Number.isFinite(price) && price !== 0 ? Math.abs(quantity) / price : 0
717
+ }
718
+ let total = Math.abs(parseFloat(trade.foreignNotional))
719
+ if (!Number.isFinite(total)) total = Math.abs(parseFloat(trade.lastQty || trade.cumQty || 0))
720
+ let mapped_trade = {
721
+ order_id: (trade.orderID || '').toString(),
722
+ trade_id: (trade.execID || trade.trdMatchID || '').toString(),
723
+ pair,
724
+ type: (trade.side || '').toLowerCase(),
725
+ price,
726
+ amount,
727
+ size: total,
728
+ total,
729
+ close_time: new Date(trade.transactTime || trade.timestamp),
730
+ }
731
+ if (trade.lastLiquidityInd === 'AddedLiquidity') mapped_trade.role = 'maker'
732
+ if (trade.lastLiquidityInd === 'RemovedLiquidity') mapped_trade.role = 'taker'
733
+ return mapped_trade
734
+ })
735
+ )
736
+ } else if (body && body.error && body.error.message === 'The system is currently overloaded. Please try again later.') {
737
+ reject({ success: false, error: error_codes.system_overloaded })
738
+ } else if (body) {
739
+ reject({ success: false, error: error_codes.other, body })
740
+ } else {
741
+ reject({ success: false, error: error_codes.timeout })
742
+ }
743
+ })
744
+ })
745
+ ;(async () => {
746
+ let trades = await fetch_split_windows({
747
+ start_ms: start_time_in_ms,
748
+ stop_ms: end_time_in_ms,
749
+ initial_window_ms: 30 * 24 * 60 * 60 * 1000,
750
+ page_limit: 500,
751
+ fetch_window,
752
+ delay_ms: 250,
753
+ })
754
+ trades = sort_history_rows(filter_history_in_range(trades, start_time_in_ms, end_time_in_ms))
755
+ cb({ success: true, body: trades })
756
+ })().catch((error) => cb(error))
757
+ }
688
758
  static get_precision(cb) {
689
759
  let price_precision = {},
690
760
  amount_precision = {}
@@ -8,6 +8,7 @@ const zlib = require('zlib')
8
8
  const ExchangeBase = require('./exchange-base.js')
9
9
  const ExchangeWs = require('./exchange-ws.js')
10
10
  const JSONbig = require('json-bigint')
11
+ const { fetch_split_windows, sort_history_rows } = ExchangeBase.history_utils
11
12
 
12
13
  const { error_codes } = require('../error_codes.json')
13
14
 
@@ -121,20 +122,21 @@ module.exports = class Bitrue extends ExchangeBase {
121
122
  }),
122
123
  )
123
124
  })
124
- this.ws.market.on_message((data) => {
125
+ this.ws.market.on_message((data, t_recv) => {
125
126
  zlib.gunzip(data, (err, decompressedData) => {
126
127
  if (err) {
127
128
  console.error('Error decompressing data:', err)
128
129
  } else {
129
130
  let data = decompressedData.toString('utf8')
130
131
  data = JSON.parse(data)
132
+ const t_parsed = process.hrtime()
131
133
  if (data && _.includes(data.channel, 'depth_step0') && data.tick) {
132
134
  let parts = data.channel.split('_')
133
135
  let pair = post_process_pair(parts[1])
134
136
  this.ws.market.pair_status[pair] = 'opened'
135
137
  this.ws.market.asks_dict[pair] = data.tick.asks.map((ask) => [parseFloat(ask[0]), parseFloat(ask[1])])
136
138
  this.ws.market.bids_dict[pair] = data.tick.buys.map((bid) => [parseFloat(bid[0]), parseFloat(bid[1])])
137
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: data.ts }
139
+ this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: data.ts, t_recv, t_parsed }
138
140
  const asks = this.ws.market.asks_dict[pair]
139
141
  const bids = this.ws.market.bids_dict[pair]
140
142
  if (asks && asks.length > 0 && bids && bids.length > 0) {
@@ -512,6 +514,61 @@ module.exports = class Bitrue extends ExchangeBase {
512
514
  }
513
515
  this.private_limiter.process(get_trades_base, pair, timeframe_in_ms, cb)
514
516
  }
517
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
518
+ const get_history_trades_base = (pair, start_time_in_ms, end_time_in_ms, cb) => {
519
+ let [api_key, secret_key] = this.api_secret_key
520
+ const options = get_options(api_key, false)
521
+ ;(async () => {
522
+ const rows = await fetch_split_windows({
523
+ start_ms: start_time_in_ms,
524
+ stop_ms: end_time_in_ms,
525
+ initial_window_ms: 60 * 60 * 1000,
526
+ page_limit: 1000,
527
+ fetch_window: async (window_start_ms, window_stop_ms) =>
528
+ await new Promise((resolve, reject) => {
529
+ let params = {
530
+ symbol: pre_process_pair(pair),
531
+ startTime: window_start_ms,
532
+ endTime: window_stop_ms - 1,
533
+ limit: 1000,
534
+ timestamp: parseInt(Date.now()),
535
+ }
536
+ let query = qs.stringify(params)
537
+ let sign = get_signature_bitrue(secret_key, query)
538
+ let url = 'https://openapi.bitrue.com/api/v2/myTrades?' + query + `&signature=${sign}`
539
+ needle.get(url, options, (err, res, body) => {
540
+ let parsed_body = typeof body === 'string' ? JSONbig.parse(body) : body
541
+ if (parsed_body && Array.isArray(parsed_body)) {
542
+ resolve(
543
+ parsed_body.map((trade) => ({
544
+ order_id: trade.orderId != null ? trade.orderId.toString() : '',
545
+ pair,
546
+ type: trade.isBuyer ? 'buy' : 'sell',
547
+ price: parseFloat(trade.price),
548
+ amount: parseFloat(trade.qty),
549
+ total: parseFloat(trade.price) * parseFloat(trade.qty),
550
+ close_time: new Date(trade.time),
551
+ fees: {
552
+ [post_process_cur(trade.commissionAsset)]: parseFloat(trade.commission),
553
+ },
554
+ role: trade.isMaker ? 'maker' : 'taker',
555
+ })),
556
+ )
557
+ } else if (parsed_body) {
558
+ reject({ error: error_codes.other, body: parsed_body })
559
+ } else {
560
+ reject({ error: error_codes.timeout })
561
+ }
562
+ })
563
+ }),
564
+ })
565
+ cb({ success: true, body: sort_history_rows(rows) })
566
+ })().catch((error) => {
567
+ cb({ success: false, error: error?.error || error_codes.other, body: error?.body })
568
+ })
569
+ }
570
+ this.private_limiter.process(get_history_trades_base, pair, start_time_in_ms, end_time_in_ms, cb)
571
+ }
515
572
  get_deposits(cur, timeframe_in_ms, cb) {
516
573
  let get_deposits_base = (timeframe_in_ms, cb) => {
517
574
  let [api_key, secret_key] = this.api_secret_key
@@ -370,6 +370,77 @@ module.exports = class Bitstamp extends ExchangeBase {
370
370
  }
371
371
  this.private_limiter.process(get_trades_base, pair, timeframe_in_ms, cb)
372
372
  }
373
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
374
+ let [api_key, secret_key] = this.api_secret_key
375
+ let { filter_history_in_range, sort_history_rows, sleep } = ExchangeBase.history_utils
376
+ let [base_cur, quote_cur] = utils.parse_pair(pair)
377
+ let base_key = base_cur.toLowerCase()
378
+ let quote_key = quote_cur.toLowerCase()
379
+ let price_key = base_key + '_' + quote_key
380
+ let parse_trade_time = (datetime) => {
381
+ let iso = datetime.replace(' ', 'T').replace(/(\.\d{3})\d+$/, '$1')
382
+ return new Date(iso + 'Z')
383
+ }
384
+ let fetch_page = (offset) =>
385
+ new Promise((resolve, reject) => {
386
+ let url = 'https://www.bitstamp.net/api/v2/user_transactions/' + pre_process_pair(pair) + '/'
387
+ let params = {
388
+ key: api_key,
389
+ nonce: Date.now(),
390
+ signature: get_signature_bitstamp(api_key, secret_key, this.account_id),
391
+ limit: 1000,
392
+ offset,
393
+ sort: 'desc',
394
+ }
395
+ needle.post(url, params, (err, res, body) => {
396
+ if (Array.isArray(body)) {
397
+ resolve(body)
398
+ } else if (body) {
399
+ reject({ success: false, error: error_codes.other, body })
400
+ } else {
401
+ reject({ success: false, error: error_codes.timeout })
402
+ }
403
+ })
404
+ })
405
+ ;(async () => {
406
+ let trades = []
407
+ let offset = 0
408
+ while (offset <= 200000) {
409
+ let rows = await fetch_page(offset)
410
+ if (rows.length === 0) break
411
+ trades.push(
412
+ ...rows
413
+ .filter((trade) => String(trade.type) === '2')
414
+ .map((trade) => {
415
+ let close_time = parse_trade_time(trade.datetime)
416
+ let price = parseFloat(trade[price_key])
417
+ let amount = Math.abs(parseFloat(trade[base_key]))
418
+ let total = Math.abs(parseFloat(trade[quote_key]))
419
+ return {
420
+ order_id: trade.order_id != null ? trade.order_id.toString() : '',
421
+ trade_id: (trade.id || trade.tid || '').toString(),
422
+ pair,
423
+ type: parseFloat(trade[base_key]) < 0 ? 'sell' : 'buy',
424
+ price,
425
+ amount,
426
+ total: Number.isFinite(total) ? total : price * amount,
427
+ close_time,
428
+ }
429
+ })
430
+ )
431
+ let oldest_seen_ms = Math.min(
432
+ ...rows
433
+ .map((trade) => parse_trade_time(trade.datetime).getTime())
434
+ .filter((time) => Number.isFinite(time))
435
+ )
436
+ if (rows.length < 1000 || (Number.isFinite(oldest_seen_ms) && oldest_seen_ms < start_time_in_ms)) break
437
+ offset += rows.length
438
+ await sleep(250)
439
+ }
440
+ trades = sort_history_rows(filter_history_in_range(trades, start_time_in_ms, end_time_in_ms))
441
+ cb({ success: true, body: trades })
442
+ })().catch((error) => cb(error))
443
+ }
373
444
  static get_min_amount(cb) {
374
445
  let min_quote_cur_amount = {}
375
446
  needle.get('https://www.bitstamp.net/api/v2/trading-pairs-info/', (err, res, body) => {
@@ -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, t_recv) => {
154
154
  try {
155
155
  data = JSON.parse(data)
156
+ const t_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.ts_dict[pair] = { received_ts: Date.now(), t_recv, t_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.ts_dict[pair] = { received_ts: Date.now(), t_recv, t_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()
@@ -583,6 +584,78 @@ module.exports = class Blofin extends ExchangeBase {
583
584
  }
584
585
  this.private_limiter.process(get_trades_base, pair, timeframe_in_ms, cb)
585
586
  }
587
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
588
+ let [api_key, secret_key] = this.api_secret_key
589
+ let { fetch_split_windows, filter_history_in_range, sort_history_rows } = ExchangeBase.history_utils
590
+ let [base_cur, quote_cur] = utils.parse_pair(pair)
591
+ let path = '/api/v1/spot/trade/fills-history'
592
+ let fetch_window = (window_start_ms, window_stop_ms) =>
593
+ new Promise((resolve, reject) => {
594
+ let params = {
595
+ instType: 'SPOT',
596
+ instId: pre_process_pair(pair),
597
+ begin: window_start_ms.toString(),
598
+ end: (window_stop_ms - 1).toString(),
599
+ limit: '100',
600
+ }
601
+ let query = qs.stringify(params)
602
+ let request_path = path + '?' + query
603
+ let timestamp = Date.now()
604
+ let nonce = generate_nonce()
605
+ let signature = get_signature_blofin(secret_key, 'GET', timestamp, request_path, nonce)
606
+ let options = get_options(api_key, signature, timestamp, nonce, this.passphrase)
607
+ let url = 'https://openapi.blofin.com' + request_path
608
+ needle.get(url, options, (err, res, body) => {
609
+ if (body && _.includes(['0', 0, 200], body.code) && Array.isArray(body.data)) {
610
+ resolve(
611
+ body.data.map((trade) => {
612
+ let price = parseFloat(trade.fillPrice || trade.trade_price)
613
+ let amount = parseFloat(trade.fillSize || trade.trade_quantity)
614
+ let total = price * amount
615
+ let fee = Math.abs(parseFloat(trade.fee || trade.trade_fee))
616
+ let fee_currency = trade.feeCurrency || trade.fee_currency
617
+ if (fee_currency === 'base_currency') fee_currency = base_cur
618
+ if (fee_currency === 'quote_currency') fee_currency = quote_cur
619
+ let mapped_trade = {
620
+ order_id: (trade.orderId || trade.order_id || '').toString(),
621
+ trade_id: (trade.tradeId || trade.trade_id || '').toString(),
622
+ pair,
623
+ type: (trade.side || trade.order_side || '').toLowerCase(),
624
+ price,
625
+ amount,
626
+ total,
627
+ close_time: new Date(parseInt(trade.ts || trade.create_time)),
628
+ }
629
+ if (Number.isFinite(fee) && fee_currency) {
630
+ mapped_trade.fees = {
631
+ [post_process_cur(fee_currency)]: fee,
632
+ }
633
+ }
634
+ return mapped_trade
635
+ })
636
+ )
637
+ } else if (body && body.toString().startsWith('<html>')) {
638
+ reject({ success: false, error: error_codes.service_unavailable, body })
639
+ } else if (body) {
640
+ reject({ success: false, error: error_codes.other, body })
641
+ } else {
642
+ reject({ success: false, error: error_codes.timeout })
643
+ }
644
+ })
645
+ })
646
+ ;(async () => {
647
+ let trades = await fetch_split_windows({
648
+ start_ms: start_time_in_ms,
649
+ stop_ms: end_time_in_ms,
650
+ initial_window_ms: 30 * 24 * 60 * 60 * 1000,
651
+ page_limit: 100,
652
+ fetch_window,
653
+ delay_ms: 250,
654
+ })
655
+ trades = sort_history_rows(filter_history_in_range(trades, start_time_in_ms, end_time_in_ms))
656
+ cb({ success: true, body: trades })
657
+ })().catch((error) => cb(error))
658
+ }
586
659
  get_all_deposits(timeframe_in_ms, cb) {
587
660
  let get_all_deposits_base = (timeframe_in_ms, cb) => {
588
661
  let [api_key, secret_key] = this.api_secret_key
@@ -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, t_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 t_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.ts_dict[pair] = { received_ts: Date.now(), event_ts: book.timestamp, t_recv, t_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()
@@ -488,6 +489,9 @@ module.exports = class Btse extends ExchangeBase {
488
489
  }
489
490
  this.private_limiter.process(get_trades_base, pair, timeframe_in_ms, cb)
490
491
  }
492
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
493
+ this._get_history_trades_via_timeframe(pair, start_time_in_ms, end_time_in_ms, cb)
494
+ }
491
495
  get_all_deposits(timeframe_in_ms, cb) {
492
496
  let get_all_deposits_base = (timeframe_in_ms, cb) => {
493
497
  let [api_key, secret_key] = this.api_secret_key
@@ -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, t_recv) => {
94
94
  try {
95
95
  data = JSON.parse(data)
96
+ const t_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.ts_dict[pair] = { received_ts: Date.now(), event_ts: data.ts, t_recv, t_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]
@@ -197,7 +197,6 @@ class CoinbaseFixMd {
197
197
  target_comp_id: 'Coinbase',
198
198
  heartbeat_interval: 30,
199
199
  build_logon_fields: (seq, time) => build_logon_fields(options.api_key, options.secret_key, options.passphrase, seq, time),
200
- on_inbound_message: options.on_inbound_message,
201
200
  on_market_data_batch: this.on_batch ? (batch) => this._handle_md_batch(batch) : undefined,
202
201
  on_market_data_incremental: this.on_batch ? undefined : (fields) => this._handle_md(fields),
203
202
  on_market_data_snapshot: (fields) => this._handle_md(fields),
@@ -7,6 +7,7 @@ const needle = require('needle')
7
7
 
8
8
  const ExchangeWs = require('./exchange-ws.js')
9
9
  const ExchangeBase = require('./exchange-base.js')
10
+ const { filter_history_in_range, history_time_ms, sleep, sort_history_rows } = ExchangeBase.history_utils
10
11
 
11
12
  const { sorted_book } = require('@icgio/icg-utils')
12
13
 
@@ -218,13 +219,14 @@ module.exports = class Coinbase extends ExchangeBase {
218
219
  }))
219
220
  console.log('coinbase ws-direct: subscribed channels:', channels.join(', '))
220
221
  })
221
- this.ws.market.on_message((data) => {
222
+ this.ws.market.on_message((data, t_recv) => {
222
223
  try {
223
224
  data = JSON.parse(data)
224
225
  } catch (err) {
225
226
  console.error('coinbase', 'ws-direct', 'JSON parse error:', err.message)
226
227
  return
227
228
  }
229
+ const t_parsed = process.hrtime()
228
230
  if (data.type === 'snapshot') {
229
231
  const pair = post_process_pair(data.product_id)
230
232
  if (!this.ws.market.asks_dict[pair]) return
@@ -251,7 +253,7 @@ module.exports = class Coinbase extends ExchangeBase {
251
253
  if (price >= lo && price <= hi) this.ws.market.asks_dict[pair].insert(price, qty)
252
254
  }
253
255
  this.ws.market.pair_status[pair] = 'opened'
254
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: Date.now() }
256
+ this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: Date.now(), t_recv, t_parsed }
255
257
  const top_bid = this.ws.market.bids_dict[pair].top()
256
258
  const top_ask = this.ws.market.asks_dict[pair].top()
257
259
  if (top_bid != null && top_ask != null) {
@@ -282,7 +284,7 @@ module.exports = class Coinbase extends ExchangeBase {
282
284
  }
283
285
  }
284
286
  }
285
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parse_event_ts_ms(data.time) || Date.now() }
287
+ this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parse_event_ts_ms(data.time) || Date.now(), t_recv, t_parsed }
286
288
  {
287
289
  const top_bid = this.ws.market.bids_dict[pair].top()
288
290
  const top_ask = this.ws.market.asks_dict[pair].top()
@@ -305,7 +307,7 @@ module.exports = class Coinbase extends ExchangeBase {
305
307
  if (options.depth === false) {
306
308
  this.ws.market.asks_dict[pair] = [[best_ask, best_ask_qty]]
307
309
  this.ws.market.bids_dict[pair] = [[best_bid, best_bid_qty]]
308
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parse_event_ts_ms(data.time) || Date.now() }
310
+ this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parse_event_ts_ms(data.time) || Date.now(), t_recv, t_parsed }
309
311
  this.ws.market.pair_status[pair] = 'opened'
310
312
  } else {
311
313
  const bids = this.ws.market.bids_dict[pair]
@@ -322,7 +324,7 @@ module.exports = class Coinbase extends ExchangeBase {
322
324
  for (const k of prune_bids) bids.del(k)
323
325
  const prune_asks = asks.keys().filter((k) => k > hi)
324
326
  for (const k of prune_asks) asks.del(k)
325
- 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.ts_dict[pair] = { received_ts: Date.now(), event_ts: parse_event_ts_ms(data.time) || Date.now(), t_recv, t_parsed }
326
328
  }
327
329
  if (bbo_changed) this.ws.bbo_observable.notify()
328
330
  } else if (data.type === 'match' || data.type === 'last_match') {
@@ -405,13 +407,14 @@ module.exports = class Coinbase extends ExchangeBase {
405
407
  this.ws.market.send(JSON.stringify({ type: 'subscribe', product_ids, channel: 'market_trades' }))
406
408
  }
407
409
  })
408
- this.ws.market.on_message((data) => {
410
+ this.ws.market.on_message((data, t_recv) => {
409
411
  try {
410
412
  data = JSON.parse(data)
411
413
  } catch (err) {
412
414
  console.error('coinbase', 'websocket', 'JSON parse error:', err.message)
413
415
  return
414
416
  }
417
+ const t_parsed = process.hrtime()
415
418
  const seq = data.sequence_num
416
419
  if (seq !== undefined) {
417
420
  const gap = (this._ws_last_seq !== undefined && seq > this._ws_last_seq + 1) ? seq - this._ws_last_seq - 1 : 0
@@ -485,6 +488,7 @@ module.exports = class Coinbase extends ExchangeBase {
485
488
  this.ws.market.ts_dict[pair] = {
486
489
  received_ts: Date.now(),
487
490
  event_ts: parse_event_ts_ms(data.timestamp) || Date.now(),
491
+ t_recv, t_parsed,
488
492
  }
489
493
  {
490
494
  const top_bid = this.ws.market.bids_dict[pair].top()
@@ -531,7 +535,7 @@ module.exports = class Coinbase extends ExchangeBase {
531
535
  if (options.depth === false) {
532
536
  this.ws.market.asks_dict[pair] = [[best_ask, best_ask_qty]]
533
537
  this.ws.market.bids_dict[pair] = [[best_bid, best_bid_qty]]
534
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parse_event_ts_ms(data.timestamp) || Date.now() }
538
+ this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parse_event_ts_ms(data.timestamp) || Date.now(), t_recv, t_parsed }
535
539
  this.ws.market.pair_status[pair] = 'opened'
536
540
  } else {
537
541
  const bids = this.ws.market.bids_dict[pair]
@@ -549,7 +553,7 @@ module.exports = class Coinbase extends ExchangeBase {
549
553
  for (const k of prune_bids) bids.del(k)
550
554
  const prune_asks = asks.keys().filter((k) => k > hi)
551
555
  for (const k of prune_asks) asks.del(k)
552
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parse_event_ts_ms(data.timestamp) || Date.now() }
556
+ this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parse_event_ts_ms(data.timestamp) || Date.now(), t_recv, t_parsed }
553
557
  }
554
558
  this.ws.bbo_observable.notify()
555
559
  }
@@ -603,9 +607,6 @@ module.exports = class Coinbase extends ExchangeBase {
603
607
  secret_key: Array.isArray(secret_key) ? secret_key[0] : secret_key,
604
608
  passphrase: this.passphrase,
605
609
  symbols,
606
- on_inbound_message: () => {
607
- this.ws.market.last_message_time = Date.now()
608
- },
609
610
  on_status: (status) => {
610
611
  console.log('[coinbase FIX MD] status:', status)
611
612
  if (status === 'ready') {
@@ -1575,6 +1576,107 @@ module.exports = class Coinbase extends ExchangeBase {
1575
1576
  cb({ success: true, body: [] })
1576
1577
  }
1577
1578
  }
1579
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
1580
+ const get_history_trades_base = async (pair, start_time_in_ms, end_time_in_ms, cb) => {
1581
+ try {
1582
+ const results = []
1583
+ if (this.advanced) {
1584
+ let cursor = null
1585
+ while (true) {
1586
+ let path = '/api/v3/brokerage/orders/historical/fills'
1587
+ let params = { product_id: pre_process_pair(pair) }
1588
+ if (cursor) params.cursor = cursor
1589
+ let options = this._get_auth('GET', path, params)
1590
+ let url = this._base_url() + path + '?' + qs.stringify(params)
1591
+ const response = await new Promise((resolve, reject) => {
1592
+ needle.get(url, options, (err, res, body) => {
1593
+ if (body && Array.isArray(body.fills)) {
1594
+ resolve(body)
1595
+ } else if (body) {
1596
+ reject({ error: error_codes.other, body })
1597
+ } else {
1598
+ reject({ error: error_codes.timeout })
1599
+ }
1600
+ })
1601
+ })
1602
+ if (response.fills.length === 0) break
1603
+ let oldest_ms = Infinity
1604
+ for (const fill of response.fills) {
1605
+ const close_time_ms = history_time_ms(fill.trade_time)
1606
+ if (close_time_ms < oldest_ms) oldest_ms = close_time_ms
1607
+ if (close_time_ms < start_time_in_ms || close_time_ms >= end_time_in_ms) continue
1608
+ let quote_cur = (fill.product_id || '').split('-')[1] || ''
1609
+ let trade = {
1610
+ order_id: fill.order_id != null ? fill.order_id.toString() : '',
1611
+ pair,
1612
+ type: String(fill.side || '').toLowerCase(),
1613
+ price: parseFloat(fill.price),
1614
+ amount: parseFloat(fill.size),
1615
+ total: parseFloat(fill.price) * parseFloat(fill.size),
1616
+ close_time: new Date(close_time_ms),
1617
+ fees: quote_cur && fill.commission != null ? { [post_process_cur(quote_cur)]: parseFloat(fill.commission) } : {},
1618
+ role: fill.liquidity_indicator === 'MAKER' ? 'maker' : 'taker',
1619
+ }
1620
+ if (fill.trade_id != null) trade.trade_id = fill.trade_id.toString()
1621
+ else if (fill.entry_id != null) trade.trade_id = fill.entry_id.toString()
1622
+ results.push(trade)
1623
+ }
1624
+ if (!response.has_next || !response.cursor || oldest_ms < start_time_in_ms) break
1625
+ cursor = response.cursor
1626
+ await sleep(50)
1627
+ }
1628
+ } else {
1629
+ let after = null
1630
+ while (true) {
1631
+ let path = '/fills'
1632
+ let params = { product_id: pre_process_pair(pair) }
1633
+ if (after) params.after = after
1634
+ let options = this._get_auth('GET', path, params)
1635
+ let url = this._base_url() + path + '?' + qs.stringify(params)
1636
+ const response = await new Promise((resolve, reject) => {
1637
+ needle.get(url, options, (err, res, body) => {
1638
+ if (body && Array.isArray(body)) {
1639
+ resolve({ res, body })
1640
+ } else if (body) {
1641
+ reject({ error: error_codes.other, body })
1642
+ } else {
1643
+ reject({ error: error_codes.timeout })
1644
+ }
1645
+ })
1646
+ })
1647
+ if (response.body.length === 0) break
1648
+ let oldest_ms = Infinity
1649
+ for (const fill of response.body) {
1650
+ const close_time_ms = history_time_ms(fill.created_at)
1651
+ if (close_time_ms < oldest_ms) oldest_ms = close_time_ms
1652
+ if (close_time_ms < start_time_in_ms || close_time_ms >= end_time_in_ms) continue
1653
+ let trade = {
1654
+ order_id: fill.order_id != null ? fill.order_id.toString() : '',
1655
+ pair,
1656
+ type: String(fill.side || '').toLowerCase(),
1657
+ price: parseFloat(fill.price),
1658
+ amount: parseFloat(fill.size),
1659
+ total: parseFloat(fill.price) * parseFloat(fill.size),
1660
+ close_time: new Date(close_time_ms),
1661
+ fees: fill.funding_currency && fill.fee != null ? { [post_process_cur(fill.funding_currency)]: parseFloat(fill.fee) } : {},
1662
+ role: fill.liquidity === 'M' ? 'maker' : 'taker',
1663
+ }
1664
+ if (fill.trade_id != null) trade.trade_id = fill.trade_id.toString()
1665
+ results.push(trade)
1666
+ }
1667
+ const next_after = response.res?.headers?.['cb-after']
1668
+ if (!next_after || oldest_ms < start_time_in_ms) break
1669
+ after = next_after
1670
+ await sleep(50)
1671
+ }
1672
+ }
1673
+ cb({ success: true, body: sort_history_rows(filter_history_in_range(results, start_time_in_ms, end_time_in_ms)) })
1674
+ } catch (error) {
1675
+ cb({ success: false, error: error?.error || error_codes.other, body: error?.body })
1676
+ }
1677
+ }
1678
+ this.private_limiter.process(get_history_trades_base, pair, start_time_in_ms, end_time_in_ms, cb)
1679
+ }
1578
1680
  _convert(from_cur, to_cur, amount, cb) {
1579
1681
  if (this.advanced) {
1580
1682
  cb({ success: false, error: error_codes.other, body: { message: 'Convert not supported on Advanced API' } })