@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.
- package/API_DOCS.md +6 -6
- package/lib/exchanges/ascendex.js +7 -3
- package/lib/exchanges/biconomy.js +65 -2
- package/lib/exchanges/binance.js +65 -3
- package/lib/exchanges/bingx.js +73 -2
- package/lib/exchanges/bitfinex.js +76 -0
- package/lib/exchanges/bitget.js +86 -3
- package/lib/exchanges/bithumb.js +3 -0
- package/lib/exchanges/bitkub.js +3 -0
- package/lib/exchanges/bitmart.js +3 -2
- package/lib/exchanges/bitmex.js +71 -1
- package/lib/exchanges/bitrue.js +59 -2
- package/lib/exchanges/bitstamp.js +71 -0
- package/lib/exchanges/blofin.js +76 -3
- package/lib/exchanges/btse.js +6 -2
- package/lib/exchanges/bybit.js +3 -2
- package/lib/exchanges/coinbase.js +113 -8
- package/lib/exchanges/coinstore.js +6 -2
- package/lib/exchanges/cryptocom.js +69 -2
- 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/exchange-ws.js +2 -1
- package/lib/exchanges/fastex.js +3 -0
- package/lib/exchanges/gate.js +4 -2
- package/lib/exchanges/gemini.js +70 -0
- package/lib/exchanges/hashkey.js +70 -2
- package/lib/exchanges/hashkeyglobal.js +70 -2
- package/lib/exchanges/hitbtc.js +2 -1
- package/lib/exchanges/hkbitex.js +6 -2
- package/lib/exchanges/htx.js +78 -3
- package/lib/exchanges/indodax.js +3 -0
- package/lib/exchanges/kraken.js +63 -0
- package/lib/exchanges/kucoin.js +4 -3
- package/lib/exchanges/lbank.js +89 -53
- package/lib/exchanges/mexc.js +61 -2
- package/lib/exchanges/okx.js +4 -3
- package/lib/exchanges/phemex.js +4 -3
- package/lib/exchanges/swft.js +12 -9
- package/lib/exchanges/upbit.js +60 -2
- package/lib/exchanges/weex.js +3 -0
- package/lib/exchanges/xt.js +64 -2
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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
|
|
@@ -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) {
|
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/gate.js
CHANGED
|
@@ -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]) {
|
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)
|
|
@@ -99,9 +108,10 @@ module.exports = class Hashkey extends ExchangeBase {
|
|
|
99
108
|
}),
|
|
100
109
|
)
|
|
101
110
|
})
|
|
102
|
-
this.ws.market.on_message((data) => {
|
|
111
|
+
this.ws.market.on_message((data, t_recv) => {
|
|
103
112
|
try {
|
|
104
113
|
data = JSON.parse(data)
|
|
114
|
+
const t_parsed = process.hrtime()
|
|
105
115
|
if (data.pong) {
|
|
106
116
|
this.ws.market.last_pong = new Date()
|
|
107
117
|
} else {
|
|
@@ -111,7 +121,7 @@ module.exports = class Hashkey extends ExchangeBase {
|
|
|
111
121
|
const { a, b } = data.data
|
|
112
122
|
this.ws.market.asks_dict[pair] = a.map((ask) => [parseFloat(ask[0]), parseFloat(ask[1])])
|
|
113
123
|
this.ws.market.bids_dict[pair] = b.map((bid) => [parseFloat(bid[0]), parseFloat(bid[1])])
|
|
114
|
-
this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parseFloat(data.data.t) || undefined, send_ts: parseFloat(data.sendTime) || undefined }
|
|
124
|
+
this.ws.market.ts_dict[pair] = { received_ts: Date.now(), event_ts: parseFloat(data.data.t) || undefined, send_ts: parseFloat(data.sendTime) || undefined, t_recv, t_parsed }
|
|
115
125
|
const asks = this.ws.market.asks_dict[pair]
|
|
116
126
|
const bids = this.ws.market.bids_dict[pair]
|
|
117
127
|
if (asks && asks.length > 0 && bids && bids.length > 0) {
|
|
@@ -462,6 +472,64 @@ module.exports = class Hashkey extends ExchangeBase {
|
|
|
462
472
|
}
|
|
463
473
|
this.private_limiter.process(get_trades_base, pair, timeframe_in_ms, cb)
|
|
464
474
|
}
|
|
475
|
+
get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
|
|
476
|
+
let [api_key, secret_key] = this.api_secret_key
|
|
477
|
+
let { fetch_split_windows, filter_history_in_range, sort_history_rows } = ExchangeBase.history_utils
|
|
478
|
+
let path = '/api/v1/account/trades'
|
|
479
|
+
let options = get_options(api_key)
|
|
480
|
+
let fetch_window = (window_start_ms, window_stop_ms) =>
|
|
481
|
+
new Promise((resolve, reject) => {
|
|
482
|
+
let params = {
|
|
483
|
+
symbol: pre_process_pair(pair),
|
|
484
|
+
startTime: window_start_ms,
|
|
485
|
+
endTime: window_stop_ms - 1,
|
|
486
|
+
limit: 1000,
|
|
487
|
+
timestamp: Date.now(),
|
|
488
|
+
}
|
|
489
|
+
params.signature = get_signature_hashkey(params, secret_key)
|
|
490
|
+
let url = base_url + path + '?' + qs.stringify(params)
|
|
491
|
+
this.private_limiter.process(needle.get, url, options, (err, res, body) => {
|
|
492
|
+
if (Array.isArray(body)) {
|
|
493
|
+
resolve(
|
|
494
|
+
body.map((trade) => {
|
|
495
|
+
let price = parseFloat(trade.price)
|
|
496
|
+
let amount = parseFloat(trade.qty)
|
|
497
|
+
let mapped_trade = {
|
|
498
|
+
order_id: (trade.orderId || '').toString(),
|
|
499
|
+
trade_id: (trade.id || trade.tradeId || '').toString(),
|
|
500
|
+
pair: post_process_pair(trade.symbol),
|
|
501
|
+
type: trade.isBuyer ? 'buy' : 'sell',
|
|
502
|
+
price,
|
|
503
|
+
amount,
|
|
504
|
+
total: Number.isFinite(parseFloat(trade.quoteQty)) ? parseFloat(trade.quoteQty) : price * amount,
|
|
505
|
+
close_time: new Date(parseInt(trade.time)),
|
|
506
|
+
role: trade.isMaker ? 'maker' : 'taker',
|
|
507
|
+
}
|
|
508
|
+
let fees = get_trade_fees_hashkey(trade)
|
|
509
|
+
if (_.keys(fees).length > 0) mapped_trade.fees = fees
|
|
510
|
+
return mapped_trade
|
|
511
|
+
})
|
|
512
|
+
)
|
|
513
|
+
} else if (body) {
|
|
514
|
+
reject({ success: false, error: error_codes.other, body })
|
|
515
|
+
} else {
|
|
516
|
+
reject({ success: false, error: error_codes.timeout })
|
|
517
|
+
}
|
|
518
|
+
})
|
|
519
|
+
})
|
|
520
|
+
;(async () => {
|
|
521
|
+
let trades = await fetch_split_windows({
|
|
522
|
+
start_ms: start_time_in_ms,
|
|
523
|
+
stop_ms: end_time_in_ms,
|
|
524
|
+
initial_window_ms: 24 * 60 * 60 * 1000,
|
|
525
|
+
page_limit: 1000,
|
|
526
|
+
fetch_window,
|
|
527
|
+
delay_ms: 250,
|
|
528
|
+
})
|
|
529
|
+
trades = sort_history_rows(filter_history_in_range(trades, start_time_in_ms, end_time_in_ms))
|
|
530
|
+
cb({ success: true, body: trades })
|
|
531
|
+
})().catch((error) => cb(error))
|
|
532
|
+
}
|
|
465
533
|
get_all_deposits(timeframe_in_ms, cb) {
|
|
466
534
|
let [api_key, secret_key] = this.api_secret_key
|
|
467
535
|
let params = {
|