@icgio/icg-exchanges 1.40.42 → 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 (43) 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.js +113 -8
  18. package/lib/exchanges/coinstore.js +6 -2
  19. package/lib/exchanges/cryptocom.js +69 -2
  20. package/lib/exchanges/deepcoin.js +64 -0
  21. package/lib/exchanges/digifinex.js +62 -0
  22. package/lib/exchanges/exchange-base.js +167 -1
  23. package/lib/exchanges/exchange-ws.js +2 -1
  24. package/lib/exchanges/fastex.js +3 -0
  25. package/lib/exchanges/gate.js +4 -2
  26. package/lib/exchanges/gemini.js +70 -0
  27. package/lib/exchanges/hashkey.js +70 -2
  28. package/lib/exchanges/hashkeyglobal.js +70 -2
  29. package/lib/exchanges/hitbtc.js +2 -1
  30. package/lib/exchanges/hkbitex.js +6 -2
  31. package/lib/exchanges/htx.js +78 -3
  32. package/lib/exchanges/indodax.js +3 -0
  33. package/lib/exchanges/kraken.js +63 -0
  34. package/lib/exchanges/kucoin.js +4 -3
  35. package/lib/exchanges/lbank.js +89 -53
  36. package/lib/exchanges/mexc.js +61 -2
  37. package/lib/exchanges/okx.js +4 -3
  38. package/lib/exchanges/phemex.js +4 -3
  39. package/lib/exchanges/swft.js +12 -9
  40. package/lib/exchanges/upbit.js +60 -2
  41. package/lib/exchanges/weex.js +3 -0
  42. package/lib/exchanges/xt.js +64 -2
  43. package/package.json +2 -2
@@ -44,6 +44,15 @@ function get_options(api_key) {
44
44
  return options
45
45
  }
46
46
 
47
+ function get_trade_fees_hashkey(trade) {
48
+ let fee_coin = trade.commissionAsset || trade.feeCoinId || _.get(trade, 'fee.feeCoinName')
49
+ let fee = parseFloat(trade.commission ?? trade.feeAmount ?? _.get(trade, 'fee.fee'))
50
+ if (!fee_coin || !Number.isFinite(fee)) return {}
51
+ return {
52
+ [post_process_cur(fee_coin)]: Math.abs(fee),
53
+ }
54
+ }
55
+
47
56
  module.exports = class Hashkeyglobal extends ExchangeBase {
48
57
  constructor(api_key, secret_key, settings) {
49
58
  super('hashkey', settings.id, api_key, secret_key)
@@ -101,8 +110,9 @@ module.exports = class Hashkeyglobal extends ExchangeBase {
101
110
  })
102
111
  this.ws.market.ping('ping')
103
112
  })
