@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.
- package/lib/exchanges/ascendex.js +1 -3
- package/lib/exchanges/biconomy.js +2 -2
- package/lib/exchanges/binance.js +1 -0
- package/lib/exchanges/bingx.js +2 -1
- package/lib/exchanges/bitget.js +1 -0
- package/lib/exchanges/bithumb.js +1 -3
- package/lib/exchanges/bitkub.js +1 -3
- package/lib/exchanges/bitmart.js +49 -32
- package/lib/exchanges/bitmex.js +1 -0
- package/lib/exchanges/bitrue.js +2 -1
- package/lib/exchanges/bitstamp.js +1 -0
- package/lib/exchanges/blofin.js +1 -0
- package/lib/exchanges/btse.js +1 -3
- package/lib/exchanges/bybit.js +48 -35
- package/lib/exchanges/coinbase.js +359 -71
- package/lib/exchanges/coinstore.js +1 -3
- package/lib/exchanges/coinw.js +48 -32
- package/lib/exchanges/cryptocom.js +1 -0
- package/lib/exchanges/deepcoin.js +1 -0
- package/lib/exchanges/digifinex.js +1 -0
- package/lib/exchanges/exchange-base.js +12 -46
- package/lib/exchanges/exchange-fix.js +191 -86
- package/lib/exchanges/fastex.js +1 -3
- package/lib/exchanges/gate.js +49 -34
- package/lib/exchanges/gemini.js +1 -0
- package/lib/exchanges/hashkey.js +1 -0
- package/lib/exchanges/hashkeyglobal.js +1 -0
- package/lib/exchanges/hitbtc.js +44 -27
- package/lib/exchanges/hkbitex.js +1 -3
- package/lib/exchanges/htx.js +4 -3
- package/lib/exchanges/indodax.js +1 -3
- package/lib/exchanges/kraken.js +1 -0
- package/lib/exchanges/kucoin.js +2 -1
- package/lib/exchanges/lbank.js +2 -1
- package/lib/exchanges/mexc.js +2 -1
- package/lib/exchanges/okx.js +51 -34
- package/lib/exchanges/phemex.js +47 -31
- package/lib/exchanges/poloniex.js +43 -27
- package/lib/exchanges/swft.js +1 -3
- package/lib/exchanges/upbit.js +1 -0
- package/lib/exchanges/weex.js +1 -3
- package/lib/exchanges/xt.js +2 -1
- package/package.json +1 -1
- 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.
|
|
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
|
|
145
|
-
|
|
146
|
-
this.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const
|
|
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.
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
1110
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
1519
|
+
if (this._fix_trading_ready()) {
|
|
1234
1520
|
const t_cancel_sent = process.hrtime()
|
|
1235
|
-
this.
|
|
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
|
-
|
|
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
|
|
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 = {}
|
package/lib/exchanges/coinw.js
CHANGED
|
@@ -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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
.
|
|
524
|
-
.
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
}
|