@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.
- 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-fix.js +0 -1
- package/lib/exchanges/coinbase.js +113 -11
- 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-fix.js +0 -2
- 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
|
@@ -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
|
-
|
|
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) {
|
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
|
// }
|