@icgio/icg-exchanges 1.40.42 → 1.40.44

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.
@@ -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
@@ -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) => {
@@ -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
  // }
@@ -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 Hashkey extends ExchangeBase {
48
57
  constructor(api_key, secret_key, settings) {
49
58
  super('hashkey', settings.id, api_key, secret_key)
@@ -462,6 +471,64 @@ module.exports = class Hashkey extends ExchangeBase {
462
471
  }
463
472
  this.private_limiter.process(get_trades_base, pair, timeframe_in_ms, cb)
464
473
  }
474
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
475
+ let [api_key, secret_key] = this.api_secret_key
476
+ let { fetch_split_windows, filter_history_in_range, sort_history_rows } = ExchangeBase.history_utils
477
+ let path = '/api/v1/account/trades'
478
+ let options = get_options(api_key)
479
+ let fetch_window = (window_start_ms, window_stop_ms) =>
480
+ new Promise((resolve, reject) => {
481
+ let params = {
482
+ symbol: pre_process_pair(pair),
483
+ startTime: window_start_ms,
484
+ endTime: window_stop_ms - 1,
485
+ limit: 1000,
486
+ timestamp: Date.now(),
487
+ }
488
+ params.signature = get_signature_hashkey(params, secret_key)
489
+ let url = base_url + path + '?' + qs.stringify(params)
490
+ this.private_limiter.process(needle.get, url, options, (err, res, body) => {
491
+ if (Array.isArray(body)) {
492
+ resolve(
493
+ body.map((trade) => {
494
+ let price = parseFloat(trade.price)
495
+ let amount = parseFloat(trade.qty)
496
+ let mapped_trade = {
497
+ order_id: (trade.orderId || '').toString(),
498
+ trade_id: (trade.id || trade.tradeId || '').toString(),
499
+ pair: post_process_pair(trade.symbol),
500
+ type: trade.isBuyer ? 'buy' : 'sell',
501
+ price,
502
+ amount,
503
+ total: Number.isFinite(parseFloat(trade.quoteQty)) ? parseFloat(trade.quoteQty) : price * amount,
504
+ close_time: new Date(parseInt(trade.time)),
505
+ role: trade.isMaker ? 'maker' : 'taker',
506
+ }
507
+ let fees = get_trade_fees_hashkey(trade)
508
+ if (_.keys(fees).length > 0) mapped_trade.fees = fees
509
+ return mapped_trade
510
+ })
511
+ )
512
+ } else if (body) {
513
+ reject({ success: false, error: error_codes.other, body })
514
+ } else {
515
+ reject({ success: false, error: error_codes.timeout })
516
+ }
517
+ })
518
+ })
519
+ ;(async () => {
520
+ let trades = await fetch_split_windows({
521
+ start_ms: start_time_in_ms,
522
+ stop_ms: end_time_in_ms,
523
+ initial_window_ms: 24 * 60 * 60 * 1000,
524
+ page_limit: 1000,
525
+ fetch_window,
526
+ delay_ms: 250,
527
+ })
528
+ trades = sort_history_rows(filter_history_in_range(trades, start_time_in_ms, end_time_in_ms))
529
+ cb({ success: true, body: trades })
530
+ })().catch((error) => cb(error))
531
+ }
465
532
  get_all_deposits(timeframe_in_ms, cb) {
466
533
  let [api_key, secret_key] = this.api_secret_key
467
534
  let params = {
@@ -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)
@@ -455,6 +464,64 @@ module.exports = class Hashkeyglobal extends ExchangeBase {
455
464
  }
456
465
  this.private_limiter.process(get_trades_base, pair, timeframe_in_ms, cb)
457
466
  }
467
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
468
+ let [api_key, secret_key] = this.api_secret_key
469
+ let { fetch_split_windows, filter_history_in_range, sort_history_rows } = ExchangeBase.history_utils
470
+ let path = '/api/v1/account/trades'
471
+ let options = get_options(api_key)
472
+ let fetch_window = (window_start_ms, window_stop_ms) =>
473
+ new Promise((resolve, reject) => {
474
+ let params = {
475
+ symbol: pre_process_pair(pair),
476
+ startTime: window_start_ms,
477
+ endTime: window_stop_ms - 1,
478
+ limit: 1000,
479
+ timestamp: Date.now(),
480
+ }
481
+ params.signature = get_signature_hashkey(params, secret_key)
482
+ let url = base_url + path + '?' + qs.stringify(params)
483
+ this.private_limiter.process(needle.get, url, options, (err, res, body) => {
484
+ if (Array.isArray(body)) {
485
+ resolve(
486
+ body.map((trade) => {
487
+ let price = parseFloat(trade.price)
488
+ let amount = parseFloat(trade.qty)
489
+ let mapped_trade = {
490
+ order_id: (trade.orderId || '').toString(),
491
+ trade_id: (trade.id || trade.tradeId || '').toString(),
492
+ pair: post_process_pair(trade.symbol),
493
+ type: trade.isBuyer ? 'buy' : 'sell',
494
+ price,
495
+ amount,
496
+ total: Number.isFinite(parseFloat(trade.quoteQty)) ? parseFloat(trade.quoteQty) : price * amount,
497
+ close_time: new Date(parseInt(trade.time)),
498
+ role: trade.isMaker ? 'maker' : 'taker',
499
+ }
500
+ let fees = get_trade_fees_hashkey(trade)
501
+ if (_.keys(fees).length > 0) mapped_trade.fees = fees
502
+ return mapped_trade
503
+ })
504
+ )
505
+ } else if (body) {
506
+ reject({ success: false, error: error_codes.other, body })
507
+ } else {
508
+ reject({ success: false, error: error_codes.timeout })
509
+ }
510
+ })
511
+ })
512
+ ;(async () => {
513
+ let trades = await fetch_split_windows({
514
+ start_ms: start_time_in_ms,
515
+ stop_ms: end_time_in_ms,
516
+ initial_window_ms: 24 * 60 * 60 * 1000,
517
+ page_limit: 1000,
518
+ fetch_window,
519
+ delay_ms: 250,
520
+ })
521
+ trades = sort_history_rows(filter_history_in_range(trades, start_time_in_ms, end_time_in_ms))
522
+ cb({ success: true, body: trades })
523
+ })().catch((error) => cb(error))
524
+ }
458
525
  get_all_deposits(timeframe_in_ms, cb) {
459
526
  let [api_key, secret_key] = this.api_secret_key
460
527
  let params = {
@@ -556,6 +556,9 @@ module.exports = class Hkbitex extends ExchangeBase {
556
556
  }
557
557
  this.private_limiter.process(get_trades_base, pair, timeframe_in_ms, cb)
558
558
  }
559
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
560
+ this._get_history_trades_via_timeframe(pair, start_time_in_ms, end_time_in_ms, cb)
561
+ }
559
562
  static get_precision(cb) {
560
563
  let price_precision = {},
561
564
  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')
@@ -539,6 +540,79 @@ module.exports = class Htx extends ExchangeBase {
539
540
  }
540
541
  this.private_limiter.process(get_trades_base, pair, timeframe_in_ms, cb)
541
542
  }