104
- this.ws.market.on_message((data) => {
113
+ this.ws.market.on_message((data, t_recv) => {
105
114
  data = JSON.parse(data)
115
+ const t_parsed = process.hrtime()
106
116
  if (data.pong) {
107
117
  this.ws.market.last_pong = new Date()
108
118
  } else {
@@ -112,7 +122,7 @@ module.exports = class Hashkeyglobal extends ExchangeBase {
112
122
  this.ws.market.pair_status[pair] = 'opened'
113
123
  this.ws.market.asks_dict[pair] = book[0].a.map((ask) => [parseFloat(ask[0]), parseFloat(ask[1])])
114
124
  this.ws.market.bids_dict[pair] = book[0].b.map((bid) => [parseFloat(bid[0]), parseFloat(bid[1])])
115
- this.ws.market.ts_dict[pair] = { received_ts: Date.now() }
125
+ this.ws.market.ts_dict[pair] = { received_ts: Date.now(), t_recv, t_parsed }
116
126
  const asks = this.ws.market.asks_dict[pair]
117
127
  const bids = this.ws.market.bids_dict[pair]
118
128
  if (asks && asks.length > 0 && bids && bids.length > 0) {
@@ -455,6 +465,64 @@ module.exports = class Hashkeyglobal extends ExchangeBase {
455
465
  }
456
466
  this.private_limiter.process(get_trades_base, pair, timeframe_in_ms, cb)
457
467
  }
468
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
469
+ let [api_key, secret_key] = this.api_secret_key
470
+ let { fetch_split_windows, filter_history_in_range, sort_history_rows } = ExchangeBase.history_utils
471
+ let path = '/api/v1/account/trades'
472
+ let options = get_options(api_key)
473
+ let fetch_window = (window_start_ms, window_stop_ms) =>
474
+ new Promise((resolve, reject) => {
475
+ let params = {
476
+ symbol: pre_process_pair(pair),
477
+ startTime: window_start_ms,
478
+ endTime: window_stop_ms - 1,
479
+ limit: 1000,
480
+ timestamp: Date.now(),
481
+ }
482
+ params.signature = get_signature_hashkey(params, secret_key)
483
+ let url = base_url + path + '?' + qs.stringify(params)
484
+ this.private_limiter.process(needle.get, url, options, (err, res, body) => {
485
+ if (Array.isArray(body)) {
486
+ resolve(
487
+ body.map((trade) => {
488
+ let price = parseFloat(trade.price)
489
+ let amount = parseFloat(trade.qty)
490
+ let mapped_trade = {
491
+ order_id: (trade.orderId || '').toString(),
492
+ trade_id: (trade.id || trade.tradeId || '').toString(),
493
+ pair: post_process_pair(trade.symbol),
494
+ type: trade.isBuyer ? 'buy' : 'sell',
495
+ price,
496
+ amount,
497
+ total: Number.isFinite(parseFloat(trade.quoteQty)) ? parseFloat(trade.quoteQty) : price * amount,
498
+ close_time: new Date(parseInt(trade.time)),
499
+ role: trade.isMaker ? 'maker' : 'taker',
500
+ }
501
+ let fees = get_trade_fees_hashkey(trade)
502
+ if (_.keys(fees).length > 0) mapped_trade.fees = fees
503
+ return mapped_trade
504
+ })
505
+ )
506
+ } else if (body) {
507
+ reject({ success: false, error: error_codes.other, body })
508
+ } else {
509
+ reject({ success: false, error: error_codes.timeout })
510
+ }
511
+ })
512
+ })
513
+ ;(async () => {
514
+ let trades = await fetch_split_windows({
515
+ start_ms: start_time_in_ms,
516
+ stop_ms: end_time_in_ms,
517
+ initial_window_ms: 24 * 60 * 60 * 1000,
518
+ page_limit: 1000,
519
+ fetch_window,
520
+ delay_ms: 250,
521
+ })
522
+ trades = sort_history_rows(filter_history_in_range(trades, start_time_in_ms, end_time_in_ms))
523
+ cb({ success: true, body: trades })
524
+ })().catch((error) => cb(error))
525
+ }
458
526
  get_all_deposits(timeframe_in_ms, cb) {
459
527
  let [api_key, secret_key] = this.api_secret_key
460
528
  let params = {
@@ -80,8 +80,9 @@ module.exports = class Hitbtc extends ExchangeBase {
80
80
  }
81
81
  }
82
82
  })
83
- this.ws.market.on_message((data) => {
83
+ this.ws.market.on_message((data, t_recv) => {
84
84
  data = JSON.parse(data)
85
+ const t_parsed = process.hrtime()
85
86
  if (data.method === 'snapshotOrderbook') {
86
87
  let pair = post_process_pair(data.params.symbol)
87
88
  this.ws.market.pair_status[pair] = 'opened'
@@ -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, t_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 t_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.ts_dict[pair] = { received_ts: Date.now(), t_recv, t_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) {
@@ -556,6 +557,9 @@ module.exports = class Hkbitex extends ExchangeBase {
556
557
  }
557
558
  this.private_limiter.process(get_trades_base, pair, timeframe_in_ms, cb)
558
559
  }
560
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
561
+ this._get_history_trades_via_timeframe(pair, start_time_in_ms, end_time_in_ms, cb)
562
+ }
559
563
  static get_precision(cb) {
560
564
  let price_precision = {},
561
565
  amount_precision = {}
@@ -8,6 +8,7 @@ const pako = require('pako')
8
8
  const ExchangeWs = require('./exchange-ws.js')
9
9
 
10
10
  const ExchangeBase = require('./exchange-base.js')
11
+ const { fetch_split_windows, sort_history_rows, stringify_sorted } = ExchangeBase.history_utils
11
12
 
12
13
  const { error_codes } = require('../error_codes.json')
13
14
  const { utils, rate_limiter: Limiter } = require('@icgio/icg-utils')
@@ -119,12 +120,13 @@ module.exports = class Htx extends ExchangeBase {
119
120
  let depth_re = new RegExp('market.(.+).depth.step0'),
120
121
  bbo_re = new RegExp('market.(.+).bbo'),
121
122
  trade_re = new RegExp('market.(.+).trade.detail')
122
- this.ws.market.on_message((data) => {
123
+ this.ws.market.on_message((data, t_recv) => {
123
124
  data = parse_body(pako.inflate(new Uint8Array(data), { to: 'string' }))
125
+ const t_parsed = process.hrtime()
124
126
  if (data.ch && depth_re.exec(data.ch)) {
125
127
  let pair = post_process_pair(depth_re.exec(data.ch)[1])
126
128
  this.ws.market.pair_status[pair] = 'opened'
127
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: data.ts }
129
+ this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: data.ts, t_recv, t_parsed }
128
130
  this.ws.market.asks_dict[pair] = data.tick.asks
129
131
  this.ws.market.bids_dict[pair] = data.tick.bids
130
132
  } else if (data.ch && bbo_re.exec(data.ch)) {
@@ -139,7 +141,7 @@ module.exports = class Htx extends ExchangeBase {
139
141
  if (options.depth === false) {
140
142
  this.ws.market.asks_dict[pair] = [[parseFloat(ask_1_price), parseFloat(ask_1_amount)]]
141
143
  this.ws.market.bids_dict[pair] = [[parseFloat(bid_1_price), parseFloat(bid_1_amount)]]
142
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: data.ts }
144
+ this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: data.ts, t_recv, t_parsed }
143
145
  this.ws.market.pair_status[pair] = 'opened'
144
146
  } else {
145
147
  if (!Array.isArray(this.ws.market.asks_dict[pair]) || !Array.isArray(this.ws.market.bids_dict[pair])) {
@@ -539,6 +541,79 @@ module.exports = class Htx extends ExchangeBase {
539
541
  }
540
542
  this.private_limiter.process(get_trades_base, pair, timeframe_in_ms, cb)
541
543
  }
544
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
545
+ const get_history_trades_base = (pair, start_time_in_ms, end_time_in_ms, cb) => {
546
+ const account_id = this.account_id
547
+ if (!account_id) {
548
+ cb({ success: false, error: error_codes.other, body: { message: 'htx account_id missing' } })
549
+ return
550
+ }
551
+ const [api_key, secret_key] = this.api_secret_key
552
+ ;(async () => {
553
+ const rows = await fetch_split_windows({
554
+ start_ms: start_time_in_ms,
555
+ stop_ms: end_time_in_ms,
556
+ initial_window_ms: 60 * 60 * 1000,
557
+ page_limit: 1000,
558
+ fetch_window: async (window_start_ms, window_stop_ms) =>
559
+ await new Promise((resolve, reject) => {
560
+ let base_url = 'api.huobi.pro'
561
+ let path = '/v1/order/matchresults'
562
+ let params = {
563
+ AccessKeyId: api_key,
564
+ SignatureMethod: 'HmacSHA256',
565
+ SignatureVersion: 2,
566
+ Timestamp: new Date().toISOString().slice(0, 19),
567
+ 'account-id': account_id,
568
+ 'start-time': window_start_ms,
569
+ 'end-time': window_stop_ms - 1,
570
+ size: 1000,
571
+ symbol: pre_process_pair(pair),
572
+ }
573
+ const signature = crypto.createHmac('sha256', secret_key).update(`GET\n${base_url}\n${path}\n${stringify_sorted(params)}`).digest('base64')
574
+ params.Signature = signature
575
+ let url = 'https://' + base_url + path + '?' + stringify_sorted(params)
576
+ needle.get(url, (err, res, body) => {
577
+ if (body && Array.isArray(body.data)) {
578
+ resolve(
579
+ body.data.map((trade) => {
580
+ let result = {
581
+ order_id: trade['order-id'] != null ? trade['order-id'].toString() : '',
582
+ pair: post_process_pair(trade.symbol),
583
+ type: String(trade.type || '').startsWith('buy') ? 'buy' : 'sell',
584
+ price: parseFloat(trade.price),
585
+ amount: parseFloat(trade['filled-amount']),
586
+ total: parseFloat(trade['filled-amount']) * parseFloat(trade.price),
587
+ close_time: new Date(trade['created-at']),
588
+ fees: {
589
+ [post_process_cur(trade['fee-currency'])]: parseFloat(trade['filled-fees']),
590
+ },
591
+ role: String(trade.role || '').toLowerCase(),
592
+ }
593
+ let trade_id = trade['match-id'] || trade['trade-id'] || trade.id
594
+ if (trade_id != null) result.trade_id = trade_id.toString()
595
+ return result
596
+ }),
597
+ )
598
+ } else if (body && body.status === 'error' && body['err-code'] === 'api-signature-not-valid') {
599
+ reject({ error: error_codes.invalid_key, body })
600
+ } else if (body && body.toString().startsWith('<html>')) {
601
+ reject({ error: error_codes.service_unavailable, body })
602
+ } else if (body) {
603
+ reject({ error: error_codes.other, body })
604
+ } else {
605
+ reject({ error: error_codes.timeout })
606
+ }
607
+ })
608
+ }),
609
+ })
610
+ cb({ success: true, body: sort_history_rows(rows) })
611
+ })().catch((error) => {
612
+ cb({ success: false, error: error?.error || error_codes.other, body: error?.body })
613
+ })
614
+ }
615
+ this.private_limiter.process(get_history_trades_base, pair, start_time_in_ms, end_time_in_ms, cb)
616
+ }
542
617
  // get_all_trades(timeframe_in_ms, cb) {
543
618
  // let get_all_trades_base = (timeframe_in_ms, cb) => {
544
619
  // let base_url = 'api.huobi.pro'
@@ -391,6 +391,9 @@ module.exports = class Indodax extends ExchangeBase {
391
391
  }
392
392
  this.private_limiter.process(get_trades_base, pair, cb)
393
393
  }
394
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
395
+ this._get_history_trades_via_timeframe(pair, start_time_in_ms, end_time_in_ms, cb)
396
+ }
394
397
  static get_min_amount(cb) {
395
398
  let min_quote_cur_amount = {}
396
399
  needle.get('https://indodax.com/api/pairs', (err, res, body) => {
@@ -333,6 +333,69 @@ module.exports = class Kraken extends ExchangeBase {
333
333
  }
334
334
  })
335
335
  }
336
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
337
+ let api_secret_key = this.api_secret_key
338
+ let { filter_history_in_range, sort_history_rows, sleep } = ExchangeBase.history_utils
339
+ let path = '/0/private/TradesHistory'
340
+ let fetch_page = (ofs) =>
341
+ new Promise((resolve, reject) => {
342
+ let params = {
343
+ nonce: Date.now() + ofs,
344
+ start: Math.floor(start_time_in_ms / 1000),
345
+ end: Math.floor((end_time_in_ms - 1) / 1000),
346
+ ofs,
347
+ }
348
+ let options = get_options(path, params, api_secret_key, params.nonce)
349
+ this.private_limiter.process(needle.post, 'https://api.kraken.com' + path, params, options, (err, res, body) => {
350
+ if (body && body.result && body.result.trades) {
351
+ let trades = []
352
+ for (let trade_id in body.result.trades) {
353
+ let trade = body.result.trades[trade_id]
354
+ let pair = post_process_pair(trade.pair)
355
+ let [, quote_cur] = utils.parse_pair(pair)
356
+ let mapped_trade = {
357
+ order_id: trade.ordertxid.toString(),
358
+ trade_id: (trade.trade_id || trade_id || trade.postxid).toString(),
359
+ pair,
360
+ type: trade.type,
361
+ price: parseFloat(trade.price),
362
+ amount: parseFloat(trade.vol),
363
+ total: parseFloat(trade.cost),
364
+ close_time: new Date(parseFloat(trade.time) * 1000),
365
+ }
366
+ if (trade.maker !== undefined) mapped_trade.role = trade.maker ? 'maker' : 'taker'
367
+ if (trade.fee !== undefined) {
368
+ mapped_trade.fees = {
369
+ [quote_cur]: parseFloat(trade.fee),
370
+ }
371
+ }
372
+ trades.push({
373
+ ...mapped_trade,
374
+ })
375
+ }
376
+ resolve({ trades, count: parseInt(body.result.count || trades.length) })
377
+ } else if (body) {
378
+ reject({ success: false, error: error_codes.other, body })
379
+ } else {
380
+ reject({ success: false, error: error_codes.timeout })
381
+ }
382
+ })
383
+ })
384
+ ;(async () => {
385
+ let trades = []
386
+ let offset = 0
387
+ while (true) {
388
+ let page = await fetch_page(offset)
389
+ trades.push(...page.trades)
390
+ offset += page.trades.length
391
+ if (page.trades.length === 0 || offset >= page.count) break
392
+ await sleep(250)
393
+ }
394
+ trades = trades.filter((trade) => trade.pair === pair)
395
+ trades = sort_history_rows(filter_history_in_range(trades, start_time_in_ms, end_time_in_ms))
396
+ cb({ success: true, body: trades })
397
+ })().catch((error) => cb(error))
398
+ }
336
399
  static get_precision(cb) {
337
400
  let price_precision = {},
338
401
  amount_precision = {}
@@ -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, t_recv) => {
125
125
  data = JSON.parse(data)
126
+ const t_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.ts_dict[pair] = { received_ts: Date.now(), event_ts: book.timestamp, t_recv, t_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.ts_dict[pair] = { received_ts: Date.now(), event_ts: book.timestamp, t_recv, t_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])) {
@@ -7,6 +7,7 @@ const needle = require('needle')
7
7
  const ExchangeWs = require('./exchange-ws.js')
8
8
 
9
9
  const ExchangeBase = require('./exchange-base.js')
10
+ const { filter_history_in_range, sleep, sort_history_rows, split_windows } = ExchangeBase.history_utils
10
11
 
11
12
  const { error_codes } = require('../error_codes.json')
12
13
  const { utils, rate_limiter: Limiter } = require('@icgio/icg-utils')
@@ -69,11 +70,29 @@ const uuid38 = () => uuid16() + uuid22()
69
70
  function get_signature_lbank(secret_key, request, timestamp, echostr) {
70
71
  request = _.extend(request, { timestamp, echostr, signature_method: 'HmacSHA256' })
71
72
  request = _.fromPairs(_.sortBy(_.toPairs(request), [0]))
72
- let message = qs.stringify(request)
73
+ let message = qs.stringify(request, { encode: false })
73
74
  message = crypto.createHash('md5').update(message).digest('hex').toUpperCase()
74
75
  return crypto.createHmac('sha256', secret_key).update(message).digest('hex')
75
76
  }
76
77
 
78
+ function format_history_time_utc8(ms) {
79
+ const date = new Date(ms + 8 * 60 * 60 * 1000)
80
+ const pad = (value) => String(value).padStart(2, '0')
81
+ return (
82
+ date.getUTCFullYear() +
83
+ '-' +
84
+ pad(date.getUTCMonth() + 1) +
85
+ '-' +
86
+ pad(date.getUTCDate()) +
87
+ ' ' +
88
+ pad(date.getUTCHours()) +
89
+ ':' +
90
+ pad(date.getUTCMinutes()) +
91
+ ':' +
92
+ pad(date.getUTCSeconds())
93
+ )
94
+ }
95
+
77
96
  function parse_lbank_ws_timestamp(value) {
78
97
  if (value instanceof Date) return value
79
98
  if (typeof value === 'number') return new Date(value)
@@ -138,8 +157,9 @@ module.exports = class Lbank extends ExchangeBase {
138
157
  })
139
158
  this.ws.market.ping(JSON.stringify({ action: 'ping', ping: 'keepalive' }))
140
159
  })
141
- this.ws.market.on_message((data) => {
160
+ this.ws.market.on_message((data, t_recv) => {
142
161
  data = JSON.parse(data)
162
+ const t_parsed = process.hrtime()
143
163
  if (data.action === 'ping') {
144
164
  this.ws.market.send(
145
165
  JSON.stringify({
@@ -162,7 +182,7 @@ module.exports = class Lbank extends ExchangeBase {
162
182
  this.ws.market.pair_status[pair] = 'opened'
163
183
  this.ws.market.asks_dict[pair] = data.depth.asks.map((ask) => [parseFloat(ask[0]), parseFloat(ask[1])])
164
184
  this.ws.market.bids_dict[pair] = data.depth.bids.map((bid) => [parseFloat(bid[0]), parseFloat(bid[1])])
165
- this.ws.market.ts_dict[pair] = { received_ts: Date.now() }
185
+ this.ws.market.ts_dict[pair] = { received_ts: Date.now(), t_recv, t_parsed }
166
186
  const asks = this.ws.market.asks_dict[pair]
167
187
  const bids = this.ws.market.bids_dict[pair]
168
188
  if (asks && asks.length > 0 && bids && bids.length > 0) {
@@ -580,61 +600,77 @@ module.exports = class Lbank extends ExchangeBase {
580
600
  this.private_limiter.process(get_trades_base, pair, timeframe_in_ms, cb)
581
601
  }
582
602
  get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
583
- let [api_key, secret_key] = this.api_secret_key
584
- const PAGE_SIZE = 100
585
- let all_results = []
586
-
587
- const fetch_page = (page_start) => {
588
- let timestamp = new Date().getTime().toString()
589
- let echostr = uuid38()
590
- let params = {
591
- api_key: api_key,
592
- direct: 'next',
593
- endTime: end_time_in_ms.toString(),
594
- size: PAGE_SIZE,
595
- startTime: page_start.toString(),
596
- symbol: pre_process_pair(pair),
597
- }
598
- params.sign = get_signature_lbank(secret_key, params, timestamp, echostr)
599
- let options = get_options(timestamp, echostr)
600
- needle.post('https://api.lbank.info/v2/transaction_history.do', params, options, (err, res, body) => {
601
- body = parse_body(body)
602
- if (body && body.result && body.error_code === 0 && !body.data) {
603
- cb({ success: true, body: all_results })
604
- } else if (body && body.result && Array.isArray(body.data)) {
605
- let result = body.data.map((order) => {
606
- return {
607
- order_id: order.orderUuid.toString(),
608
- pair,
609
- type: order.tradeType,
610
- price: parseFloat(order.dealPrice),
611
- amount: parseFloat(order.dealQuantity),
612
- total: parseFloat(order.dealVolumePrice),
613
- close_time: new Date(order.dealTime),
614
- fees: {
615
- [order.tradeType === 'buy' ? pair_to_base_cur(pair) : pair_to_quote_cur(pair)]: parseFloat(order.tradeFee),
616
- },
617
- }
618
- })
619
- all_results = all_results.concat(result)
620
- // direct=next returns oldest first — advance startTime past the newest to get next page
621
- if (result.length >= PAGE_SIZE) {
622
- const newest_time = Math.max(...result.map(t => t.close_time.getTime()))
623
- if (newest_time > page_start && newest_time < end_time_in_ms) {
624
- fetch_page(newest_time + 1)
625
- return
603
+ const get_history_trades_base = (pair, start_time_in_ms, end_time_in_ms, cb) => {
604
+ let [api_key, secret_key] = this.api_secret_key
605
+ ;(async () => {
606
+ const rows = []
607
+ const max_window_ms = 2 * 24 * 60 * 60 * 1000
608
+ const windows = split_windows(start_time_in_ms, end_time_in_ms, Math.min(max_window_ms, end_time_in_ms - start_time_in_ms))
609
+ for (const [window_start_ms, window_stop_ms] of windows) {
610
+ let last_trade_id = null
611
+ while (true) {
612
+ const raw_items = await new Promise((resolve, reject) => {
613
+ let timestamp = new Date().getTime().toString()
614
+ let echostr = uuid38()
615
+ let params = {
616
+ api_key: api_key,
617
+ endTime: format_history_time_utc8(window_stop_ms),
618
+ limit: '100',
619
+ startTime: format_history_time_utc8(window_start_ms),
620
+ symbol: pre_process_pair(pair),
621
+ }
622
+ if (last_trade_id) params.fromId = last_trade_id
623
+ params.sign = get_signature_lbank(secret_key, params, timestamp, echostr)
624
+ let options = get_options(timestamp, echostr)
625
+ needle.post('https://api.lbank.info/v2/supplement/transaction_history.do', params, options, (err, res, body) => {
626
+ body = parse_body(body)
627
+ if (body && body.result && body.error_code === 0 && !body.data) {
628
+ resolve([])
629
+ } else if (body && body.result && Array.isArray(body.data)) {
630
+ resolve(
631
+ body.data.map((trade) => {
632
+ let result = {
633
+ order_id: trade.orderId != null ? trade.orderId.toString() : '',
634
+ pair,
635
+ type: trade.isBuyer ? 'buy' : 'sell',
636
+ price: parseFloat(trade.price),
637
+ amount: parseFloat(trade.qty),
638
+ total: parseFloat(trade.quoteQty),
639
+ close_time: new Date(trade.time),
640
+ fees: {
641
+ [trade.isBuyer ? pair_to_base_cur(pair) : pair_to_quote_cur(pair)]: parseFloat(trade.commission),
642
+ },
643
+ role: trade.isMaker ? 'maker' : 'taker',
644
+ }
645
+ if (trade.id != null) result.trade_id = trade.id.toString()
646
+ return result
647
+ }),
648
+ )
649
+ } else if (body) {
650
+ reject({ error: error_codes.other, body })
651
+ } else {
652
+ reject({ error: error_codes.timeout })
653
+ }
654
+ })
655
+ })
656
+ if (raw_items.length === 0) break
657
+ let items = raw_items
658
+ if (last_trade_id && items[0] && items[0].trade_id === last_trade_id) {
659
+ items = items.slice(1)
626
660
  }
661
+ if (items.length === 0) break
662
+ rows.push(...items)
663
+ last_trade_id = raw_items[raw_items.length - 1]?.trade_id || null
664
+ if (raw_items.length < 100 || !last_trade_id) break
665
+ await sleep(50)
627
666
  }
628
- cb({ success: true, body: all_results })
629
- } else if (body) {
630
- cb({ success: false, error: error_codes.other, body })
631
- } else {
632
- cb({ success: false, error: error_codes.timeout })
633
667
  }
668
+ cb({ success: true, body: sort_history_rows(filter_history_in_range(rows, start_time_in_ms, end_time_in_ms)) })
669
+ })().catch((error) => {
670
+ cb({ success: false, error: error?.error || error_codes.other, body: error?.body })
634
671
  })
635
672
  }
636
-
637
- this.private_limiter.process(fetch_page, start_time_in_ms)
673
+ this.private_limiter.process(get_history_trades_base, pair, start_time_in_ms, end_time_in_ms, cb)
638
674
  }
639
675
  get_all_deposits(timeframe_in_ms, cb) {
640
676
  const get_all_deposits_base = (timeframe_in_ms, cb) => {
@@ -7,6 +7,7 @@ const needle = require('needle')
7
7
 
8
8
  const ExchangeBase = require('./exchange-base.js')
9
9
  const ExchangeWs = require('./exchange-ws.js')
10
+ const { fetch_split_windows, sort_history_rows } = ExchangeBase.history_utils
10
11
 
11
12
  const { utils, rate_limiter: Limiter } = require('@icgio/icg-utils')
12
13
 
@@ -130,7 +131,7 @@ module.exports = class Mexc extends ExchangeBase {
130
131
  return false
131
132
  }
132
133
  }
133
- this.ws.market.on_message((data) => {
134
+ this.ws.market.on_message((data, t_recv) => {
134
135
  if (is_json(data)) {
135
136
  try {
136
137
  const json = JSON.parse(data.toString())
@@ -151,6 +152,7 @@ module.exports = class Mexc extends ExchangeBase {
151
152
  try {
152
153
  const message = PushDataV3ApiWrapper.decode(data)
153
154
  const obj = PushDataV3ApiWrapper.toObject(message, { enums: String, longs: String })
155
+ const t_parsed = process.hrtime()
154
156
  const pair = post_process_pair(obj.symbol)
155
157
  const channel = obj.channel
156
158
  if (channel.startsWith('spot@public.limit.depth.v3.api.pb@')) {
@@ -158,7 +160,7 @@ module.exports = class Mexc extends ExchangeBase {
158
160
  this.ws.market.pair_status[pair] = 'opened'
159
161
  this.ws.market.asks_dict[pair] = asks.map((ask) => [parseFloat(ask.price), parseFloat(ask.quantity)])
160
162
  this.ws.market.bids_dict[pair] = bids.map((bid) => [parseFloat(bid.price), parseFloat(bid.quantity)])
161
- this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parseInt(obj.sendTime) || undefined }
163
+ this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parseInt(obj.sendTime) || undefined, t_recv, t_parsed }
162
164
  // Derive BBO from depth data
163
165
  if (options.bbo !== false && this.ws.market.asks_dict[pair].length > 0 && this.ws.market.bids_dict[pair].length > 0) {
164
166
  this.ws.market.tickers[pair] = {
@@ -515,6 +517,63 @@ module.exports = class Mexc extends ExchangeBase {
515
517
  }
516
518
  this.private_limiter.process(get_trades_base, pair, timeframe_in_ms, cb)
517
519
  }
520
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
521
+ const get_history_trades_base = (pair, start_time_in_ms, end_time_in_ms, cb) => {
522
+ let [api_key, secret_key] = this.api_secret_key
523
+ const options = get_options(api_key)
524
+ ;(async () => {
525
+ const rows = await fetch_split_windows({
526
+ start_ms: start_time_in_ms,
527
+ stop_ms: end_time_in_ms,
528
+ initial_window_ms: Math.max(end_time_in_ms - start_time_in_ms, 60 * 1000),
529
+ page_limit: 1000,
530
+ fetch_window: async (window_start_ms, window_stop_ms) =>
531
+ await new Promise((resolve, reject) => {
532
+ let params = {
533
+ symbol: pre_process_pair(pair),
534
+ startTime: window_start_ms,
535
+ endTime: window_stop_ms - 1,
536
+ limit: 1000,
537
+ timestamp: parseInt(Date.now()),
538
+ }
539
+ let signature = get_signature_mexc(secret_key, params)
540
+ let url = 'https://api.mexc.com/api/v3/myTrades?' + qs.stringify(params) + '&signature=' + signature
541
+ needle.get(url, options, (err, res, body) => {
542
+ if (body && Array.isArray(body)) {
543
+ resolve(
544
+ body.map((trade) => {
545
+ let result = {
546
+ order_id: trade.orderId != null ? trade.orderId.toString() : '',
547
+ pair,
548
+ type: trade.isBuyer ? 'buy' : 'sell',
549
+ price: parseFloat(trade.price),
550
+ amount: parseFloat(trade.qty),
551
+ total: parseFloat(trade.quoteQty),
552
+ close_time: new Date(parseInt(trade.time)),
553
+ fees: {
554
+ [post_process_cur(trade.commissionAsset)]: parseFloat(trade.commission),
555
+ },
556
+ role: trade.isMaker ? 'maker' : 'taker',
557
+ }
558
+ if (trade.id != null) result.trade_id = trade.id.toString()
559
+ return result
560
+ }),
561
+ )
562
+ } else if (body) {
563
+ reject({ error: error_codes.other, body })
564
+ } else {
565
+ reject({ error: error_codes.timeout })
566
+ }
567
+ })
568
+ }),
569
+ })
570
+ cb({ success: true, body: sort_history_rows(rows) })
571
+ })().catch((error) => {
572
+ cb({ success: false, error: error?.error || error_codes.other, body: error?.body })
573
+ })
574
+ }
575
+ this.private_limiter.process(get_history_trades_base, pair, start_time_in_ms, end_time_in_ms, cb)
576
+ }
518
577
  get_all_deposits(timeframe_in_ms, cb) {
519
578
  let get_all_deposits_base = (timeframe_in_ms, cb) => {
520
579
  let [api_key, secret_key] = this.api_secret_key