@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
@@ -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, t_recv) => {
139
139
  data = JSON.parse(data)
140
+ const t_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.ts_dict[pair] = { received_ts: Date.now(), t_recv, t_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) {
@@ -476,6 +477,9 @@ module.exports = class Coinstore extends ExchangeBase {
476
477
  }
477
478
  this.private_limiter.process(get_trades_base, pair, timeframe_in_ms, cb)
478
479
  }
480
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
481
+ this._get_history_trades_via_timeframe(pair, start_time_in_ms, end_time_in_ms, cb)
482
+ }
479
483
  static get_precision(cb) {
480
484
  const price_precision = {}
481
485
  const amount_precision = {}
@@ -109,7 +109,7 @@ 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, t_recv) => {
113
113
  let parsed
114
114
  try {
115
115
  parsed = JSON.parse(data)
@@ -125,12 +125,13 @@ module.exports = class Cryptocom extends ExchangeBase {
125
125
  )
126
126
  return
127
127
  }
128
+ const t_parsed = process.hrtime()
128
129
  let result = parsed.result
129
130
  if (result) {
130
131
  let pair = post_process_pair(result.instrument_name, true)
131
132
  if (result.channel === 'book') {
132
133
  this.ws.market.pair_status[pair] = 'opened'
133
- this.ws.market.ts_dict[pair] = { received_ts: Date.now() }
134
+ this.ws.market.ts_dict[pair] = { received_ts: Date.now(), t_recv, t_parsed }
134
135
  let { asks, bids } = result.data[0]
135
136
  this.ws.market.asks_dict[pair] = asks.map((e) => [parseFloat(e[0]), parseFloat(e[1])])
136
137
  this.ws.market.bids_dict[pair] = bids.map((e) => [parseFloat(e[0]), parseFloat(e[1])])
@@ -493,6 +494,72 @@ module.exports = class Cryptocom extends ExchangeBase {
493
494
  }
494
495
  })
495
496
  }