543
+ get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
544
+ const get_history_trades_base = (pair, start_time_in_ms, end_time_in_ms, cb) => {
545
+ const account_id = this.account_id
546
+ if (!account_id) {
547
+ cb({ success: false, error: error_codes.other, body: { message: 'htx account_id missing' } })
548
+ return
549
+ }
550
+ const [api_key, secret_key] = this.api_secret_key
551
+ ;(async () => {
552
+ const rows = await fetch_split_windows({
553
+ start_ms: start_time_in_ms,
554
+ stop_ms: end_time_in_ms,
555
+ initial_window_ms: 60 * 60 * 1000,
556
+ page_limit: 1000,
557
+ fetch_window: async (window_start_ms, window_stop_ms) =>
558
+ await new Promise((resolve, reject) => {
559
+ let base_url = 'api.huobi.pro'
560
+ let path = '/v1/order/matchresults'
561
+ let params = {
562
+ AccessKeyId: api_key,
563
+ SignatureMethod: 'HmacSHA256',
564
+ SignatureVersion: 2,
565
+ Timestamp: new Date().toISOString().slice(0, 19),
566
+ 'account-id': account_id,
567
+ 'start-time': window_start_ms,
568
+ 'end-time': window_stop_ms - 1,
569
+ size: 1000,
570
+ symbol: pre_process_pair(pair),
571
+ }
572
+ const signature = crypto.createHmac('sha256', secret_key).update(`GET\n${base_url}\n${path}\n${stringify_sorted(params)}`).digest('base64')
573
+ params.Signature = signature
574
+ let url = 'https://' + base_url + path + '?' + stringify_sorted(params)
575
+ needle.get(url, (err, res, body) => {
576
+ if (body && Array.isArray(body.data)) {
577
+ resolve(
578
+ body.data.map((trade) => {
579
+ let result = {
580
+ order_id: trade['order-id'] != null ? trade['order-id'].toString() : '',
581
+ pair: post_process_pair(trade.symbol),
582
+ type: String(trade.type || '').startsWith('buy') ? 'buy' : 'sell',
583
+ price: parseFloat(trade.price),
584
+ amount: parseFloat(trade['filled-amount']),
585
+ total: parseFloat(trade['filled-amount']) * parseFloat(trade.price),
586
+ close_time: new Date(trade['created-at']),
587
+ fees: {
588
+ [post_process_cur(trade['fee-currency'])]: parseFloat(trade['filled-fees']),
589
+ },
590
+ role: String(trade.role || '').toLowerCase(),
591
+ }
592
+ let trade_id = trade['match-id'] || trade['trade-id'] || trade.id
593
+ if (trade_id != null) result.trade_id = trade_id.toString()
594
+ return result
595
+ }),
596
+ )
597
+ } else if (body && body.status === 'error' && body['err-code'] === 'api-signature-not-valid') {
598
+ reject({ error: error_codes.invalid_key, body })
599
+ } else if (body && body.toString().startsWith('<html>')) {
600
+ reject({ error: error_codes.service_unavailable, body })
601
+ } else if (body) {
602
+ reject({ error: error_codes.other, body })
603
+ } else {
604
+ reject({ error: error_codes.timeout })
605
+ }
606
+ })
607
+ }),
608
+ })
609
+ cb({ success: true, body: sort_history_rows(rows) })
610
+ })().catch((error) => {
611
+ cb({ success: false, error: error?.error || error_codes.other, body: error?.body })
612
+ })
613
+ }
614
+ this.private_limiter.process(get_history_trades_base, pair, start_time_in_ms, end_time_in_ms, cb)
615
+ }
542
616
  // get_all_trades(timeframe_in_ms, cb) {
543
617
  // let get_all_trades_base = (timeframe_in_ms, cb) => {
544
618
  // 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 = {}