@icgio/icg-exchanges 1.40.49 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/lib/exchanges/ascendex.js +1 -3
  2. package/lib/exchanges/biconomy.js +2 -2
  3. package/lib/exchanges/binance.js +1 -0
  4. package/lib/exchanges/bingx.js +2 -1
  5. package/lib/exchanges/bitget.js +1 -0
  6. package/lib/exchanges/bithumb.js +1 -3
  7. package/lib/exchanges/bitkub.js +1 -3
  8. package/lib/exchanges/bitmart.js +49 -32
  9. package/lib/exchanges/bitmex.js +1 -0
  10. package/lib/exchanges/bitrue.js +2 -1
  11. package/lib/exchanges/bitstamp.js +1 -0
  12. package/lib/exchanges/blofin.js +1 -0
  13. package/lib/exchanges/btse.js +1 -3
  14. package/lib/exchanges/bybit.js +48 -35
  15. package/lib/exchanges/coinbase.js +359 -71
  16. package/lib/exchanges/coinstore.js +1 -3
  17. package/lib/exchanges/coinw.js +48 -32
  18. package/lib/exchanges/cryptocom.js +1 -0
  19. package/lib/exchanges/deepcoin.js +1 -0
  20. package/lib/exchanges/digifinex.js +1 -0
  21. package/lib/exchanges/exchange-base.js +12 -46
  22. package/lib/exchanges/exchange-fix.js +191 -86
  23. package/lib/exchanges/fastex.js +1 -3
  24. package/lib/exchanges/gate.js +49 -34
  25. package/lib/exchanges/gemini.js +1 -0
  26. package/lib/exchanges/hashkey.js +1 -0
  27. package/lib/exchanges/hashkeyglobal.js +1 -0
  28. package/lib/exchanges/hitbtc.js +44 -27
  29. package/lib/exchanges/hkbitex.js +1 -3
  30. package/lib/exchanges/htx.js +4 -3
  31. package/lib/exchanges/indodax.js +1 -3
  32. package/lib/exchanges/kraken.js +1 -0
  33. package/lib/exchanges/kucoin.js +2 -1
  34. package/lib/exchanges/lbank.js +2 -1
  35. package/lib/exchanges/mexc.js +2 -1
  36. package/lib/exchanges/okx.js +51 -34
  37. package/lib/exchanges/phemex.js +47 -31
  38. package/lib/exchanges/poloniex.js +43 -27
  39. package/lib/exchanges/swft.js +1 -3
  40. package/lib/exchanges/upbit.js +1 -0
  41. package/lib/exchanges/weex.js +1 -3
  42. package/lib/exchanges/xt.js +2 -1
  43. package/package.json +1 -1
  44. package/lib/exchanges/coinbase-fix.js +0 -267
@@ -440,6 +440,7 @@ module.exports = class Digifinex extends ExchangeBase {
440
440
  delay_ms: 250,
441
441
  })
442
442
  trades = sort_history_rows(filter_history_in_range(trades, start_time_in_ms, end_time_in_ms))
443
+ trades = utils.merge_trades_with_same_id(trades)
443
444
  cb({ success: true, body: trades })
444
445
  })().catch((error) => cb(error))
445
446
  }
