@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
@@ -2,9 +2,10 @@ const _ = require('lodash')
2
2
 
3
3
  const qs = require('qs')
4
4
  const websocket = require('ws')
5
- const { createHmac, randomBytes } = require('crypto')
5
+ const { createHmac, randomBytes, randomUUID } = require('crypto')
6
6
  const needle = require('needle')
7
7
 
8
+ const ExchangeFix = require('./exchange-fix.js')
8
9
  const ExchangeWs = require('./exchange-ws.js')
9
10
  const ExchangeBase = require('./exchange-base.js')
10
11
  const { filter_history_in_range, history_time_ms, sleep, sort_history_rows } = ExchangeBase.history_utils
@@ -15,6 +16,8 @@ const { error_codes } = require('../error_codes.json')
15
16
  const { utils, rate_limiter: Limiter } = require('@icgio/icg-utils')
16
17
  const { sign } = require('jsonwebtoken')
17
18
 
19
+ const FIX_SOH = '\x01'
20
+
18
21
  // Detect key type: advanced keys start with "organizations/"
19
22
  function is_advanced_key(api_key) {
20
23
  return typeof api_key === 'string' && api_key.startsWith('organizations/')
@@ -54,6 +57,63 @@ function coinbase_taker_side(side) {
54
57
  return undefined
55
58
  }
56
59
 
60
+ function build_fix_logon_fields(api_key, secret_key, passphrase, seq_num, sending_time, options = {}) {
61
+ const prehash = sending_time + FIX_SOH + 'A' + FIX_SOH + seq_num + FIX_SOH + api_key + FIX_SOH + 'Coinbase' + FIX_SOH + passphrase
62
+ const signature = createHmac('sha256', Buffer.from(secret_key, 'base64')).update(prehash).digest('base64')
63
+ const fields = [
64
+ [553, api_key], [554, passphrase],
65
+ [95, signature.length], [96, signature],
66
+ [98, 0], [108, 30], [141, 'Y'], [1137, 9],
67
+ ]
68
+ if (options.order_entry) fields.push([8013, 'S'])
69
+ return fields
70
+ }
71
+
72
+ function parse_fix_event_ts_ms(fix_time) {
73
+ if (!fix_time || fix_time.length < 17) return undefined
74
+ const iso = fix_time.slice(0, 4) + '-' + fix_time.slice(4, 6) + '-' + fix_time.slice(6, 8) + 'T' + fix_time.slice(9, 21) + 'Z'
75
+ const ms = Date.parse(iso)
76
+ return Number.isFinite(ms) ? ms : undefined
77
+ }
78
+
79
+ function parse_fix_md_entry(fields) {
80
+ const symbol = fields['55']
81
+ const entry_type = fields['269']
82
+ const action = fields['279']
83
+ const md_entry_id = fields['278']
84
+ const order_id = fields['37']
85
+ if (!symbol || entry_type === undefined || action === undefined) return null
86
+
87
+ const price = parseFloat(fields['270'])
88
+ const size = parseFloat(fields['271'])
89
+ if (!Number.isFinite(price)) return null
90
+
91
+ const rpt_seq = fields['83'] ? parseInt(fields['83'], 10) : undefined
92
+ if (entry_type === '2') {
93
+ return {
94
+ kind: 'trade', symbol, price, size,
95
+ time: fields['60'], trade_id: fields['1003'],
96
+ aggressor_side: fields['5797'], rpt_seq,
97
+ }
98
+ }
99
+ if (entry_type !== '0' && entry_type !== '1') return null
100
+ if (!md_entry_id && order_id) return null
101
+
102
+ let action_str
103
+ if (action === '0') action_str = 'new'
104
+ else if (action === '1') action_str = 'change'
105
+ else if (action === '2') action_str = 'delete'
106
+ else return null
107
+
108
+ return {
109
+ kind: 'l2', symbol,
110
+ side: entry_type === '0' ? 'bid' : 'offer',
111
+ action: action_str,
112
+ price, size,
113
+ time: fields['60'], md_entry_id, rpt_seq,
114
+ }
115
+ }
116
+
57
117
  // --- Legacy auth (HMAC + passphrase) ---
58
118
 
59
119
  function get_signature_legacy(api_key, secret_key, timestamp, method, request_path, body) {
@@ -138,18 +198,57 @@ module.exports = class Coinbase extends ExchangeBase {
138
198
  })
139
199
 
140
200
  // FIX protocol — default for legacy keys
141
- this.fix_ord = null
201
+ this.fix = null
142
202
  this._use_fix = false
203
+ this._fix_pending = new Map()
204
+ this._fix_md_pair_list = []
205
+ this._fix_md_symbols = []
206
+ this._fix_md_subscribed = false
207
+ this._fix_md_session_id = 0
143
208
  if (!this.advanced && this.passphrase && !this.settings.no_fix) {
144
- const { CoinbaseFixOrd, CoinbaseFixMd } = require('./coinbase-fix.js')
145
- this._CoinbaseFixMd = CoinbaseFixMd
146
- this.fix_ord = new CoinbaseFixOrd(
147
- Array.isArray(api_key) ? api_key[0] : api_key,
148
- Array.isArray(secret_key) ? secret_key[0] : secret_key,
149
- this.passphrase,
150
- { error_codes, pre_process_pair },
151
- )
152
- this.fix_ord.connect()
209
+ const fix_api_key = Array.isArray(api_key) ? api_key[0] : api_key
210
+ const fix_secret_key = Array.isArray(secret_key) ? secret_key[0] : secret_key
211
+ this.fix = new ExchangeFix(this.name, {
212
+ market: {
213
+ host: 'fix-md.exchange.coinbase.com',
214
+ port: 6121,
215
+ sender_comp_id: fix_api_key,
216
+ target_comp_id: 'Coinbase',
217
+ heartbeat_interval: 30,
218
+ build_logon_fields: (seq, time) => build_fix_logon_fields(fix_api_key, fix_secret_key, this.passphrase, seq, time),
219
+ on_market_data_batch: (batch) => this._handle_fix_market_batch_fields(batch),
220
+ on_market_data_snapshot: (fields) => this._handle_fix_market_fields(fields),
221
+ on_market_data_request_reject: (fields) => {
222
+ console.log('[coinbase FIX md] request reject:', fields['58'] || '')
223
+ },
224
+ on_status_change: (status) => this._on_fix_market_status(status),
225
+ },
226
+ trading: {
227
+ host: 'fix-ord.exchange.coinbase.com',
228
+ port: 6121,
229
+ sender_comp_id: fix_api_key,
230
+ target_comp_id: 'Coinbase',
231
+ heartbeat_interval: 30,
232
+ build_logon_fields: (seq, time) => build_fix_logon_fields(fix_api_key, fix_secret_key, this.passphrase, seq, time, { order_entry: true }),
233
+ on_execution_report: (fields) => this._handle_fix_exec_report(fields),
234
+ on_cancel_reject: (fields) => this._handle_fix_cancel_reject(fields),
235
+ on_reject: (fields) => this._handle_fix_reject(fields),
236
+ on_status_change: (status) => this._on_fix_trading_status(status),
237
+ },
238
+ })
239
+ this.ws.market.end = () => {
240
+ if (this.fix && this.fix.market) this.fix.market.end()
241
+ }
242
+ this.ws.market.close = () => {
243
+ if (this.fix && this.fix.market) this.fix.market.close()
244
+ }
245
+ this.ws.trading.end = () => {
246
+ if (this.fix && this.fix.trading) this.fix.trading.end()
247
+ }
248
+ this.ws.trading.close = () => {
249
+ if (this.fix && this.fix.trading) this.fix.trading.close()
250
+ }
251
+ this.fix.trading.start()
153
252
  this._use_fix = true
154
253
  }
155
254
  }