497
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
498
+ let [api_key, secret_key] = this.api_secret_key
499
+ let { fetch_split_windows, filter_history_in_range, sort_history_rows } = ExchangeBase.history_utils
500
+ let fetch_window = (window_start_ms, window_stop_ms) =>
501
+ new Promise((resolve, reject) => {
502
+ let req = {
503
+ id: 11,
504
+ method: 'private/get-trades',
505
+ params: {
506
+ instrument_name: pre_process_pair(pair),
507
+ start_time: window_start_ms,
508
+ end_time: window_stop_ms - 1,
509
+ limit: 100,
510
+ },
511
+ api_key: api_key,
512
+ nonce: Date.now(),
513
+ }
514
+ req.sig = get_signature_crypto(req, secret_key)
515
+ needle.post('https://api.crypto.com/v2/private/get-trades', req, options, (err, res, body) => {
516
+ body = parse_body(body)
517
+ let rows = body?.result?.data || body?.result?.trade_list
518
+ if (body && body.code === 0 && Array.isArray(rows)) {
519
+ resolve(
520
+ rows.map((trade) => {
521
+ let price = parseFloat(trade.traded_price)
522
+ let amount = parseFloat(trade.traded_quantity)
523
+ let fee = Math.abs(parseFloat(trade.fees || trade.fee))
524
+ let mapped_trade = {
525
+ order_id: (trade.order_id || '').toString(),
526
+ trade_id: (trade.trade_id || trade.trade_match_id || '').toString(),
527
+ pair,
528
+ type: (trade.side || '').toLowerCase(),
529
+ price,
530
+ amount,
531
+ total: price * amount,
532
+ close_time: new Date(parseInt(trade.create_time)),
533
+ }
534
+ if (trade.taker_side) mapped_trade.role = trade.taker_side.toLowerCase()
535
+ if (Number.isFinite(fee) && trade.fee_instrument_name) {
536
+ mapped_trade.fees = {
537
+ [post_process_cur(trade.fee_instrument_name)]: fee,
538
+ }
539
+ }
540
+ return mapped_trade
541
+ })
542
+ )
543
+ } else if (body) {
544
+ reject({ success: false, error: error_codes.other, body })
545
+ } else {
546
+ reject({ success: false, error: error_codes.timeout })
547
+ }
548
+ })
549
+ })
550
+ ;(async () => {
551
+ let trades = await fetch_split_windows({
552
+ start_ms: start_time_in_ms,
553
+ stop_ms: end_time_in_ms,
554
+ initial_window_ms: 7 * 24 * 60 * 60 * 1000,
555
+ page_limit: 100,
556
+ fetch_window,
557
+ delay_ms: 250,
558
+ })
559
+ trades = sort_history_rows(filter_history_in_range(trades, start_time_in_ms, end_time_in_ms))
560
+ cb({ success: true, body: trades })
561
+ })().catch((error) => cb(error))
562
+ }
496
563
  get_all_deposits(timeframe_in_ms, cb) {
497
564
  let [api_key, secret_key] = this.api_secret_key
498
565
  let req = {
@@ -467,6 +467,70 @@ module.exports = class Deepcoin extends ExchangeBase {
467
467
  }
468
468
  this.private_limiter.process(get_all_trades_base, timeframe_in_ms, cb, 2)
469
469
  }
470
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
471
+ let [api_key, secret_key] = this.api_secret_key
472
+ let { fetch_split_windows, filter_history_in_range, sort_history_rows } = ExchangeBase.history_utils
473
+ let path = '/deepcoin/trade/fills'
474
+ let fetch_window = (window_start_ms, window_stop_ms) =>
475
+ new Promise((resolve, reject) => {
476
+ let params = {
477
+ instType: 'SPOT',
478
+ instId: pre_process_pair(pair),
479
+ begin: window_start_ms,
480
+ end: window_stop_ms - 1,
481
+ limit: 100,
482
+ }
483
+ let query = qs.stringify(params)
484
+ let options = get_options(api_key, secret_key, this.passphrase, 'GET', path + '?' + query, '')
485
+ needle.get(base_url + path + '?' + query, options, (err, res, body) => {
486
+ body = parse_body(body)
487
+ if (body && body.code === '0' && Array.isArray(body.data)) {
488
+ resolve(
489
+ body.data.map((trade) => {
490
+ let price = parseFloat(trade.fillPx)
491
+ let amount = parseFloat(trade.fillSz)
492
+ let mapped_trade = {
493
+ order_id: (trade.ordId || '').toString(),
494
+ trade_id: (trade.tradeId || trade.billId || '').toString(),
495
+ pair: post_process_pair(trade.instId),
496
+ type: trade.side,
497
+ price,
498
+ amount,
499
+ total: price * amount,
500
+ close_time: new Date(parseInt(trade.ts)),
501
+ }
502
+ if (trade.execType === 'M') mapped_trade.role = 'maker'
503
+ if (trade.execType === 'T') mapped_trade.role = 'taker'
504
+ if (trade.feeCcy && Number.isFinite(parseFloat(trade.fee))) {
505
+ mapped_trade.fees = {
506
+ [post_process_cur(trade.feeCcy)]: Math.abs(parseFloat(trade.fee)),
507
+ }
508
+ }
509
+ return mapped_trade
510
+ })
511
+ )
512
+ } else if (body && (body.code === '50026' || body.code === '50001')) {
513
+ reject({ success: false, error: error_codes.service_unavailable, body })
514
+ } else if (body) {
515
+ reject({ success: false, error: error_codes.other, body })
516
+ } else {
517
+ reject({ success: false, error: error_codes.timeout })
518
+ }
519
+ })
520
+ })
521
+ ;(async () => {
522
+ let trades = await fetch_split_windows({
523
+ start_ms: start_time_in_ms,
524
+ stop_ms: end_time_in_ms,
525
+ initial_window_ms: 30 * 24 * 60 * 60 * 1000,
526
+ page_limit: 100,
527
+ fetch_window,
528
+ delay_ms: 250,
529
+ })
530
+ trades = sort_history_rows(filter_history_in_range(trades, start_time_in_ms, end_time_in_ms))
531
+ cb({ success: true, body: trades })
532
+ })().catch((error) => cb(error))
533
+ }
470
534
  static get_min_amount(cb) {
471
535
  let min_quote_cur_amount = {}
472
536
  needle.get('https://api.deepcoin.com/deepcoin/market/instruments?instType=SPOT', (err, res, body) => {
@@ -381,6 +381,68 @@ module.exports = class Digifinex extends ExchangeBase {
381
381
  }
382
382
  this.private_limiter.process(get_trades_base, pair, timeframe_in_ms, cb)
383
383
  }
384
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
385
+ let [api_key, secret_key] = this.api_secret_key
386
+ let { fetch_split_windows, filter_history_in_range, sort_history_rows } = ExchangeBase.history_utils
387
+ let fetch_window = (window_start_ms, window_stop_ms) =>
388
+ new Promise((resolve, reject) => {
389
+ let params = {
390
+ symbol: pre_process_pair(pair),
391
+ limit: 500,
392
+ start_time: Math.floor(window_start_ms / 1000),
393
+ end_time: Math.floor((window_stop_ms - 1) / 1000),
394
+ }
395
+ let signature = get_signature_digifinex(secret_key, params)
396
+ let options = get_options(api_key, signature)
397
+ let url = 'https://openapi.digifinex.com/v3/spot/mytrades?' + qs.stringify(params)
398
+ needle.get(url, options, (err, res, body) => {
399
+ if (body && body.code === 0 && Array.isArray(body.list)) {
400
+ resolve(
401
+ body.list.map((trade) => {
402
+ let price = parseFloat(trade.price)
403
+ let amount = parseFloat(trade.amount)
404
+ let is_maker = trade.is_maker === true || trade.is_maker === 1 || trade.is_maker === '1' || trade.is_maker === 'true'
405
+ let mapped_trade = {
406
+ order_id: (trade.order_id || '').toString(),
407
+ trade_id: (trade.id || trade.tid || '').toString(),
408
+ pair,
409
+ type: (trade.side || '').split('_')[0],
410
+ price,
411
+ amount,
412
+ total: price * amount,
413
+ close_time: new Date(parseInt(trade.timestamp) * 1000),
414
+ role: is_maker ? 'maker' : 'taker',
415
+ }
416
+ if (trade.fee_currency && Number.isFinite(parseFloat(trade.fee))) {
417
+ mapped_trade.fees = {
418
+ [post_process_cur(trade.fee_currency)]: Math.abs(parseFloat(trade.fee)),
419
+ }
420
+ }
421
+ return mapped_trade
422
+ })
423
+ )
424
+ } else if (body && body.toString().startsWith('<html>')) {
425
+ reject({ success: false, error: error_codes.service_unavailable, body })
426
+ } else if (body) {
427
+ reject({ success: false, error: error_codes.other, body })
428
+ } else {
429
+ reject({ success: false, error: error_codes.timeout })
430
+ }
431
+ })
432
+ })
433
+ ;(async () => {
434
+ let trades = await fetch_split_windows({
435
+ start_ms: start_time_in_ms,
436
+ stop_ms: end_time_in_ms,
437
+ initial_window_ms: 30 * 24 * 60 * 60 * 1000,
438
+ page_limit: 500,
439
+ fetch_window,
440
+ delay_ms: 250,
441
+ })
442
+ trades = sort_history_rows(filter_history_in_range(trades, start_time_in_ms, end_time_in_ms))
443
+ cb({ success: true, body: trades })
444
+ })().catch((error) => cb(error))
445
+ }
384
446
  get_all_deposits(timeframe_in_ms, cb) {
385
447
  let get_all_deposits_base = (timeframe_in_ms, cb) => {
386
448
  let [api_key, secret_key] = this.api_secret_key
@@ -1,5 +1,6 @@
1
1
  const _ = require('lodash')
2
2
  const { randomUUID: uuid } = require('crypto')
3
+ const qs = require('qs')
3
4
 
4
5
  function trim_orderbook_body(body, depth = 1) {
5
6
  if (!body || typeof body !== 'object') return body
@@ -18,6 +19,101 @@ function trim_orderbook_response(res, depth = 1) {
18
19
  return { ...res, body: trim_orderbook_body(res.body, depth) }
19
20
  }
20
21
 
22
+ const DEFAULT_HISTORY_MIN_WINDOW_MS = 60 * 1000
23
+
24
+ function sleep(ms) {
25
+ return new Promise((resolve) => setTimeout(resolve, ms))
26
+ }
27
+
28
+ function history_time_ms(value) {
29
+ if (value instanceof Date) return value.getTime()
30
+ if (typeof value === 'number') return value
31
+ if (typeof value === 'string' && /^\d+$/.test(value)) return parseInt(value, 10)
32
+ const parsed = new Date(value).getTime()
33
+ return Number.isFinite(parsed) ? parsed : NaN
34
+ }
35
+
36
+ function history_row_ts(row) {
37
+ return history_time_ms(row?.close_time || row?.open_time || row?.event_time || row?.time)
38
+ }
39
+
40
+ function filter_history_in_range(rows, start_ms, stop_ms) {
41
+ return (rows || []).filter((row) => {
42
+ const ts = history_row_ts(row)
43
+ return Number.isFinite(ts) && ts >= start_ms && ts < stop_ms
44
+ })
45
+ }
46
+
47
+ function sort_history_rows(rows) {
48
+ return (rows || []).slice().sort((left, right) => {
49
+ const left_ts = history_row_ts(left)
50
+ const right_ts = history_row_ts(right)
51
+ if (left_ts !== right_ts) return left_ts - right_ts
52
+ const left_trade_id = left?.trade_id != null ? String(left.trade_id) : ''
53
+ const right_trade_id = right?.trade_id != null ? String(right.trade_id) : ''
54
+ if (left_trade_id !== right_trade_id) return left_trade_id.localeCompare(right_trade_id)
55
+ const left_order_id = left?.order_id != null ? String(left.order_id) : ''
56
+ const right_order_id = right?.order_id != null ? String(right.order_id) : ''
57
+ return left_order_id.localeCompare(right_order_id)
58
+ })
59
+ }
60
+
61
+ function split_windows(start_ms, stop_ms, window_ms) {
62
+ const windows = []
63
+ for (let cursor = start_ms; cursor < stop_ms; cursor += window_ms) {
64
+ windows.push([cursor, Math.min(cursor + window_ms, stop_ms)])
65
+ }
66
+ return windows
67
+ }
68
+
69
+ async function fetch_split_windows({ start_ms, stop_ms, initial_window_ms, min_window_ms = DEFAULT_HISTORY_MIN_WINDOW_MS, page_limit, fetch_window, delay_ms = 0 }) {
70
+ async function fetch_recursive(window_start_ms, window_stop_ms) {
71
+ const rows = await fetch_window(window_start_ms, window_stop_ms)
72
+ if (rows.length >= page_limit && window_stop_ms - window_start_ms > min_window_ms) {
73
+ const midpoint = window_start_ms + Math.floor((window_stop_ms - window_start_ms) / 2)
74
+ const left = await fetch_recursive(window_start_ms, midpoint)
75
+ const right = await fetch_recursive(midpoint, window_stop_ms)
76
+ return left.concat(right)
77
+ }
78
+ return rows
79
+ }
80
+
81
+ const results = []
82
+ const windows = split_windows(start_ms, stop_ms, initial_window_ms)
83
+ for (const [window_start_ms, window_stop_ms] of windows) {
84
+ const rows = await fetch_recursive(window_start_ms, window_stop_ms)
85
+ results.push(...filter_history_in_range(rows, start_ms, stop_ms))
86
+ if (delay_ms > 0) await sleep(delay_ms)
87
+ }
88
+ return results
89
+ }
90
+
91
+ async function with_retry(fn, { retries = 4, should_retry = () => false, backoff_ms = (attempt) => attempt * 1000 } = {}) {
92
+ let last_error = null
93
+ for (let attempt = 1; attempt <= retries; attempt++) {
94
+ try {
95
+ return await fn(attempt)
96
+ } catch (error) {
97
+ last_error = error
98
+ if (attempt >= retries || !should_retry(error)) break
99
+ await sleep(backoff_ms(attempt, error))
100
+ }
101
+ }
102
+ throw last_error
103
+ }
104
+
105
+ function sort_query_params(params) {
106
+ const ordered = {}
107
+ for (const key of Object.keys(params).sort()) {
108
+ ordered[key] = params[key]
109
+ }
110
+ return ordered
111
+ }
112
+
113
+ function stringify_sorted(params) {
114
+ return qs.stringify(sort_query_params(params))
115
+ }
116
+
21
117
  /**
22
118
  * @typedef {import('./types').BalanceEntry} BalanceEntry
23
119
  * @typedef {import('./types').OrderResult} OrderResult
@@ -37,7 +133,7 @@ function trim_orderbook_response(res, depth = 1) {
37
133
  * Subclasses must override methods to provide exchange-specific implementations.
38
134
  * Methods that are not overridden will throw "not implemented" errors.
39
135
  */
40
- module.exports = class ExchangeBase {
136
+ class ExchangeBase {
41
137
  constructor(name, id = '', api_key, secret_key, exchange_type = 'spot') {
42
138
  this.name = name
43
139
  this.Name = _.upperFirst(name)
@@ -248,6 +344,61 @@ module.exports = class ExchangeBase {
248
344
  cb({ success: false, error: `${this.name}: get_trades not implemented` })
249
345
  }
250
346
 
347
+ /**
348
+ * Get trade history for a trading pair in an absolute time window.
349
+ * Implementations should return individual fills filtered to [start_time_in_ms, end_time_in_ms).
350
+ * @param {string} pair - Trading pair
351
+ * @param {number} start_time_in_ms - Inclusive start timestamp in milliseconds
352
+ * @param {number} end_time_in_ms - Exclusive end timestamp in milliseconds
353
+ * @param {function(ApiResponse<TradeResult[]>): void} cb - Callback with trades
354
+ */
355
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
356
+ cb({ success: false, error: `${this.name}: get_history_trades not implemented` })
357
+ }
358
+
359
+ /**
360
+ * Best-effort absolute-window history built from the exchange's existing recent trade endpoint.
361
+ * @param {string} pair
362
+ * @param {number} start_time_in_ms
363
+ * @param {number} end_time_in_ms
364
+ * @param {function(ApiResponse<TradeResult[]>): void} cb
365
+ * @param {{use_all_trades?: boolean}} options
366
+ */
367
+ _get_history_trades_via_timeframe(pair, start_time_in_ms, end_time_in_ms, cb, options = {}) {
368
+ if (!(start_time_in_ms < end_time_in_ms)) {
369
+ cb({ success: true, body: [] })
370
+ return
371
+ }
372
+
373
+ const use_all_trades = options.use_all_trades === true
374
+ const timeframe_in_ms = Math.max(end_time_in_ms - start_time_in_ms, Date.now() - start_time_in_ms, 0)
375
+ const handle = (res) => {
376
+ if (!res || !res.success) {
377
+ cb(res)
378
+ return
379
+ }
380
+ let trades = Array.isArray(res.body) ? res.body : []
381
+ if (pair) {
382
+ trades = trades.filter((trade) => !trade?.pair || trade.pair === pair)
383
+ }
384
+ cb({
385
+ success: true,
386
+ body: ExchangeBase.history_utils.sort_history_rows(ExchangeBase.history_utils.filter_history_in_range(trades, start_time_in_ms, end_time_in_ms)),
387
+ })
388
+ }
389
+
390
+ if (use_all_trades) {
391
+ if (typeof this.get_all_trades !== 'function') {
392
+ cb({ success: false, error: `${this.name}: get_all_trades not implemented` })
393
+ return
394
+ }
395
+ this.get_all_trades(timeframe_in_ms, handle)
396
+ return
397
+ }
398
+
399
+ this.get_trades(pair, timeframe_in_ms, handle)
400
+ }
401
+
251
402
  // ============================================
252
403
  // Deposit/Withdrawal Methods
253
404
  // ============================================
@@ -423,3 +574,18 @@ module.exports = class ExchangeBase {
423
574
  cb({ success: false, error: 'get_precision not implemented' })
424
575
  }
425
576
  }
577
+
578
+ ExchangeBase.history_utils = {
579
+ DEFAULT_HISTORY_MIN_WINDOW_MS,
580
+ fetch_split_windows,
581
+ filter_history_in_range,
582
+ history_time_ms,
583
+ sleep,
584
+ sort_history_rows,
585
+ sort_query_params,
586
+ split_windows,
587
+ stringify_sorted,
588
+ with_retry,
589
+ }
590
+
591
+ module.exports = ExchangeBase
@@ -82,7 +82,6 @@ module.exports = class ExchangeFix {
82
82
  this.on_market_data_batch = options.on_market_data_batch // called once per data chunk with array of fields
83
83
  this.on_market_data_snapshot = options.on_market_data_snapshot
84
84
  this.on_market_data_request_reject = options.on_market_data_request_reject
85
- this.on_inbound_message = options.on_inbound_message || (() => {})
86
85
  this.on_status_change = options.on_status_change || (() => {})
87
86
 
88
87
  this.socket = null
@@ -140,7 +139,6 @@ module.exports = class ExchangeFix {
140
139
  this.recv_buffer = remaining
141
140
  let md_batch = null
142
141
  for (let msg of messages) {
143
- this.on_inbound_message(msg)
144
142
  let t = msg.msg_type
145
143
  if (t === 'A') {
146
144
  this._set_status('ready')
@@ -218,8 +218,9 @@ module.exports = class ExchangeWs {
218
218
  const boundSocket = this.market_socket
219
219
  let message_handler = (...args) => {
220
220
  if (boundSocket !== this.market_socket) return
221
+ const t_recv = process.hrtime()
221
222
  this.market.last_message_time = Date.now()
222
- listener(...args)
223
+ listener(...args, t_recv)
223
224
  this.market_observable.notify()
224
225
  }
225
226
  if (this.market_socket.serviceHandlers) {
@@ -316,6 +316,9 @@ module.exports = class Fastex extends ExchangeBase {
316
316
  }
317
317
  this.private_limiter.process(get_trades_base, pair, cb)
318
318
  }
319
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
320
+ this._get_history_trades_via_timeframe(pair, start_time_in_ms, end_time_in_ms, cb)
321
+ }
319
322
  static get_min_amount(cb) {
320
323
  let min_quote_cur_amount = {}
321
324
  needle.get('https://exchange.fastex.com/api/v1/pair/list?items_per_page=100&page=1', (err, res, body) => {
@@ -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, t_recv) => {
163
163
  data = JSON.parse(data)
164
+ const t_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) {
@@ -174,6 +175,7 @@ module.exports = class Gate extends ExchangeBase {
174
175
  this.ws.market.ts_dict[pair] = {
175
176
  received_ts: Date.now(),
176
177
  event_ts: parseInt(t),
178
+ t_recv, t_parsed,
177
179
  }
178
180
  last_update_dict[pair] = lastUpdateId
179
181
  }
@@ -189,7 +191,7 @@ module.exports = class Gate extends ExchangeBase {
189
191
  if (options.depth === false) {
190
192
  this.ws.market.asks_dict[pair] = [[parseFloat(ask_1_price), parseFloat(ask_1_amount)]]
191
193
  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() }
194
+ this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parseInt(data.result.t) || Date.now(), t_recv, t_parsed }
193
195
  this.ws.market.pair_status[pair] = 'opened'
194
196
  } else {
195
197
  if (!this.ws.market.asks_dict[pair] || !this.ws.market.bids_dict[pair]) {
@@ -355,6 +355,76 @@ module.exports = class Gemini extends ExchangeBase {
355
355
  }
356
356
  this.private_limiter.process(get_trade_base, pair, cb)
357
357
  }
358
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
359
+ let [api_key, secret_key] = this.api_secret_key
360
+ let { filter_history_in_range, sort_history_rows, sleep } = ExchangeBase.history_utils
361
+ let fetch_page = (cursor_ms) =>
362
+ new Promise((resolve, reject) => {
363
+ let params = {
364
+ request: '/v1/mytrades',
365
+ nonce: Date.now(),
366
+ symbol: pre_process_pair(pair),
367
+ limit_trades: 500,
368
+ timestamp: cursor_ms,
369
+ }
370
+ let sign = get_signature_gemini(secret_key, params)
371
+ let request_options = get_options(api_key, secret_key, params, sign)
372
+ needle.post('https://api.gemini.com/v1/mytrades', {}, request_options, (err, res, body) => {
373
+ if (Array.isArray(body)) {
374
+ resolve(body)
375
+ } else if (body) {
376
+ reject({ success: false, error: error_codes.other, body })
377
+ } else {
378
+ reject({ success: false, error: error_codes.timeout })
379
+ }
380
+ })
381
+ })
382
+ ;(async () => {
383
+ let trades = []
384
+ let cursor_ms = Math.max(0, end_time_in_ms - 1)
385
+ while (cursor_ms >= start_time_in_ms) {
386
+ let rows = await fetch_page(cursor_ms)
387
+ if (rows.length === 0) break
388
+ trades.push(
389
+ ...rows
390
+ .filter((trade) => trade.break !== 'full')
391
+ .map((trade) => {
392
+ let price = parseFloat(trade.price)
393
+ let amount = parseFloat(trade.amount)
394
+ let close_time_ms = trade.timestampms != null ? parseInt(trade.timestampms) : parseInt(trade.timestamp) * 1000
395
+ let is_aggressor = trade.aggressor === true || trade.aggressor === 1 || trade.aggressor === '1' || trade.aggressor === 'true'
396
+ let mapped_trade = {
397
+ order_id: (trade.order_id || '').toString(),
398
+ trade_id: (trade.tid || '').toString(),
399
+ pair,
400
+ type: (trade.type || '').toLowerCase(),
401
+ price,
402
+ amount,
403
+ total: price * amount,
404
+ close_time: new Date(close_time_ms),
405
+ role: is_aggressor ? 'taker' : 'maker',
406
+ }
407
+ if (trade.fee_currency && Number.isFinite(parseFloat(trade.fee_amount))) {
408
+ mapped_trade.fees = {
409
+ [post_process_cur(trade.fee_currency)]: Math.abs(parseFloat(trade.fee_amount)),
410
+ }
411
+ }
412
+ return mapped_trade
413
+ })
414
+ )
415
+ let oldest_seen_ms = Math.min(
416
+ ...rows
417
+ .map((trade) => (trade.timestampms != null ? parseInt(trade.timestampms) : parseInt(trade.timestamp) * 1000))
418
+ .filter((time) => Number.isFinite(time))
419
+ )
420
+ if (rows.length < 500 || !Number.isFinite(oldest_seen_ms) || oldest_seen_ms < start_time_in_ms || oldest_seen_ms >= cursor_ms) break
421
+ cursor_ms = oldest_seen_ms - 1
422
+ await sleep(250)
423
+ }
424
+ trades = sort_history_rows(filter_history_in_range(trades, start_time_in_ms, end_time_in_ms))
425
+ cb({ success: true, body: trades })
426
+ })().catch((error) => cb(error))
427
+ }
358
428
  // get_all_trades (timeframe_in_ms, cb) {
359
429
  //
360
430
  // }