@@ -450,9 +450,14 @@ class ExchangeBase {
450
450
  // ============================================
451
451
 
452
452
  /**
453
- * Get trade history for a trading pair
453
+ * Get trade history for a trading pair within a relative time window.
454
+ *
455
+ * Implementations should call utils.merge_trades_with_same_id() before returning,
456
+ * which groups fills by order_id, aggregates amount/total/fees, recalculates price,
457
+ * and populates fill_history[] on every returned trade (even single-fill trades).
458
+ *
454
459
  * @param {string} pair - Trading pair
455
- * @param {number} timeframe_in_ms - Time range in milliseconds
460
+ * @param {number} timeframe_in_ms - Time range in milliseconds (now - timeframe_in_ms .. now)
456
461
  * @param {function(ApiResponse<TradeResult[]>): void} cb - Callback with trades
457
462
  */
458
463
  get_trades(pair, timeframe_in_ms, cb) {
@@ -461,7 +466,11 @@ class ExchangeBase {
461
466
 
462
467
  /**
463
468
  * Get trade history for a trading pair in an absolute time window.
464
- * Implementations should return individual fills filtered to [start_time_in_ms, end_time_in_ms).
469
+ *
470
+ * Implementations should call utils.merge_trades_with_same_id() before
471
+ * returning, which groups fills by order_id, aggregates amount/total/fees,
472
+ * recalculates price, and populates fill_history[] on every returned trade.
473
+ *
465
474
  * @param {string} pair - Trading pair
466
475
  * @param {number} start_time_in_ms - Inclusive start timestamp in milliseconds
467
476
  * @param {number} end_time_in_ms - Exclusive end timestamp in milliseconds
@@ -471,49 +480,6 @@ class ExchangeBase {
471
480
  cb({ success: false, error: `${this.name}: get_history_trades not implemented` })
472
481
  }
473
482
 
474
- /**
475
- * Best-effort absolute-window history built from the exchange's existing recent trade endpoint.
476
- * @param {string} pair
477
- * @param {number} start_time_in_ms
478
- * @param {number} end_time_in_ms
479
- * @param {function(ApiResponse<TradeResult[]>): void} cb
480
- * @param {{use_all_trades?: boolean}} options
481
- */
482
- _get_history_trades_via_timeframe(pair, start_time_in_ms, end_time_in_ms, cb, options = {}) {
483
- if (!(start_time_in_ms < end_time_in_ms)) {
484
- cb({ success: true, body: [] })
485
- return
486
- }
487
-
488
- const use_all_trades = options.use_all_trades === true
489
- const timeframe_in_ms = Math.max(end_time_in_ms - start_time_in_ms, Date.now() - start_time_in_ms, 0)
490
- const handle = (res) => {
491
- if (!res || !res.success) {
492
- cb(res)
493
- return
494
- }
495
- let trades = Array.isArray(res.body) ? res.body : []
496
- if (pair) {
497
- trades = trades.filter((trade) => !trade?.pair || trade.pair === pair)
498
- }
499
- cb({
500
- success: true,
501
- body: ExchangeBase.history_utils.sort_history_rows(ExchangeBase.history_utils.filter_history_in_range(trades, start_time_in_ms, end_time_in_ms)),
502
- })
503
- }
504
-
505
- if (use_all_trades) {
506
- if (typeof this.get_all_trades !== 'function') {
507
- cb({ success: false, error: `${this.name}: get_all_trades not implemented` })
508
- return
509
- }
510
- this.get_all_trades(timeframe_in_ms, handle)
511
- return
512
- }
513
-
514
- this.get_trades(pair, timeframe_in_ms, handle)
515
- }
516
-
517
483
  // ============================================
518
484
  // Deposit/Withdrawal Methods
519
485
  // ============================================
@@ -1,8 +1,8 @@
1
- // Generic FIX 5.0 protocol handler over TLS.
1
+ // Generic FIX 5.0 transport manager over TLS.
2
2
  //
3
- // Handles: TLS connection, message encode/decode, heartbeat, sequence numbers,
4
- // logon/logout lifecycle, auto-reconnect. Exchange-specific logic lives in
5
- // the caller (e.g. coinbase-fix.js).
3
+ // Exposes `market` and `trading` channels that each own their own FIX session
4
+ // lifecycle: connect, heartbeat, routine reconnect, disconnect, and restart.
5
+ // Exchange-specific message handling lives in the caller.
6
6
 
7
7
  const tls = require('tls')
8
8
 
@@ -67,126 +67,231 @@ function parse_fix_messages(recv_buffer) {
67
67
  // --- ExchangeFix ---
68
68
 
69
69
  module.exports = class ExchangeFix {
70
- constructor(name, options) {
70
+ constructor(name, options = {}) {
71
71
  this.name = name
72
- this.host = options.host
73
- this.port = options.port
74
- this.sender_comp_id = options.sender_comp_id
75
- this.target_comp_id = options.target_comp_id
76
- this.heartbeat_interval = (options.heartbeat_interval || 30) * 1000
77
- this.build_logon_fields = options.build_logon_fields
78
- this.on_execution_report = options.on_execution_report
79
- this.on_cancel_reject = options.on_cancel_reject
80
- this.on_reject = options.on_reject
81
- this.on_market_data_incremental = options.on_market_data_incremental
82
- this.on_market_data_batch = options.on_market_data_batch // called once per data chunk with array of fields
83
- this.on_market_data_snapshot = options.on_market_data_snapshot
84
- this.on_market_data_request_reject = options.on_market_data_request_reject
85
- this.on_status_change = options.on_status_change || (() => {})
86
-
87
- this.socket = null
88
- this.status = 'disconnected'
89
- this.seq_out = 1
90
- this.recv_buffer = Buffer.alloc(0)
91
- this.heartbeat_timer = null
92
- this.reconnect_timer = null
72
+ this.market = this._create_channel('market', options.market)
73
+ this.trading = this._create_channel('trading', options.trading)
93
74
  }
94
75
 
95
- connect() {
96
- if (this.status !== 'disconnected') return
97
- this._set_status('connecting')
98
- this.socket = tls.connect({ host: this.host, port: this.port }, () => {
99
- this._set_status('logging_in')
100
- let sending_time = fix_timestamp()
101
- let logon_fields = this.build_logon_fields(this.seq_out, sending_time)
102
- this.send('A', logon_fields, sending_time)
76
+ _create_channel(kind, options) {
77
+ if (!options) return null
78
+ const channel = {
79
+ kind,
80
+ name: `${this.name}_${kind}`,
81
+ host: options.host,
82
+ port: options.port,
83
+ sender_comp_id: options.sender_comp_id,
84
+ target_comp_id: options.target_comp_id,
85
+ heartbeat_interval: (options.heartbeat_interval || 30) * 1000,
86
+ reconnecting_time: options.reconnecting_time || 2 * 60 * 60 * 1000,
87
+ restart_after_close_time: options.restart_after_close_time || 3 * 1000,
88
+ build_logon_fields: options.build_logon_fields,
89
+ on_execution_report: options.on_execution_report,
90
+ on_cancel_reject: options.on_cancel_reject,
91
+ on_reject: options.on_reject,
92
+ on_market_data_incremental: options.on_market_data_incremental,
93
+ on_market_data_batch: options.on_market_data_batch,
94
+ on_market_data_snapshot: options.on_market_data_snapshot,
95
+ on_market_data_request_reject: options.on_market_data_request_reject,
96
+ on_status_change: options.on_status_change || (() => {}),
97
+ status: 'n-opened',
98
+ session_status: 'disconnected',
99
+ socket: null,
100
+ seq_out: 1,
101
+ recv_buffer: Buffer.alloc(0),
102
+ heartbeat_timer: null,
103
+ restart_timeout: null,
104
+ reconnecting_timeout: null,
105
+ start: () => this._start_channel(channel),
106
+ end: () => this._end_channel(channel),
107
+ close: () => this._close_channel(channel),
108
+ send: (msg_type, fields, sending_time) => this._send(channel, msg_type, fields, sending_time),
109
+ is_ready: () => channel.status === 'opened' && channel.session_status === 'ready',
110
+ }
111
+ return channel
112
+ }
113
+
114
+ _start_channel(channel) {
115
+ if (!channel) return
116
+ if (channel.status !== 'n-opened' && channel.status !== 'closed' && channel.status !== 'stopped') return
117
+
118
+ clearTimeout(channel.restart_timeout)
119
+ channel.restart_timeout = null
120
+ clearTimeout(channel.reconnecting_timeout)
121
+ channel.reconnecting_timeout = null
122
+ this._clear_heartbeat(channel)
123
+
124
+ channel.status = 'opening'
125
+ this._set_session_status(channel, 'connecting')
126
+
127
+ const socket = tls.connect({ host: channel.host, port: channel.port }, () => {
128
+ if (socket !== channel.socket) return
129
+ this._set_session_status(channel, 'logging_in')
130
+ const sending_time = fix_timestamp()
131
+ const logon_fields = channel.build_logon_fields(channel.seq_out, sending_time)
132
+ this._send(channel, 'A', logon_fields, sending_time)
133
+ })
134
+
135
+ channel.socket = socket
136
+ socket.on('data', (chunk) => {
137
+ if (socket !== channel.socket) return
138
+ this._on_data(channel, chunk)
103
139
  })
104
- this.socket.on('data', (chunk) => this._on_data(chunk))
105
- this.socket.on('error', (err) => {
106
- console.log('[FIX %s] error: %s', this.name, err.message)
107
- this._reconnect()
140
+ socket.on('error', (err) => {
141
+ if (socket !== channel.socket) return
142
+ console.log('[FIX %s] error: %s', channel.name, err.message)
143
+ this._handle_disconnect(channel, { restart: channel.status !== 'stopped' })
108
144
  })
109
- this.socket.on('close', () => {
110
- if (this.status !== 'disconnected') this._reconnect()
145
+ socket.on('close', () => {
146
+ if (socket !== channel.socket) return
147
+ this._handle_disconnect(channel, { restart: channel.status !== 'stopped' })
111
148
  })
112
149
  }
113
150
 
114
- disconnect() {
115
- this._clear_timers()
116
- this.status = 'disconnected'
117
- if (this.socket) {
118
- try { this.send('5', []) } catch (e) {}
119
- this.socket.destroy()
120
- this.socket = null
151
+ _end_channel(channel) {
152
+ if (!channel) return
153
+ if (channel.status !== 'opened' && channel.status !== 'opening' && !channel.socket) return
154
+ this._handle_disconnect(channel, { restart: true, graceful: true, next_status: 'closed' })
155
+ }
156
+
157
+ _close_channel(channel) {
158
+ if (!channel) return
159
+ clearTimeout(channel.restart_timeout)
160
+ channel.restart_timeout = null
161
+ clearTimeout(channel.reconnecting_timeout)
162
+ channel.reconnecting_timeout = null
163
+
164
+ if (channel.socket || channel.session_status !== 'disconnected') {
165
+ this._handle_disconnect(channel, { restart: false, graceful: true, next_status: 'stopped' })
166
+ } else {
167
+ channel.status = 'stopped'
168
+ channel.session_status = 'disconnected'
169
+ this._reset_session_state(channel)
121
170
  }
122
- this.on_status_change('disconnected')
123
171
  }
124
172
 
125
- send(msg_type, fields, sending_time) {
126
- if (!this.socket || this.socket.destroyed) return false
127
- this.socket.write(encode_fix_msg(msg_type, fields, this.seq_out, this.sender_comp_id, this.target_comp_id, sending_time))
128
- this.seq_out++
173
+ _send(channel, msg_type, fields, sending_time) {
174
+ return this._send_on_socket(channel, channel.socket, msg_type, fields, sending_time)
175
+ }
176
+
177
+ _send_on_socket(channel, socket, msg_type, fields, sending_time) {
178
+ if (!socket || socket.destroyed) return false
179
+ socket.write(encode_fix_msg(msg_type, fields, channel.seq_out, channel.sender_comp_id, channel.target_comp_id, sending_time))
180
+ channel.seq_out++
129
181
  return true
130
182
  }
131
183
 
132
- is_ready() { return this.status === 'ready' }
184
+ _set_session_status(channel, status) {
185
+ if (channel.session_status === status) return
186
+ channel.session_status = status
187
+ channel.on_status_change(status)
188
+ }
133
189
 
134
- _set_status(s) { this.status = s; this.on_status_change(s) }
190
+ _mark_ready(channel) {
191
+ channel.status = 'opened'
192
+ this._start_heartbeat(channel)
193
+ clearTimeout(channel.reconnecting_timeout)
194
+ channel.reconnecting_timeout = setTimeout(() => {
195
+ channel.reconnecting_timeout = null
196
+ console.log('[FIX %s] routine reconnecting', channel.name)
197
+ channel.end()
198
+ }, channel.reconnecting_time)
199
+ this._set_session_status(channel, 'ready')
200
+ }
135
201
 
136
- _on_data(chunk) {
137
- this.recv_buffer = Buffer.concat([this.recv_buffer, chunk])
138
- let { messages, remaining } = parse_fix_messages(this.recv_buffer)
139
- this.recv_buffer = remaining
202
+ _on_data(channel, chunk) {
203
+ channel.recv_buffer = Buffer.concat([channel.recv_buffer, chunk])
204
+ const { messages, remaining } = parse_fix_messages(channel.recv_buffer)
205
+ channel.recv_buffer = remaining
140
206
  let md_batch = null
141
- for (let msg of messages) {
142
- let t = msg.msg_type
207
+
208
+ for (const msg of messages) {
209
+ const t = msg.msg_type
143
210
  if (t === 'A') {
144
- this._set_status('ready')
145
- this._start_heartbeat()
211
+ this._mark_ready(channel)
146
212
  } else if (t === '0') {
147
213
  // Heartbeat
148
214
  } else if (t === '1') {
149
- this.send('0', [[112, msg.fields['112'] || '']])
215
+ channel.send('0', [[112, msg.fields['112'] || '']])
150
216
  } else if (t === '5') {
151
- this._reconnect()
217
+ this._handle_disconnect(channel, { restart: channel.status !== 'stopped' })
152
218
  } else if (t === '8') {
153
- if (this.on_execution_report) this.on_execution_report(msg.fields)
219
+ if (channel.on_execution_report) channel.on_execution_report(msg.fields)
154
220
  } else if (t === '9') {
155
- if (this.on_cancel_reject) this.on_cancel_reject(msg.fields)
221
+ if (channel.on_cancel_reject) channel.on_cancel_reject(msg.fields)
156
222
  } else if (t === 'W') {
157
- if (this.on_market_data_snapshot) this.on_market_data_snapshot(msg.fields)
223
+ if (channel.on_market_data_snapshot) channel.on_market_data_snapshot(msg.fields)
158
224
  } else if (t === 'X') {
159
- if (this.on_market_data_batch) {
225
+ if (channel.on_market_data_batch) {
160
226
  if (!md_batch) md_batch = []
161
227
  md_batch.push(msg.fields)
162
- } else if (this.on_market_data_incremental) {
163
- this.on_market_data_incremental(msg.fields)
228
+ } else if (channel.on_market_data_incremental) {
229
+ channel.on_market_data_incremental(msg.fields)
164
230
  }
165
231
  } else if (t === 'Y') {
166
- if (this.on_market_data_request_reject) this.on_market_data_request_reject(msg.fields)
232
+ if (channel.on_market_data_request_reject) channel.on_market_data_request_reject(msg.fields)
167
233
  } else if (t === '3' || t === 'j') {
168
- if (this.on_reject) this.on_reject(msg.fields)
234
+ if (channel.on_reject) channel.on_reject(msg.fields)
169
235
  }
170
236
  }
171
- if (md_batch) this.on_market_data_batch(md_batch)
237
+
238
+ if (md_batch && channel.on_market_data_batch) channel.on_market_data_batch(md_batch)
239
+ }
240
+
241
+ _start_heartbeat(channel) {
242
+ this._clear_heartbeat(channel)
243
+ channel.heartbeat_timer = setInterval(() => channel.send('0', []), channel.heartbeat_interval)
172
244
  }
173
245
 
174
- _start_heartbeat() {
175
- this._clear_timers()
176
- this.heartbeat_timer = setInterval(() => this.send('0', []), this.heartbeat_interval)
246
+ _clear_heartbeat(channel) {
247
+ if (channel.heartbeat_timer) {
248
+ clearInterval(channel.heartbeat_timer)
249
+ channel.heartbeat_timer = null
250
+ }
177
251
  }
178
252
 
179
- _clear_timers() {
180
- if (this.heartbeat_timer) { clearInterval(this.heartbeat_timer); this.heartbeat_timer = null }
181
- if (this.reconnect_timer) { clearTimeout(this.reconnect_timer); this.reconnect_timer = null }
253
+ _reset_session_state(channel) {
254
+ channel.recv_buffer = Buffer.alloc(0)
255
+ channel.seq_out = 1
182
256
  }
183
257
 
184
- _reconnect() {
185
- this._clear_timers()
186
- if (this.socket) { this.socket.destroy(); this.socket = null }
187
- this.recv_buffer = Buffer.alloc(0)
188
- this.seq_out = 1
189
- this._set_status('disconnected')
190
- this.reconnect_timer = setTimeout(() => { this.reconnect_timer = null; this.connect() }, 3000)
258
+ _handle_disconnect(channel, options = {}) {
259
+ const restart = options.restart === true
260
+ const next_status = options.next_status || (restart ? 'closed' : 'stopped')
261
+ const socket = channel.socket
262
+ const should_notify = !!socket || channel.session_status !== 'disconnected'
263
+
264
+ if (!socket && !should_notify && channel.status === next_status) return
265
+
266
+ channel.socket = null
267
+
268
+ if (socket && options.graceful) {
269
+ try { this._send_on_socket(channel, socket, '5', []) } catch (e) {}
270
+ }
271
+
272
+ this._clear_heartbeat(channel)
273
+ clearTimeout(channel.reconnecting_timeout)
274
+ channel.reconnecting_timeout = null
275
+ clearTimeout(channel.restart_timeout)
276
+ channel.restart_timeout = null
277
+
278
+ this._reset_session_state(channel)
279
+ channel.status = next_status
280
+ channel.session_status = 'disconnected'
281
+
282
+ if (socket) {
283
+ try { socket.destroy() } catch (e) {}
284
+ }
285
+
286
+ if (should_notify) {
287
+ process.nextTick(() => channel.on_status_change('disconnected'))
288
+ }
289
+
290
+ if (restart && next_status !== 'stopped') {
291
+ channel.restart_timeout = setTimeout(() => {
292
+ channel.restart_timeout = null
293
+ channel.start()
294
+ }, channel.restart_after_close_time)
295
+ }
191
296
  }
192
297
  }
@@ -316,9 +316,7 @@ 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
+ // get_history_trades not implemented, no native absolute-window endpoint
322
320
  static get_min_amount(cb) {
323
321
  let min_quote_cur_amount = {}
324
322
  needle.get('https://exchange.fastex.com/api/v1/pair/list?items_per_page=100&page=1', (err, res, body) => {
@@ -10,6 +10,7 @@ const ExchangeBase = require('./exchange-base.js')
10
10
 
11
11
  const { error_codes } = require('../error_codes.json')
12
12
  const { utils, rate_limiter: Limiter } = require('@icgio/icg-utils')
13
+ const { fetch_split_windows, sort_history_rows } = ExchangeBase.history_utils
13
14
 
14
15
  function pre_process_pair(pair) {
15
16
  let [cur, quote_cur] = utils.parse_pair(pair)
@@ -663,41 +664,55 @@ module.exports = class Gate extends ExchangeBase {
663
664
  }
664
665
  get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
665
666
  let get_history_trades_base = (pair, start_time_in_ms, end_time_in_ms, cb) => {
666
- let params = {
667
- currency_pair: pre_process_pair(pair),
668
- from: parseInt(start_time_in_ms / 1000),
669
- limit: 1000,
670
- to: parseInt(end_time_in_ms / 1000),
671
- }
672
667
  let [api_key, secret_key] = this.api_secret_key
673
- let timestamp = parseInt(Date.now() / 1000)
674
- let path = '/api/v4/spot/my_trades'
675
- let sign = get_signature_gatev4('GET', path, params, secret_key, timestamp)
676
- let options = get_options(api_key, timestamp, sign)
677
- let url = 'https://api.gateio.ws' + path + '?' + qs.stringify(params)
678
- needle.get(url, options, (err, res, body) => {
679
- if (body && Array.isArray(body)) {
680
- let trades = body.map((trade) => {
681
- return {
682
- order_id: trade.order_id,
683
- pair,
684
- type: trade.side,
685
- price: parseFloat(trade.price),
686
- amount: parseFloat(trade.amount),
687
- total: parseFloat(trade.price) * parseFloat(trade.amount),
688
- close_time: new Date(parseInt(trade.create_time_ms)),
689
- fees: {
690
- [post_process_cur(trade.fee_currency)]: parseFloat(trade.fee),
691
- },
692
- }
693
- })
694
-
695
- cb({ success: true, body: trades })
696
- } else if (body) {
697
- cb({ success: false, error: error_codes.other, body })
698
- } else {
699
- cb({ success: false, error: error_codes.timeout })
700
- }
668
+ ;(async () => {
669
+ const rows = await fetch_split_windows({
670
+ start_ms: start_time_in_ms,
671
+ stop_ms: end_time_in_ms,
672
+ initial_window_ms: Math.max(end_time_in_ms - start_time_in_ms, 60 * 1000),
673
+ page_limit: 1000,
674
+ fetch_window: async (window_start_ms, window_stop_ms) =>
675
+ await new Promise((resolve, reject) => {
676
+ let params = {
677
+ currency_pair: pre_process_pair(pair),
678
+ from: parseInt(window_start_ms / 1000),
679
+ limit: 1000,
680
+ to: parseInt(window_stop_ms / 1000),
681
+ }
682
+ let timestamp = parseInt(Date.now() / 1000)
683
+ let path = '/api/v4/spot/my_trades'
684
+ let sign = get_signature_gatev4('GET', path, params, secret_key, timestamp)
685
+ let options = get_options(api_key, timestamp, sign)
686
+ let url = 'https://api.gateio.ws' + path + '?' + qs.stringify(params)
687
+ needle.get(url, options, (err, res, body) => {
688
+ if (body && Array.isArray(body)) {
689
+ resolve(
690
+ body.map((trade) => ({
691
+ order_id: trade.order_id,
692
+ pair,
693
+ type: trade.side,
694
+ price: parseFloat(trade.price),
695
+ amount: parseFloat(trade.amount),
696
+ total: parseFloat(trade.price) * parseFloat(trade.amount),
697
+ close_time: new Date(parseInt(trade.create_time_ms)),
698
+ fees: {
699
+ [post_process_cur(trade.fee_currency)]: parseFloat(trade.fee),
700
+ },
701
+ role: trade.role,
702
+ })),
703
+ )
704
+ } else if (body) {
705
+ reject({ error: error_codes.other, body })
706
+ } else {
707
+ reject({ error: error_codes.timeout })
708
+ }
709
+ })
710
+ }),
711
+ })
712
+ const merged = utils.merge_trades_with_same_id(rows)
713
+ cb({ success: true, body: sort_history_rows(merged) })
714
+ })().catch((error) => {
715
+ cb({ success: false, error: error?.error || error_codes.other, body: error?.body })
701
716
  })
702
717
  }
703
718
  this.private_limiter.process(get_history_trades_base, pair, start_time_in_ms, end_time_in_ms, cb)
@@ -422,6 +422,7 @@ module.exports = class Gemini extends ExchangeBase {
422
422
  await sleep(250)
423
423
  }
424
424
  trades = sort_history_rows(filter_history_in_range(trades, start_time_in_ms, end_time_in_ms))
425
+ trades = utils.merge_trades_with_same_id(trades)
425
426
  cb({ success: true, body: trades })
426
427
  })().catch((error) => cb(error))
427
428
  }
@@ -532,6 +532,7 @@ module.exports = class Hashkey extends ExchangeBase {
532
532
  delay_ms: 250,
533
533
  })
534
534
  trades = sort_history_rows(filter_history_in_range(trades, start_time_in_ms, end_time_in_ms))
535
+ trades = utils.merge_trades_with_same_id(trades)
535
536
  cb({ success: true, body: trades })
536
537
  })().catch((error) => cb(error))
537
538
  }
@@ -520,6 +520,7 @@ module.exports = class Hashkeyglobal extends ExchangeBase {
520
520
  delay_ms: 250,
521
521
  })
522
522
  trades = sort_history_rows(filter_history_in_range(trades, start_time_in_ms, end_time_in_ms))
523
+ trades = utils.merge_trades_with_same_id(trades)
523
524
  cb({ success: true, body: trades })
524
525
  })().catch((error) => cb(error))
525
526
  }
@@ -6,6 +6,7 @@ const needle = require('needle')
6
6
  const ExchangeWs = require('./exchange-ws.js')
7
7
 
8
8
  const ExchangeBase = require('./exchange-base.js')
9
+ const { fetch_split_windows, sort_history_rows } = ExchangeBase.history_utils
9
10
 
10
11
  const { error_codes } = require('../error_codes.json')
11
12
  const { utils, rate_limiter: Limiter } = require('@icgio/icg-utils')
@@ -651,34 +652,50 @@ module.exports = class Hitbtc extends ExchangeBase {
651
652
  })
652
653
  }
653
654
  get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
654
- let from = new Date(start_time_in_ms).toISOString(),
655
- end = new Date(end_time_in_ms).toISOString()
656
- let params = {
657
- from: from,
658
- symbol: pre_process_pair(pair),
659
- till: end,
660
- }
661
- let url = `https://api.hitbtc.com/api/3/spot/history/trade?` + qs.stringify(params)
662
- this.private_limiter.process(needle.get, url, get_signature_hitbtc(this.api_secret_key), (err, res, body) => {
663
- if (body && Array.isArray(body) && body[0]) {
664
- let result = body.map((o) => {
665
- return {
666
- order_id: o.client_order_id.toString(),
667
- pair: post_process_pair(o.symbol),
668
- type: o.side,
669
- price: parseFloat(o.price),
670
- amount: parseFloat(o.quantity),
671
- total: parseFloat(o.price) * parseFloat(o.quantity),
672
- close_time: new Date(o.timestamp),
673
- }
655
+ let get_history_trades_base = (pair, start_time_in_ms, end_time_in_ms, cb) => {
656
+ ;(async () => {
657
+ const rows = await fetch_split_windows({
658
+ start_ms: start_time_in_ms,
659
+ stop_ms: end_time_in_ms,
660
+ initial_window_ms: Math.max(end_time_in_ms - start_time_in_ms, 60 * 1000),
661
+ page_limit: 1000,
662
+ fetch_window: async (window_start_ms, window_stop_ms) =>
663
+ await new Promise((resolve, reject) => {
664
+ let params = {
665
+ from: new Date(window_start_ms).toISOString(),
666
+ symbol: pre_process_pair(pair),
667
+ till: new Date(window_stop_ms).toISOString(),
668
+ limit: 1000,
669
+ }
670
+ let url = `https://api.hitbtc.com/api/3/spot/history/trade?` + qs.stringify(params)
671
+ needle.get(url, get_signature_hitbtc(this.api_secret_key), (err, res, body) => {
672
+ if (body && Array.isArray(body)) {
673
+ resolve(
674
+ body.map((o) => ({
675
+ order_id: o.client_order_id.toString(),
676
+ pair: post_process_pair(o.symbol),
677
+ type: o.side,
678
+ price: parseFloat(o.price),
679
+ amount: parseFloat(o.quantity),
680
+ total: parseFloat(o.price) * parseFloat(o.quantity),
681
+ close_time: new Date(o.timestamp),
682
+ })),
683
+ )
684
+ } else if (body) {
685
+ reject({ error: error_codes.other, body })
686
+ } else {
687
+ reject({ error: error_codes.timeout })
688
+ }
689
+ })
690
+ }),
674
691
  })
675
- cb({ success: true, body: result })
676
- } else if (body) {
677
- cb({ success: false, error: error_codes.other, body })
678
- } else {
679
- cb({ success: false, error: error_codes.timeout })
680
- }
681
- })
692
+ const merged = utils.merge_trades_with_same_id(rows)
693
+ cb({ success: true, body: sort_history_rows(merged) })
694
+ })().catch((error) => {
695
+ cb({ success: false, error: error?.error || error_codes.other, body: error?.body })
696
+ })
697
+ }
698
+ this.private_limiter.process(get_history_trades_base, pair, start_time_in_ms, end_time_in_ms, cb)
682
699
  }
683
700
  // get_all_trades_ws (timeframe_in_ms, cb) {
684
701
  //
@@ -557,9 +557,7 @@ module.exports = class Hkbitex extends ExchangeBase {
557
557
  }
558
558
  this.private_limiter.process(get_trades_base, pair, timeframe_in_ms, cb)
559
559
  }
560
- get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
561
- this._get_history_trades_via_timeframe(pair, start_time_in_ms, end_time_in_ms, cb)
562
- }
560
+ // get_history_trades not implemented, no native absolute-window endpoint
563
561
  static get_precision(cb) {
564
562
  let price_precision = {},
565
563
  amount_precision = {}