@@ -580,65 +679,250 @@ module.exports = class Coinbase extends ExchangeBase {
580
679
  this.ws.market.start(pair_list)
581
680
  }
582
681
 
583
- // FIX market data — L3 incremental with REST L3 snapshot bootstrap.
584
- _fix_market_data(pair_list, options = {}) {
585
- this.ws.ws_market_options = options
586
- const OB_WINDOW = options.fix_ob_window || 0.05
587
- const RESNAPSHOT_DRIFT = options.fix_resnapshot_drift || 0.03
588
- const CROSS_GRACE_MS = options.fix_cross_grace_ms || 500
589
- const [api_key, secret_key] = this.api_secret_key
682
+ _fix_trading_ready() {
683
+ return !!(this.fix && this.fix.trading && this.fix.trading.is_ready())
684
+ }
685
+
686
+ _send_fix_new_order(pair, method, price, amount, order_options, cb) {
687
+ if (typeof order_options === 'function') { cb = order_options; order_options = {} }
688
+ const cl_ord_id = randomUUID()
689
+ const side = method === 'buy' ? '1' : '2'
690
+ let tif = '1'
691
+ if (order_options && order_options.ioc) tif = '3'
692
+ else if (order_options && order_options.fok) tif = '4'
693
+ const fields = [
694
+ [11, cl_ord_id], [55, pre_process_pair(pair)], [54, side],
695
+ [44, price.toString()], [38, amount.toString()], [40, 2], [59, tif], [21, 1],
696
+ ]
697
+ if (order_options && order_options.post_only) fields.push([18, 'A'])
698
+ const timeout_id = setTimeout(() => this._resolve_fix_pending(cl_ord_id, { success: false, error: error_codes.timeout }), 15000)
699
+ this._fix_pending.set(cl_ord_id, { cb, timeout_id, type: 'new' })
700
+ this.fix.trading.send('D', fields)
701
+ }
702
+
703
+ _send_fix_cancel(pair, order_id, side, cb) {
704
+ const cl_ord_id = randomUUID()
705
+ const fix_side = side === 'buy' ? '1' : '2'
706
+ const fields = [
707
+ [11, cl_ord_id], [41, cl_ord_id], [37, order_id],
708
+ [55, pre_process_pair(pair)], [54, fix_side],
709
+ ]
710
+ const timeout_id = setTimeout(() => this._resolve_fix_pending(cl_ord_id, { success: false, error: error_codes.timeout }), 15000)
711
+ this._fix_pending.set(cl_ord_id, { cb, timeout_id, type: 'cancel', order_id })
712
+ this.fix.trading.send('F', fields)
713
+ }
714
+
715
+ _on_fix_trading_status(status) {
716
+ console.log('[coinbase FIX ord] status:', status)
717
+ if (status === 'connecting' || status === 'logging_in') {
718
+ this.ws.trading.status = 'opening'
719
+ } else if (status === 'ready') {
720
+ this.ws.trading.status = 'opened'
721
+ } else if (status === 'disconnected') {
722
+ this.ws.trading.status = this.fix && this.fix.trading && this.fix.trading.status === 'stopped' ? 'stopped' : 'closed'
723
+ this._flush_fix_pending()
724
+ }
725
+ }
726
+
727
+ _handle_fix_exec_report(fields) {
728
+ const cl_ord_id = fields['11']
729
+ const ord_status = fields['39']
730
+ const order_id = fields['37'] || ''
731
+ if (ord_status === '0') {
732
+ this._resolve_fix_pending(cl_ord_id, { success: true, body: order_id })
733
+ } else if (ord_status === '8') {
734
+ const text = fields['58'] || ''
735
+ let error = error_codes.other
736
+ if (text.includes('nsufficient') || text.includes('NSUFFICIENT')) error = error_codes.insufficient_funds
737
+ else if (fields['103'] === '2' || text.includes('too small') || text.includes('TOO_SMALL')) error = error_codes.amount_too_small
738
+ this._resolve_fix_pending(cl_ord_id, { success: false, error, body: text })
739
+ } else if (ord_status === '4') {
740
+ if (this._fix_pending.has(cl_ord_id)) this._resolve_fix_pending(cl_ord_id, { success: true, body: order_id })
741
+ else this._resolve_fix_pending_by_order_id(order_id, { success: true, body: order_id })
742
+ } else if (ord_status === '1' || ord_status === '2') {
743
+ if (this._fix_pending.has(cl_ord_id) && this._fix_pending.get(cl_ord_id).type === 'cancel') {
744
+ this._resolve_fix_pending(cl_ord_id, { success: false, error: error_codes.order_not_open, body: order_id })
745
+ } else {
746
+ this._resolve_fix_pending_by_order_id(order_id, { success: false, error: error_codes.order_not_open, body: order_id }, 'cancel')
747
+ }
748
+ }
749
+ }
750
+
751
+ _handle_fix_cancel_reject(fields) {
752
+ let error = error_codes.other
753
+ if (fields['102'] === '1') error = error_codes.order_not_open
754
+ this._resolve_fix_pending(fields['11'], { success: false, error, body: fields['58'] || '' })
755
+ }
756
+
757
+ _handle_fix_reject(fields) {
758
+ const cl_ord_id = fields['379'] || fields['11'] || ''
759
+ if (cl_ord_id && this._fix_pending.has(cl_ord_id)) {
760
+ this._resolve_fix_pending(cl_ord_id, { success: false, error: error_codes.other, body: fields['58'] || '' })
761
+ }
762
+ }
763
+
764
+ _resolve_fix_pending(cl_ord_id, result) {
765
+ const pending = this._fix_pending.get(cl_ord_id)
766
+ if (!pending) return
767
+ clearTimeout(pending.timeout_id)
768
+ this._fix_pending.delete(cl_ord_id)
769
+ pending.cb(result)
770
+ }
771
+
772
+ _resolve_fix_pending_by_order_id(order_id, result, type_filter) {
773
+ for (const [id, pending] of this._fix_pending) {
774
+ if (pending.order_id === order_id && (!type_filter || pending.type === type_filter)) {
775
+ this._resolve_fix_pending(id, result)
776
+ return
777
+ }
778
+ }
779
+ }
780
+
781
+ _flush_fix_pending() {
782
+ for (const [id, pending] of this._fix_pending) {
783
+ clearTimeout(pending.timeout_id)
784
+ pending.cb({ success: false, error: error_codes.timeout })
785
+ }
786
+ this._fix_pending.clear()
787
+ }
590
788
 
789
+ _prepare_fix_market_session() {
790
+ this._fix_md_session_id++
791
+ this._fix_md_subscribed = false
591
792
  this._fix_md_orders = {}
592
793
  this._fix_md_snapshot_mid = {}
593
794
  this._fix_md_snapshot_seq = {}
594
795
  this._fix_md_event_buffer = {}
595
796
  this._fix_md_pair_by_symbol = {}
596
797
  this._fix_md_cross_pending = {}
597
- this._fix_md_cross_grace_ms = CROSS_GRACE_MS
798
+ this._fix_md_resnapshot_pending = {}
799
+ this._fix_md_resnapshot_buffer = {}
800
+ this._fix_md_notify_pending = false
801
+ this.ws.market.status = 'opening'
802
+ this.ws.market.last_message_time = undefined
803
+
804
+ for (const pair of this._fix_md_pair_list) {
805
+ this.ws.market.pair_status[pair] = 'opening'
806
+ this.ws.market.asks_dict[pair] = sorted_book(true)
807
+ this.ws.market.bids_dict[pair] = sorted_book(false)
808
+ this.ws.market.market_history_dict[pair] = []
809
+ delete this.ws.market.market_ts_by_pair[pair]
810
+ delete this.ws.market.tickers[pair]
811
+ this._fix_md_orders[pair] = {}
812
+ this._fix_md_snapshot_mid[pair] = null
813
+ this._fix_md_snapshot_seq[pair] = null
814
+ this._fix_md_event_buffer[pair] = []
815
+ this._fix_md_pair_by_symbol[pre_process_pair(pair)] = pair
816
+ }
817
+ }
598
818
 
599
- this.ws.market.start_base = () => {
600
- pair_list.map((pair) => {
601
- this.ws.market.pair_status[pair] = 'opening'
602
- this.ws.market.asks_dict[pair] = sorted_book(true)
603
- this.ws.market.bids_dict[pair] = sorted_book(false)
604
- this.ws.market.market_history_dict[pair] = []
605
- this._fix_md_orders[pair] = {}
606
- this._fix_md_snapshot_seq[pair] = null
607
- this._fix_md_event_buffer[pair] = []
608
- this._fix_md_pair_by_symbol[pre_process_pair(pair)] = pair
609
- })
819
+ _send_fix_market_subscribe() {
820
+ if (!this.fix || !this.fix.market || !this._fix_md_symbols.length) return
821
+ const fields = [
822
+ [262, 'icg-md-' + Date.now()], [263, 1], [264, 0], [265, 1],
823
+ [267, 3], [269, 0], [269, 1], [269, 2],
824
+ [146, this._fix_md_symbols.length],
825
+ ]
826
+ for (const symbol of this._fix_md_symbols) fields.push([55, symbol])
827
+ if (this.fix.market.send('V', fields)) {
828
+ this._fix_md_subscribed = true
829
+ console.log('[coinbase FIX md] subscribed:', this._fix_md_symbols.join(','))
830
+ }
831
+ }
610
832
 
611
- const symbols = pair_list.map((pair) => pre_process_pair(pair))
612
-
613
- this.fix_md = new this._CoinbaseFixMd({
614
- api_key: Array.isArray(api_key) ? api_key[0] : api_key,
615
- secret_key: Array.isArray(secret_key) ? secret_key[0] : secret_key,
616
- passphrase: this.passphrase,
617
- symbols,
618
- on_status: (status) => {
619
- console.log('[coinbase FIX MD] status:', status)
620
- if (status === 'ready') {
621
- this.ws.market.status = 'opened'
622
- this.ws.market.last_message_time = Date.now()
623
- for (const pair of pair_list) this._fix_md_load_snapshot(pair, OB_WINDOW, RESNAPSHOT_DRIFT)
624
- }
625
- },
626
- on_batch: (entries) => this._fix_md_handle_batch(entries, OB_WINDOW, RESNAPSHOT_DRIFT),
627
- })
628
- this.fix_md.connect()
833
+ _on_fix_market_status(status) {
834
+ console.log('[coinbase FIX md] status:', status)
835
+ if (!this._fix_md_pair_list.length) return
836
+
837
+ if (status === 'connecting') {
838
+ this._prepare_fix_market_session()
839
+ return
629
840
  }
630
- this.ws.market.end_base = () => {
631
- if (this.fix_md) {
632
- try { this.fix_md.disconnect() } catch (e) {}
633
- this.fix_md = null
841
+ if (status === 'logging_in') {
842
+ this.ws.market.status = 'opening'
843
+ return
844
+ }
845
+ if (status === 'ready') {
846
+ if (!this._fix_md_subscribed) this._send_fix_market_subscribe()
847
+ this.ws.market.status = 'opened'
848
+ this.ws.market.last_message_time = Date.now()
849
+ const session_id = this._fix_md_session_id
850
+ for (const pair of this._fix_md_pair_list) {
851
+ this._fix_md_load_snapshot(pair, this._fix_md_ob_window, this._fix_md_resnapshot_drift, session_id)
852
+ }
853
+ return
854
+ }
855
+ if (status === 'disconnected') {
856
+ const final_status = this.fix && this.fix.market && this.fix.market.status === 'stopped' ? 'stopped' : 'closed'
857
+ this._fix_md_session_id++
858
+ this._fix_md_subscribed = false
859
+ this.ws.market.status = final_status
860
+ this.ws.market.last_message_time = undefined
861
+ for (const pair of this._fix_md_pair_list) this.ws.market.pair_status[pair] = final_status
862
+ if (final_status === 'stopped') {
863
+ clearInterval(this.ws.market.notify_heartbeat_session)
864
+ clearInterval(this.ws.market.stale_check_session)
865
+ clearInterval(this.ws.market.ping_session)
866
+ clearTimeout(this.ws.market.restart_timeout)
867
+ clearTimeout(this.ws.market.reconnecting_timeout)
634
868
  }
635
- this._fix_md_orders = {}
636
- this._fix_md_snapshot_mid = {}
637
- this._fix_md_snapshot_seq = {}
638
- this._fix_md_event_buffer = {}
639
- this._fix_md_cross_pending = {}
640
869
  }
641
- this.ws.market.start(pair_list)
870
+ }
871
+
872
+ _handle_fix_market_fields(fields) {
873
+ const hr_recv = process.hrtime()
874
+ const entry = parse_fix_md_entry(fields)
875
+ if (!entry) return
876
+ const hr_parsed = process.hrtime()
877
+ entry.ts_ms_venue_event = parse_fix_event_ts_ms(entry.time)
878
+ entry.ts_ms_local_received = Date.now()
879
+ entry.hr_recv = hr_recv
880
+ entry.hr_parsed = hr_parsed
881
+ this._fix_md_handle_batch([entry], this._fix_md_ob_window, this._fix_md_resnapshot_drift)
882
+ }
883
+
884
+ _handle_fix_market_batch_fields(batch) {
885
+ const entries = []
886
+ const now = Date.now()
887
+ for (const fields of batch) {
888
+ const hr_recv = process.hrtime()
889
+ const entry = parse_fix_md_entry(fields)
890
+ if (!entry) continue
891
+ const hr_parsed = process.hrtime()
892
+ entry.ts_ms_venue_event = parse_fix_event_ts_ms(entry.time)
893
+ entry.ts_ms_local_received = now
894
+ entry.hr_recv = hr_recv
895
+ entry.hr_parsed = hr_parsed
896
+ entries.push(entry)
897
+ }
898
+ if (entries.length) this._fix_md_handle_batch(entries, this._fix_md_ob_window, this._fix_md_resnapshot_drift)
899
+ }
900
+
901
+ // FIX market data — L3 incremental with REST L3 snapshot bootstrap.
902
+ _fix_market_data(pair_list, options = {}) {
903
+ if (!this.fix || !this.fix.market) return
904
+ if (this.fix.market.status === 'opening' || this.fix.market.status === 'opened') return
905
+
906
+ this.ws.ws_market_options = options
907
+ this._fix_md_ob_window = options.fix_ob_window || 0.05
908
+ this._fix_md_resnapshot_drift = options.fix_resnapshot_drift || 0.03
909
+ this._fix_md_cross_grace_ms = options.fix_cross_grace_ms || 500
910
+ this._fix_md_pair_list = pair_list.slice()
911
+ this._fix_md_symbols = pair_list.map((pair) => pre_process_pair(pair))
912
+
913
+ clearInterval(this.ws.market.ping_session)
914
+ clearInterval(this.ws.market.notify_heartbeat_session)
915
+ clearInterval(this.ws.market.stale_check_session)
916
+ clearTimeout(this.ws.market.restart_timeout)
917
+ clearTimeout(this.ws.market.reconnecting_timeout)
918
+
919
+ this.ws.market.clear()
920
+ this.ws.market.pair_list = pair_list.slice()
921
+ this.ws.market.status = 'closed'
922
+ pair_list.map((pair) => (this.ws.market.pair_status[pair] = 'closed'))
923
+ this.ws.market.start_notify_heartbeat()
924
+ this.ws.market.start_stale_check()
925
+ this.fix.market.start()
642
926
  }
643
927
 
644
928
  // Build book from L3 snapshot, apply to pair, replay buffered events.
@@ -709,11 +993,12 @@ module.exports = class Coinbase extends ExchangeBase {
709
993
  return snap_seq
710
994
  }
711
995
 
712
- _fix_md_load_snapshot(pair, OB_WINDOW, RESNAPSHOT_DRIFT) {
996
+ _fix_md_load_snapshot(pair, OB_WINDOW, RESNAPSHOT_DRIFT, session_id = this._fix_md_session_id) {
713
997
  this._fix_md_fetch_snapshot(pair, (res) => {
998
+ if (session_id !== this._fix_md_session_id) return
714
999
  if (!res.success) {
715
1000
  console.log('[coinbase FIX MD] snapshot fetch failed for %s, retrying in 1s', pair)
716
- setTimeout(() => this._fix_md_load_snapshot(pair, OB_WINDOW, RESNAPSHOT_DRIFT), 1000)
1001
+ setTimeout(() => this._fix_md_load_snapshot(pair, OB_WINDOW, RESNAPSHOT_DRIFT, session_id), 1000)
717
1002
  return
718
1003
  }
719
1004
  const buffered = this._fix_md_event_buffer[pair] || []
@@ -723,11 +1008,12 @@ module.exports = class Coinbase extends ExchangeBase {
723
1008
  }
724
1009
 
725
1010
  // Background resnapshot: old book stays live while new one is built, then atomic swap.
726
- _fix_md_resnapshot(pair, OB_WINDOW, RESNAPSHOT_DRIFT) {
1011
+ _fix_md_resnapshot(pair, OB_WINDOW, RESNAPSHOT_DRIFT, session_id = this._fix_md_session_id) {
727
1012
  this._fix_md_fetch_snapshot(pair, (res) => {
1013
+ if (session_id !== this._fix_md_session_id) return
728
1014
  if (!res.success) {
729
1015
  console.log('[coinbase FIX MD] resnapshot fetch failed for %s, retrying in 1s', pair)
730
- setTimeout(() => this._fix_md_resnapshot(pair, OB_WINDOW, RESNAPSHOT_DRIFT), 1000)
1016
+ setTimeout(() => this._fix_md_resnapshot(pair, OB_WINDOW, RESNAPSHOT_DRIFT, session_id), 1000)
731
1017
  return
732
1018
  }
733
1019
  const buffered = this._fix_md_resnapshot_buffer[pair] || []
@@ -931,7 +1217,7 @@ module.exports = class Coinbase extends ExchangeBase {
931
1217
  this._fix_md_resnapshot_buffer[pair] = []
932
1218
  console.log('[coinbase FIX MD] %s: %s snap_mid=%s cur_mid=%s bid=%s ask=%s, refetching',
933
1219
  reason, pair, snap_mid.toFixed(2), cur_mid.toFixed(2), best_bid, best_ask)
934
- this._fix_md_resnapshot(pair, OB_WINDOW, RESNAPSHOT_DRIFT)
1220
+ this._fix_md_resnapshot(pair, OB_WINDOW, RESNAPSHOT_DRIFT, this._fix_md_session_id)
935
1221
  }
936
1222
 
937
1223
  _fix_md_update_bbo(pair, OB_WINDOW, RESNAPSHOT_DRIFT, options = {}) {
@@ -987,7 +1273,7 @@ module.exports = class Coinbase extends ExchangeBase {
987
1273
  }
988
1274
 
989
1275
  ws_status() {
990
- return { market: this.ws.market.pair_status, trading: 'n-opened' }
1276
+ return { market: this.ws.market.pair_status, trading: this.ws.trading.status }
991
1277
  }
992
1278
  rate_ws(pair, cb) {
993
1279
  if (this.ws.market.pair_status[pair] === 'opened') {
@@ -1106,8 +1392,8 @@ module.exports = class Coinbase extends ExchangeBase {
1106
1392
  cb({ success: false, error: error_codes.amount_too_small })
1107
1393
  return
1108
1394
  }
1109
- if (this.fix_ord && this.fix_ord.is_ready()) {
1110
- this.fix_ord.send_new_order(pair, method, price, amount, order_options, cb)
1395
+ if (this._fix_trading_ready()) {
1396
+ this._send_fix_new_order(pair, method, price, amount, order_options, cb)
1111
1397
  return
1112
1398
  }
1113
1399
  if (this.advanced) {
@@ -1173,13 +1459,13 @@ module.exports = class Coinbase extends ExchangeBase {
1173
1459
  cb({ success: false, error: error_codes.amount_too_small })
1174
1460
  return
1175
1461
  }
1176
- if (this.fix_ord && this.fix_ord.is_ready()) {
1462
+ if (this._fix_trading_ready()) {
1177
1463
  const normalized = this.normalize_order_options(order_options)
1178
1464
  const t_order_sent = process.hrtime()
1179
1465
  if (normalized.on_sent) {
1180
1466
  normalized.on_sent({ t_order_sent })
1181
1467
  }
1182
- this.fix_ord.send_new_order(pair, method, price, amount, normalized.order_options, (res = {}) => {
1468
+ this._send_fix_new_order(pair, method, price, amount, normalized.order_options, (res = {}) => {
1183
1469
  res.meta = {
1184
1470
  ...(res.meta || {}),
1185
1471
  t_order_sent,
@@ -1230,9 +1516,9 @@ module.exports = class Coinbase extends ExchangeBase {
1230
1516
  }
1231
1517
  }
1232
1518
  cancel_order_by_order(order, cb) {
1233
- if (this.fix_ord && this.fix_ord.is_ready()) {
1519
+ if (this._fix_trading_ready()) {
1234
1520
  const t_cancel_sent = process.hrtime()
1235
- this.fix_ord.send_cancel(order.pair, order.order_id, order.type, (res = {}) => {
1521
+ this._send_fix_cancel(order.pair, order.order_id, order.type, (res = {}) => {
1236
1522
  res.meta = {
1237
1523
  ...(res.meta || {}),
1238
1524
  t_cancel_sent,
@@ -1708,7 +1994,9 @@ module.exports = class Coinbase extends ExchangeBase {
1708
1994
  await sleep(50)
1709
1995
  }
1710
1996
  }
1711
- cb({ success: true, body: sort_history_rows(filter_history_in_range(results, start_time_in_ms, end_time_in_ms)) })
1997
+ let trades = sort_history_rows(filter_history_in_range(results, start_time_in_ms, end_time_in_ms))
1998
+ trades = utils.merge_trades_with_same_id(trades)
1999
+ cb({ success: true, body: trades })
1712
2000
  } catch (error) {
1713
2001
  cb({ success: false, error: error?.error || error_codes.other, body: error?.body })
1714
2002
  }
@@ -477,9 +477,7 @@ module.exports = class Coinstore extends ExchangeBase {
477
477
  }
478
478
  this.private_limiter.process(get_trades_base, pair, timeframe_in_ms, cb)
479
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
- }
480
+ // get_history_trades not implemented, no native absolute-window endpoint
483
481
  static get_precision(cb) {
484
482
  const price_precision = {}
485
483
  const amount_precision = {}
@@ -7,6 +7,7 @@ const needle = require('needle')
7
7
  const ExchangeWs = require('./exchange-ws.js')
8
8
 
9
9
  const ExchangeBase = require('./exchange-base.js')
10
+ const { fetch_split_windows, sort_history_rows } = ExchangeBase.history_utils
10
11
 
11
12
  const { error_codes } = require('../error_codes.json')
12
13
  const { utils, rate_limiter: Limiter } = require('@icgio/icg-utils')
@@ -506,38 +507,53 @@ module.exports = class Coinw extends ExchangeBase {
506
507
  get_history_trades(pair, start_time_in_ms, end_time_in_ms, cb) {
507
508
  const [api_key, secret_key] = this.api_secret_key
508
509
  const get_trades_base = (pair, start_time_in_ms, end_time_in_ms, cb) => {
509
- const params = {
510
- api_key,
511
- currencyPair: pre_process_pair(pair),
512
- endAt: end_time_in_ms,
513
- startAt: start_time_in_ms,
514
- }
515
- params.sign = get_signature_coinw(secret_key, params)
516
- needle.post('https://api.coinw.com/api/v1/private?command=returnUTradeHistory', params, options, (_err, _res, body) => {
517
- body = parse_body(body)
518
- if (body && body.code === '200') {
519
- if (!body.data) {
520
- cb({ success: true, body: [] })
521
- } else {
522
- let trades = body.data
523
- .filter((t) => t.status === '2' || t.status === '3')
524
- .map((trade) => ({
525
- order_id: trade.tradeID.toString(),
526
- pair,
527
- type: trade.type,
528
- price: parseFloat(trade.prize), // exchange typo
529
- amount: parseFloat(trade.success_count),
530
- total: parseFloat(trade.success_amount),
531
- close_time: new Date(parseInt(trade.date)),
532
- }))
533
- trades = utils.merge_trades_with_same_id(trades)
534
- cb({ success: true, body: trades })
535
- }
536
- } else if (body) {
537
- cb({ success: false, error: error_codes.other, body })
538
- } else {
539
- cb({ success: false, error: error_codes.timeout })
540
- }
510
+ ;(async () => {
511
+ const rows = await fetch_split_windows({
512
+ start_ms: start_time_in_ms,
513
+ stop_ms: end_time_in_ms,
514
+ initial_window_ms: Math.max(end_time_in_ms - start_time_in_ms, 60 * 1000),
515
+ page_limit: 100,
516
+ fetch_window: async (window_start_ms, window_stop_ms) =>
517
+ await new Promise((resolve, reject) => {
518
+ const params = {
519
+ api_key,
520
+ currencyPair: pre_process_pair(pair),
521
+ endAt: window_stop_ms,
522
+ startAt: window_start_ms,
523
+ }
524
+ params.sign = get_signature_coinw(secret_key, params)
525
+ needle.post('https://api.coinw.com/api/v1/private?command=returnUTradeHistory', params, options, (_err, _res, body) => {
526
+ body = parse_body(body)
527
+ if (body && body.code === '200') {
528
+ if (!body.data) {
529
+ resolve([])
530
+ } else {
531
+ resolve(
532
+ body.data
533
+ .filter((t) => t.status === '2' || t.status === '3')
534
+ .map((trade) => ({
535
+ order_id: trade.tradeID.toString(),
536
+ pair,
537
+ type: trade.type,
538
+ price: parseFloat(trade.prize), // exchange typo
539
+ amount: parseFloat(trade.success_count),
540
+ total: parseFloat(trade.success_amount),
541
+ close_time: new Date(parseInt(trade.date)),
542
+ })),
543
+ )
544
+ }
545
+ } else if (body) {
546
+ reject({ error: error_codes.other, body })
547
+ } else {
548
+ reject({ error: error_codes.timeout })
549
+ }
550
+ })
551
+ }),
552
+ })
553
+ const merged = utils.merge_trades_with_same_id(rows)
554
+ cb({ success: true, body: sort_history_rows(merged) })
555
+ })().catch((error) => {
556
+ cb({ success: false, error: error?.error || error_codes.other, body: error?.body })
541
557
  })
542
558
  }
543
559
  this.private_limiter.process(get_trades_base, pair, start_time_in_ms, end_time_in_ms, cb)
@@ -558,6 +558,7 @@ module.exports = class Cryptocom extends ExchangeBase {
558
558
  delay_ms: 250,
559
559
  })
560
560
  trades = sort_history_rows(filter_history_in_range(trades, start_time_in_ms, end_time_in_ms))
561
+ trades = utils.merge_trades_with_same_id(trades)
561
562
  cb({ success: true, body: trades })
562
563
  })().catch((error) => cb(error))
563
564
  }
@@ -528,6 +528,7 @@ module.exports = class Deepcoin extends ExchangeBase {
528
528
  delay_ms: 250,
529
529
  })
530
530
  trades = sort_history_rows(filter_history_in_range(trades, start_time_in_ms, end_time_in_ms))
531
+ trades = utils.merge_trades_with_same_id(trades)
531
532
  cb({ success: true, body: trades })
532
533
  })().catch((error) => cb(error))
533
534
  }