@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.
- package/API_DOCS.md +6 -6
- package/lib/exchanges/ascendex.js +3 -0
- package/lib/exchanges/biconomy.js +62 -0
- package/lib/exchanges/binance.js +61 -0
- package/lib/exchanges/bingx.js +70 -0
- package/lib/exchanges/bitfinex.js +76 -0
- package/lib/exchanges/bitget.js +82 -0
- package/lib/exchanges/bithumb.js +3 -0
- package/lib/exchanges/bitkub.js +3 -0
- package/lib/exchanges/bitmex.js +69 -0
- package/lib/exchanges/bitrue.js +56 -0
- package/lib/exchanges/bitstamp.js +71 -0
- package/lib/exchanges/blofin.js +72 -0
- package/lib/exchanges/btse.js +3 -0
- package/lib/exchanges/coinbase.js +102 -0
- package/lib/exchanges/coinstore.js +3 -0
- package/lib/exchanges/cryptocom.js +66 -0
- package/lib/exchanges/deepcoin.js +64 -0
- package/lib/exchanges/digifinex.js +62 -0
- package/lib/exchanges/exchange-base.js +167 -1
- package/lib/exchanges/fastex.js +3 -0
- package/lib/exchanges/gemini.js +70 -0
- package/lib/exchanges/hashkey.js +67 -0
- package/lib/exchanges/hashkeyglobal.js +67 -0
- package/lib/exchanges/hkbitex.js +3 -0
- package/lib/exchanges/htx.js +74 -0
- package/lib/exchanges/indodax.js +3 -0
- package/lib/exchanges/kraken.js +63 -0
- package/lib/exchanges/lbank.js +86 -51
- package/lib/exchanges/mexc.js +58 -0
- package/lib/exchanges/swft.js +3 -0
- package/lib/exchanges/upbit.js +57 -0
- package/lib/exchanges/weex.js +3 -0
- package/lib/exchanges/xt.js +61 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
package/lib/exchanges/fastex.js
CHANGED
|
@@ -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) => {
|
package/lib/exchanges/gemini.js
CHANGED
|
@@ -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
|
// }
|
package/lib/exchanges/hashkey.js
CHANGED
|
@@ -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 = {
|
package/lib/exchanges/hkbitex.js
CHANGED
|
@@ -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 = {}
|
package/lib/exchanges/htx.js
CHANGED
|
@@ -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'
|
package/lib/exchanges/indodax.js
CHANGED
|
@@ -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) => {
|
package/lib/exchanges/kraken.js
CHANGED
|
@@ -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 = {}
|