@icgio/icg-exchanges-wrapper 1.14.256 → 1.14.258
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.
|
@@ -4,7 +4,111 @@ const DEFAULT_ORDERBOOK_LEVEL = 50
|
|
|
4
4
|
|
|
5
5
|
let first_time = {},
|
|
6
6
|
exchange_pair_rates = {},
|
|
7
|
-
pair_exchange_rates = {}
|
|
7
|
+
pair_exchange_rates = {},
|
|
8
|
+
// Track active WebSocket connections per exchange to avoid duplicates
|
|
9
|
+
ws_connection_promises = {},
|
|
10
|
+
ws_connection_pairs = {},
|
|
11
|
+
// Track wrapped promises with fallback handlers to avoid duplicate handlers
|
|
12
|
+
ws_connection_promises_with_fallback = {},
|
|
13
|
+
// Track persistent WebSocket failures per pair to avoid repeated retries
|
|
14
|
+
ws_failed_pairs = {}, // { "ExchangeName": { "PAIR": { count: number, last_failed: timestamp } } }
|
|
15
|
+
WS_FAILURE_THRESHOLD = 3, // Skip WebSocket after 3 consecutive failures
|
|
16
|
+
WS_RETRY_INTERVAL_MS = 5 * 60 * 1000 // Retry WebSocket after 5 minutes
|
|
17
|
+
|
|
18
|
+
// ============ HELPER FUNCTIONS ============
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Group exchanges by name and merge all pairs
|
|
22
|
+
* Returns: { exchange_instances: {}, exchange_all_pairs: {} }
|
|
23
|
+
*/
|
|
24
|
+
function group_exchanges_by_name(Exchanges, exchange_pair_dict) {
|
|
25
|
+
let exchange_instances = {}
|
|
26
|
+
let exchange_all_pairs = {}
|
|
27
|
+
for (let Exchange of Exchanges) {
|
|
28
|
+
let Exchange_name = _.upperFirst(Exchange.name)
|
|
29
|
+
if (!exchange_pair_dict[Exchange_name]) {
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
if (!exchange_instances[Exchange_name]) {
|
|
33
|
+
exchange_instances[Exchange_name] = Exchange
|
|
34
|
+
exchange_all_pairs[Exchange_name] = []
|
|
35
|
+
}
|
|
36
|
+
exchange_all_pairs[Exchange_name] = _.union(exchange_all_pairs[Exchange_name], exchange_pair_dict[Exchange_name])
|
|
37
|
+
}
|
|
38
|
+
return { exchange_instances, exchange_all_pairs }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Set data in both exchange_pair and pair_exchange structures
|
|
43
|
+
*/
|
|
44
|
+
function set_pair_data(exchange_pair_dict, pair_exchange_dict, exchange_name, pair, data) {
|
|
45
|
+
_.set(exchange_pair_dict, [exchange_name, pair], data)
|
|
46
|
+
_.set(pair_exchange_dict, [pair, exchange_name], data)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Delete data from both exchange_pair and pair_exchange structures
|
|
51
|
+
*/
|
|
52
|
+
function delete_pair_data(exchange_pair_dict, pair_exchange_dict, exchange_name, pair) {
|
|
53
|
+
if (exchange_pair_dict[exchange_name]) {
|
|
54
|
+
delete exchange_pair_dict[exchange_name][pair]
|
|
55
|
+
}
|
|
56
|
+
if (pair_exchange_dict[pair]) {
|
|
57
|
+
delete pair_exchange_dict[pair][exchange_name]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Track WebSocket failure for a pair
|
|
63
|
+
*/
|
|
64
|
+
function track_ws_failure(failed_pairs_dict, exchange_name, pair) {
|
|
65
|
+
if (!failed_pairs_dict[exchange_name]) {
|
|
66
|
+
failed_pairs_dict[exchange_name] = {}
|
|
67
|
+
}
|
|
68
|
+
if (!failed_pairs_dict[exchange_name][pair]) {
|
|
69
|
+
failed_pairs_dict[exchange_name][pair] = { count: 0, last_failed: 0 }
|
|
70
|
+
}
|
|
71
|
+
failed_pairs_dict[exchange_name][pair].count++
|
|
72
|
+
failed_pairs_dict[exchange_name][pair].last_failed = Date.now()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Reset WebSocket failure count for a pair (on success)
|
|
77
|
+
*/
|
|
78
|
+
function reset_ws_failure(failed_pairs_dict, exchange_name, pair) {
|
|
79
|
+
if (failed_pairs_dict[exchange_name] && failed_pairs_dict[exchange_name][pair]) {
|
|
80
|
+
delete failed_pairs_dict[exchange_name][pair]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if pair should skip WebSocket due to persistent failures
|
|
86
|
+
*/
|
|
87
|
+
function should_skip_websocket(failed_pairs_dict, exchange_name, pair) {
|
|
88
|
+
const failure_info = failed_pairs_dict[exchange_name]?.[pair]
|
|
89
|
+
return (
|
|
90
|
+
failure_info &&
|
|
91
|
+
failure_info.count >= WS_FAILURE_THRESHOLD &&
|
|
92
|
+
(Date.now() - failure_info.last_failed) < WS_RETRY_INTERVAL_MS
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create WebSocket timeout checker
|
|
98
|
+
*/
|
|
99
|
+
function create_ws_timeout_checker(Exchange_name, timeout_ms, max_checks, on_timeout) {
|
|
100
|
+
const start_time = Date.now()
|
|
101
|
+
let check_count = 0
|
|
102
|
+
return () => {
|
|
103
|
+
check_count++
|
|
104
|
+
const elapsed = Date.now() - start_time
|
|
105
|
+
if (elapsed > timeout_ms || check_count > max_checks) {
|
|
106
|
+
on_timeout(elapsed)
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
}
|
|
8
112
|
|
|
9
113
|
const get_exchanges_rates = function (Exchanges, exchange_pair_dict, cb, ws_enabled = true) {
|
|
10
114
|
let checked_rates = {}
|
|
@@ -14,38 +118,76 @@ const get_exchanges_rates = function (Exchanges, exchange_pair_dict, cb, ws_enab
|
|
|
14
118
|
console.error('Exchanges should be an array')
|
|
15
119
|
return
|
|
16
120
|
} else {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
121
|
+
// First pass: group pairs by exchange name and find the Exchange instance
|
|
122
|
+
// This ensures ALL pairs for each exchange are collected BEFORE creating any WebSocket
|
|
123
|
+
const { exchange_instances, exchange_all_pairs } = group_exchanges_by_name(Exchanges, exchange_pair_dict)
|
|
124
|
+
|
|
125
|
+
// Second pass: process each unique exchange once
|
|
126
|
+
// Now we have all pairs collected, create ONE WebSocket per exchange with ALL pairs
|
|
127
|
+
for (let Exchange_name in exchange_instances) {
|
|
128
|
+
let Exchange = exchange_instances[Exchange_name]
|
|
129
|
+
let all_pairs = exchange_all_pairs[Exchange_name]
|
|
130
|
+
|
|
131
|
+
// Log how many pairs will be used for this exchange
|
|
132
|
+
if (all_pairs.length > 0 && Exchange.ws_market && ws_enabled) {
|
|
133
|
+
console.log(`[WebSocket Rates] ${Exchange_name}: Creating connection with ${all_pairs.length} pair(s): ${all_pairs.join(', ')}`)
|
|
25
134
|
}
|
|
135
|
+
|
|
26
136
|
if (Exchange.ws_market && ws_enabled) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
137
|
+
// Check if WebSocket is already opening/opened for this exchange
|
|
138
|
+
const ws_key = Exchange_name
|
|
139
|
+
const ws_status = Exchange.ws?.market?.status
|
|
140
|
+
const is_opening_or_opened = ws_status === 'opening' || ws_status === 'opened'
|
|
141
|
+
|
|
142
|
+
let ws_promise
|
|
143
|
+
let ws_promise_with_fallback
|
|
144
|
+
if (is_opening_or_opened && ws_connection_promises[ws_key]) {
|
|
145
|
+
// Reuse existing WebSocket connection promise
|
|
146
|
+
ws_promise = ws_connection_promises[ws_key]
|
|
147
|
+
// Merge pairs if needed
|
|
148
|
+
const existing_pairs = ws_connection_pairs[ws_key] || []
|
|
149
|
+
const new_pairs = _.difference(all_pairs, existing_pairs)
|
|
150
|
+
if (new_pairs.length > 0) {
|
|
151
|
+
ws_connection_pairs[ws_key] = _.union(existing_pairs, all_pairs)
|
|
152
|
+
// Add new pairs to existing connection if exchange supports it
|
|
153
|
+
// Note: This depends on exchange implementation - some may need to restart
|
|
154
|
+
// For now, we'll wait for existing connection and check if new pairs are already included
|
|
155
|
+
}
|
|
156
|
+
// Reuse wrapped promise with fallback if it exists (avoid duplicate catch handlers)
|
|
157
|
+
if (ws_connection_promises_with_fallback[ws_key]) {
|
|
158
|
+
ws_promise_with_fallback = ws_connection_promises_with_fallback[ws_key]
|
|
32
159
|
}
|
|
160
|
+
} else {
|
|
161
|
+
// Create new WebSocket connection
|
|
162
|
+
ws_connection_pairs[ws_key] = all_pairs
|
|
163
|
+
ws_promise = new Promise((resolve, reject) => {
|
|
164
|
+
if (!Exchange.ws || !Exchange.ws.market) {
|
|
165
|
+
console.error(`${Exchange_name} WebSocket not properly initialized`)
|
|
166
|
+
reject(new Error(`WebSocket not initialized for ${Exchange_name}`))
|
|
167
|
+
return
|
|
168
|
+
}
|
|
33
169
|
|
|
34
|
-
|
|
170
|
+
// Store promise before starting connection
|
|
171
|
+
ws_connection_promises[ws_key] = ws_promise
|
|
172
|
+
|
|
173
|
+
// Create WebSocket with ALL pairs collected in first pass
|
|
174
|
+
// This ensures only ONE WebSocket per exchange, even with multiple clients/pairs
|
|
175
|
+
Exchange.ws_market(all_pairs)
|
|
35
176
|
|
|
36
177
|
const WS_TIMEOUT_MS = 30000 // 30 seconds timeout
|
|
37
|
-
const start_time = Date.now()
|
|
38
|
-
let check_count = 0
|
|
39
178
|
const MAX_CHECKS = 300 // 30 seconds / 100ms
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// Timeout check
|
|
46
|
-
if (elapsed > WS_TIMEOUT_MS || check_count > MAX_CHECKS) {
|
|
179
|
+
const check_timeout = create_ws_timeout_checker(
|
|
180
|
+
Exchange_name,
|
|
181
|
+
WS_TIMEOUT_MS,
|
|
182
|
+
MAX_CHECKS,
|
|
183
|
+
(elapsed) => {
|
|
47
184
|
console.error(`WebSocket timeout for ${Exchange_name} after ${elapsed}ms`)
|
|
48
185
|
reject(new Error(`WebSocket timeout for ${Exchange_name}`))
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
const check_status = () => {
|
|
190
|
+
if (check_timeout()) {
|
|
49
191
|
return
|
|
50
192
|
}
|
|
51
193
|
|
|
@@ -56,74 +198,184 @@ const get_exchanges_rates = function (Exchanges, exchange_pair_dict, cb, ws_enab
|
|
|
56
198
|
return
|
|
57
199
|
}
|
|
58
200
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
let rate_promises = exchange_pair_dict[Exchange_name].map((pair) => {
|
|
201
|
+
// Check only the pairs we need for this call (not all pairs in connection)
|
|
202
|
+
let pairs_to_check = exchange_pair_dict[Exchange_name] || all_pairs
|
|
203
|
+
|
|
204
|
+
// Check if WebSocket connection is at least opening/opened (not closed/stopped)
|
|
205
|
+
const ws_connection_ready = Exchange.ws.market.status === 'opening' || Exchange.ws.market.status === 'opened'
|
|
206
|
+
|
|
207
|
+
if (ws_connection_ready) {
|
|
208
|
+
// Try each pair individually - wait a bit for pairs to become ready
|
|
209
|
+
// This allows us to detect individual pair failures while giving time for subscription
|
|
210
|
+
const PAIR_READY_TIMEOUT_MS = 10000 // Wait up to 10 seconds for pair to become ready
|
|
211
|
+
const PAIR_CHECK_INTERVAL_MS = 100 // Check every 100ms
|
|
212
|
+
|
|
213
|
+
let rate_promises = pairs_to_check.map((pair) => {
|
|
73
214
|
return new Promise((rate_resolve) => {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
215
|
+
// Check if this pair should skip WebSocket due to persistent failures
|
|
216
|
+
if (should_skip_websocket(ws_failed_pairs, Exchange_name, pair)) {
|
|
217
|
+
const failure_info = ws_failed_pairs[Exchange_name][pair]
|
|
218
|
+
console.log(`[WebSocket Skip] ${Exchange_name} ${pair}: Skipping WebSocket (${failure_info.count} consecutive failures, last failed ${Math.round((Date.now() - failure_info.last_failed) / 1000)}s ago)`)
|
|
219
|
+
rate_resolve({ pair, failed: true, skip_ws: true })
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Wait for pair to become ready with retry logic
|
|
224
|
+
const pair_start_time = Date.now()
|
|
225
|
+
const check_pair_ready = () => {
|
|
226
|
+
// Check if this specific pair is ready before calling rate_ws
|
|
227
|
+
const pair_ready = (
|
|
228
|
+
Exchange.ws.market.pair_status &&
|
|
229
|
+
Exchange.ws.market.pair_status[pair] === 'opened' &&
|
|
230
|
+
Exchange.ws.market.asks_dict &&
|
|
231
|
+
Exchange.ws.market.asks_dict[pair] &&
|
|
232
|
+
Exchange.ws.market.asks_dict[pair].length > 0 &&
|
|
233
|
+
Exchange.ws.market.bids_dict &&
|
|
234
|
+
Exchange.ws.market.bids_dict[pair] &&
|
|
235
|
+
Exchange.ws.market.bids_dict[pair].length > 0
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if (pair_ready) {
|
|
239
|
+
// Pair is ready, try to get rate
|
|
240
|
+
Exchange.rate_ws(pair, function (res) {
|
|
241
|
+
if (res.success) {
|
|
242
|
+
// Success - reset failure count
|
|
243
|
+
reset_ws_failure(ws_failed_pairs, Exchange_name, pair)
|
|
244
|
+
set_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair, res.body)
|
|
245
|
+
rate_resolve({ pair, failed: false })
|
|
246
|
+
} else {
|
|
247
|
+
console.error(`getting rate ws failed for ${Exchange_name} ${pair}:`, res)
|
|
248
|
+
delete_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair)
|
|
249
|
+
track_ws_failure(ws_failed_pairs, Exchange_name, pair)
|
|
250
|
+
rate_resolve({ pair, failed: true })
|
|
251
|
+
}
|
|
252
|
+
})
|
|
78
253
|
} else {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
254
|
+
// Pair not ready yet - check if we've exceeded timeout
|
|
255
|
+
const elapsed = Date.now() - pair_start_time
|
|
256
|
+
if (elapsed >= PAIR_READY_TIMEOUT_MS) {
|
|
257
|
+
// Timeout reached - mark as failed, will fall back to REST
|
|
258
|
+
console.log(`[WebSocket] ${Exchange_name} ${pair}: Not ready after ${elapsed}ms (status: ${Exchange.ws.market.pair_status?.[pair] || 'unknown'})`)
|
|
259
|
+
rate_resolve({ pair, failed: true })
|
|
260
|
+
} else {
|
|
261
|
+
// Retry after a short delay
|
|
262
|
+
setTimeout(check_pair_ready, PAIR_CHECK_INTERVAL_MS)
|
|
85
263
|
}
|
|
86
264
|
}
|
|
87
|
-
|
|
88
|
-
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Start checking
|
|
268
|
+
check_pair_ready()
|
|
89
269
|
})
|
|
90
270
|
})
|
|
91
271
|
Promise.all(rate_promises)
|
|
92
|
-
.then(() => {
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
272
|
+
.then((results) => {
|
|
273
|
+
// Check which pairs failed and fall back to REST for those pairs only
|
|
274
|
+
const failed_pairs = results.filter((r) => r && r.failed).map((r) => r.pair)
|
|
275
|
+
if (failed_pairs.length > 0 && Exchange.rate) {
|
|
276
|
+
console.log(`[WebSocket Fallback] ${Exchange_name}: Falling back to REST for ${failed_pairs.length} pair(s): ${failed_pairs.join(', ')}`)
|
|
277
|
+
const rest_promises = failed_pairs.map((pair) => {
|
|
278
|
+
return new Promise((rest_resolve) => {
|
|
279
|
+
Exchange.rate(pair, DEFAULT_ORDERBOOK_LEVEL, function (res) {
|
|
280
|
+
if (res.success) {
|
|
281
|
+
set_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair, res.body)
|
|
282
|
+
console.log(`[REST Fallback] ${Exchange_name} ${pair}: Successfully fetched via REST`)
|
|
283
|
+
} else {
|
|
284
|
+
console.error(`[REST Fallback] getting rate failed for ${Exchange_name} ${pair}:`, res)
|
|
285
|
+
delete_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair)
|
|
286
|
+
}
|
|
287
|
+
rest_resolve()
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
|
+
// Wait for REST fallback to complete before resolving
|
|
292
|
+
return Promise.all(rest_promises)
|
|
293
|
+
.catch((err) => {
|
|
294
|
+
console.error(`[REST Fallback] Error fetching rates for ${Exchange_name}:`, err)
|
|
295
|
+
})
|
|
296
|
+
.then(() => {
|
|
297
|
+
// Resolve after REST fallback completes
|
|
298
|
+
resolve()
|
|
299
|
+
})
|
|
300
|
+
} else {
|
|
301
|
+
// if (Exchange.ws.market.end_base) {
|
|
302
|
+
// Exchange.ws.market.end_base(); // Close the connection and prevent reconnection
|
|
303
|
+
// console.log(`${Exchange_name} WebSocket connection closed.`);
|
|
304
|
+
// }
|
|
305
|
+
resolve()
|
|
306
|
+
}
|
|
98
307
|
})
|
|
99
308
|
.catch((err) => {
|
|
100
309
|
console.error(`Error in rate_ws for ${Exchange_name}:`, err)
|
|
101
310
|
resolve()
|
|
102
311
|
})
|
|
103
312
|
} else {
|
|
313
|
+
// WebSocket connection not ready yet, wait and retry
|
|
104
314
|
setTimeout(check_status, 100)
|
|
105
315
|
}
|
|
106
316
|
}
|
|
107
317
|
|
|
108
318
|
check_status()
|
|
109
319
|
})
|
|
320
|
+
// Clean up promise reference when done (but keep connection open for reuse)
|
|
321
|
+
ws_promise.finally(() => {
|
|
322
|
+
// Don't delete immediately - keep for a short time to allow reuse
|
|
323
|
+
setTimeout(() => {
|
|
324
|
+
if (ws_connection_promises[ws_key] === ws_promise) {
|
|
325
|
+
delete ws_connection_promises[ws_key]
|
|
326
|
+
// Also clean up wrapped promise if it exists
|
|
327
|
+
if (ws_connection_promises_with_fallback[ws_key]) {
|
|
328
|
+
delete ws_connection_promises_with_fallback[ws_key]
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}, 1000)
|
|
332
|
+
})
|
|
333
|
+
}
|
|
110
334
|
|
|
111
|
-
|
|
335
|
+
// Wrap WebSocket promise to handle rejection and fall back to REST
|
|
336
|
+
// Only create wrapper if it doesn't exist (avoid duplicate catch handlers on reused promises)
|
|
337
|
+
if (!ws_promise_with_fallback) {
|
|
338
|
+
ws_promise_with_fallback = ws_promise.catch((err) => {
|
|
339
|
+
console.error(`[WebSocket Failed] ${Exchange_name}: Falling back to REST for all pairs. Error:`, err.message)
|
|
340
|
+
// Fall back to REST for all pairs
|
|
341
|
+
if (Exchange.rate) {
|
|
342
|
+
return Promise.all(
|
|
343
|
+
all_pairs.map((pair) => {
|
|
344
|
+
return new Promise((rest_resolve) => {
|
|
345
|
+
Exchange.rate(pair, DEFAULT_ORDERBOOK_LEVEL, function (res) {
|
|
346
|
+
if (res.success) {
|
|
347
|
+
set_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair, res.body)
|
|
348
|
+
console.log(`[REST Fallback] ${Exchange_name} ${pair}: Successfully fetched via REST`)
|
|
349
|
+
} else {
|
|
350
|
+
console.error(`[REST Fallback] getting rate failed for ${Exchange_name} ${pair}:`, res)
|
|
351
|
+
delete_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair)
|
|
352
|
+
}
|
|
353
|
+
rest_resolve()
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
})
|
|
357
|
+
).then(() => {
|
|
358
|
+
// Resolve after REST fallback completes
|
|
359
|
+
return Promise.resolve()
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
// If no REST fallback available, still resolve to prevent Promise.all from failing
|
|
363
|
+
return Promise.resolve()
|
|
364
|
+
})
|
|
365
|
+
// Store wrapped promise for reuse
|
|
366
|
+
ws_connection_promises_with_fallback[ws_key] = ws_promise_with_fallback
|
|
367
|
+
}
|
|
368
|
+
promises.push(ws_promise_with_fallback)
|
|
112
369
|
} else if (Exchange.rate) {
|
|
113
|
-
let
|
|
370
|
+
let pairs_to_fetch = exchange_pair_dict[Exchange_name] || all_pairs
|
|
371
|
+
let rate_promises = pairs_to_fetch.map((pair) => {
|
|
114
372
|
return new Promise((resolve, reject) => {
|
|
115
373
|
Exchange.rate(pair, DEFAULT_ORDERBOOK_LEVEL, function (res) {
|
|
116
374
|
if (res.success) {
|
|
117
|
-
|
|
118
|
-
_.set(pair_exchange_rates, [pair, Exchange_name], res.body)
|
|
375
|
+
set_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair, res.body)
|
|
119
376
|
} else {
|
|
120
377
|
console.error(`getting rate failed for ${Exchange_name} ${pair}:`, res)
|
|
121
|
-
|
|
122
|
-
delete exchange_pair_rates[Exchange_name][pair]
|
|
123
|
-
}
|
|
124
|
-
if (pair_exchange_rates[pair]) {
|
|
125
|
-
delete pair_exchange_rates[pair][Exchange_name]
|
|
126
|
-
}
|
|
378
|
+
delete_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair)
|
|
127
379
|
}
|
|
128
380
|
resolve()
|
|
129
381
|
})
|
|
@@ -132,23 +384,18 @@ const get_exchanges_rates = function (Exchanges, exchange_pair_dict, cb, ws_enab
|
|
|
132
384
|
|
|
133
385
|
promises = promises.concat(rate_promises) // Add all rate promises
|
|
134
386
|
} else if (Exchange.rate_with_routes) {
|
|
135
|
-
let
|
|
387
|
+
let pairs_to_fetch = exchange_pair_dict[Exchange_name] || all_pairs
|
|
388
|
+
let rate_with_routes_promises = pairs_to_fetch.map((pair) => {
|
|
136
389
|
return new Promise((resolve, reject) => {
|
|
137
390
|
Exchange.rate_with_routes(pair, function (res) {
|
|
138
391
|
if (res.success) {
|
|
139
392
|
let { asks, bids } = res.body
|
|
140
393
|
asks = asks.map((ask) => ask.slice(0, 2))
|
|
141
394
|
bids = bids.map((bid) => bid.slice(0, 2))
|
|
142
|
-
|
|
143
|
-
_.set(pair_exchange_rates, [pair, Exchange_name], { asks, bids })
|
|
395
|
+
set_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair, { asks, bids })
|
|
144
396
|
} else {
|
|
145
397
|
console.error(`getting rate with routes failed for ${Exchange_name} ${pair}:`, res)
|
|
146
|
-
|
|
147
|
-
delete exchange_pair_rates[Exchange_name][pair]
|
|
148
|
-
}
|
|
149
|
-
if (pair_exchange_rates[pair]) {
|
|
150
|
-
delete pair_exchange_rates[pair][Exchange_name]
|
|
151
|
-
}
|
|
398
|
+
delete_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair)
|
|
152
399
|
}
|
|
153
400
|
resolve()
|
|
154
401
|
})
|
|
@@ -172,7 +419,14 @@ const get_exchanges_rates = function (Exchanges, exchange_pair_dict, cb, ws_enab
|
|
|
172
419
|
}
|
|
173
420
|
|
|
174
421
|
let exchange_pair_market_history = {},
|
|
175
|
-
pair_exchange_market_history = {}
|
|
422
|
+
pair_exchange_market_history = {},
|
|
423
|
+
// Track active WebSocket connections for market history
|
|
424
|
+
ws_market_history_promises = {},
|
|
425
|
+
ws_market_history_pairs = {},
|
|
426
|
+
// Track wrapped promises with fallback handlers to avoid duplicate handlers
|
|
427
|
+
ws_market_history_promises_with_fallback = {},
|
|
428
|
+
// Track persistent WebSocket failures for market history
|
|
429
|
+
ws_market_history_failed_pairs = {} // { "ExchangeName": { "PAIR": { count: number, last_failed: timestamp } } }
|
|
176
430
|
const get_exchanges_market_history = function (Exchanges, exchange_pair_dict, timeframe_in_ms, cb, ws_enabled = true) {
|
|
177
431
|
let checked_market_history = {}
|
|
178
432
|
let promises = []
|
|
@@ -181,41 +435,71 @@ const get_exchanges_market_history = function (Exchanges, exchange_pair_dict, ti
|
|
|
181
435
|
console.error('Exchanges should be an array')
|
|
182
436
|
return
|
|
183
437
|
} else {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
438
|
+
// First pass: group pairs by exchange name and find the Exchange instance
|
|
439
|
+
const { exchange_instances, exchange_all_pairs } = group_exchanges_by_name(Exchanges, exchange_pair_dict)
|
|
440
|
+
|
|
441
|
+
// Second pass: process each unique exchange once
|
|
442
|
+
for (let Exchange_name in exchange_instances) {
|
|
443
|
+
let Exchange = exchange_instances[Exchange_name]
|
|
444
|
+
let all_pairs = exchange_all_pairs[Exchange_name]
|
|
445
|
+
|
|
193
446
|
if (Exchange.ws_market && ws_enabled) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return
|
|
199
|
-
}
|
|
447
|
+
// Check if WebSocket is already opening/opened for this exchange
|
|
448
|
+
const ws_key = `market_history_${Exchange_name}`
|
|
449
|
+
const ws_status = Exchange.ws?.market?.status
|
|
450
|
+
const is_opening_or_opened = ws_status === 'opening' || ws_status === 'opened'
|
|
200
451
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
452
|
+
let ws_promise
|
|
453
|
+
let ws_promise_with_fallback
|
|
454
|
+
if (is_opening_or_opened && ws_market_history_promises[ws_key]) {
|
|
455
|
+
// Reuse existing WebSocket connection promise
|
|
456
|
+
ws_promise = ws_market_history_promises[ws_key]
|
|
457
|
+
// Merge pairs if needed
|
|
458
|
+
const existing_pairs = ws_market_history_pairs[ws_key] || []
|
|
459
|
+
const new_pairs = _.difference(all_pairs, existing_pairs)
|
|
460
|
+
if (new_pairs.length > 0) {
|
|
461
|
+
ws_market_history_pairs[ws_key] = _.union(existing_pairs, all_pairs)
|
|
462
|
+
}
|
|
463
|
+
// Reuse wrapped promise with fallback if it exists (avoid duplicate catch handlers)
|
|
464
|
+
if (ws_market_history_promises_with_fallback[ws_key]) {
|
|
465
|
+
ws_promise_with_fallback = ws_market_history_promises_with_fallback[ws_key]
|
|
204
466
|
}
|
|
467
|
+
} else {
|
|
468
|
+
// Create new WebSocket connection (or use existing one from rates)
|
|
469
|
+
ws_market_history_pairs[ws_key] = all_pairs
|
|
470
|
+
ws_promise = new Promise((resolve, reject) => {
|
|
471
|
+
if (!Exchange.ws || !Exchange.ws.market) {
|
|
472
|
+
console.error(`${Exchange_name} WebSocket not properly initialized`)
|
|
473
|
+
reject(new Error(`WebSocket not initialized for ${Exchange_name}`))
|
|
474
|
+
return
|
|
475
|
+
}
|
|
205
476
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
let check_count = 0
|
|
209
|
-
const MAX_CHECKS = 300 // 30 seconds / 100ms
|
|
477
|
+
// Store promise before starting connection
|
|
478
|
+
ws_market_history_promises[ws_key] = ws_promise
|
|
210
479
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
480
|
+
// Only start if not already started (reuse connection from rates if available)
|
|
481
|
+
// If starting new, use ALL pairs collected in first pass
|
|
482
|
+
if (!first_time[Exchange_name]) {
|
|
483
|
+
// Create WebSocket with ALL pairs collected in first pass
|
|
484
|
+
// This ensures only ONE WebSocket per exchange, even with multiple clients/pairs
|
|
485
|
+
Exchange.ws_market(all_pairs)
|
|
486
|
+
first_time[Exchange_name] = true
|
|
487
|
+
}
|
|
214
488
|
|
|
215
|
-
|
|
216
|
-
|
|
489
|
+
const WS_TIMEOUT_MS = 30000 // 30 seconds timeout
|
|
490
|
+
const MAX_CHECKS = 300 // 30 seconds / 100ms
|
|
491
|
+
const check_timeout = create_ws_timeout_checker(
|
|
492
|
+
Exchange_name,
|
|
493
|
+
WS_TIMEOUT_MS,
|
|
494
|
+
MAX_CHECKS,
|
|
495
|
+
(elapsed) => {
|
|
217
496
|
console.error(`WebSocket timeout for ${Exchange_name} market history after ${elapsed}ms`)
|
|
218
497
|
reject(new Error(`WebSocket timeout for ${Exchange_name} market history`))
|
|
498
|
+
}
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
const check_status = () => {
|
|
502
|
+
if (check_timeout()) {
|
|
219
503
|
return
|
|
220
504
|
}
|
|
221
505
|
|
|
@@ -226,37 +510,106 @@ const get_exchanges_market_history = function (Exchanges, exchange_pair_dict, ti
|
|
|
226
510
|
return
|
|
227
511
|
}
|
|
228
512
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
513
|
+
// Check only the pairs we need for this call
|
|
514
|
+
let pairs_to_check = exchange_pair_dict[Exchange_name] || all_pairs
|
|
515
|
+
|
|
516
|
+
// Check if WebSocket connection is at least opening/opened (not closed/stopped)
|
|
517
|
+
const ws_connection_ready = Exchange.ws.market.status === 'opening' || Exchange.ws.market.status === 'opened'
|
|
518
|
+
|
|
519
|
+
if (ws_connection_ready) {
|
|
520
|
+
// Try each pair individually - wait a bit for pairs to become ready
|
|
521
|
+
// This allows us to detect individual pair failures while giving time for subscription
|
|
522
|
+
const PAIR_READY_TIMEOUT_MS = 10000 // Wait up to 10 seconds for pair to become ready
|
|
523
|
+
const PAIR_CHECK_INTERVAL_MS = 100 // Check every 100ms
|
|
524
|
+
|
|
525
|
+
let market_history_promises = pairs_to_check.map((pair) => {
|
|
240
526
|
return new Promise((rate_resolve) => {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
527
|
+
// Check if this pair should skip WebSocket due to persistent failures
|
|
528
|
+
if (should_skip_websocket(ws_market_history_failed_pairs, Exchange_name, pair)) {
|
|
529
|
+
const failure_info = ws_market_history_failed_pairs[Exchange_name][pair]
|
|
530
|
+
console.log(`[WebSocket Skip] ${Exchange_name} ${pair} market history: Skipping WebSocket (${failure_info.count} consecutive failures, last failed ${Math.round((Date.now() - failure_info.last_failed) / 1000)}s ago)`)
|
|
531
|
+
rate_resolve({ pair, failed: true, skip_ws: true })
|
|
532
|
+
return
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Wait for pair to become ready with retry logic
|
|
536
|
+
const pair_start_time = Date.now()
|
|
537
|
+
const check_pair_ready = () => {
|
|
538
|
+
// Check if this specific pair is ready before calling market_history_ws
|
|
539
|
+
const pair_ready = (
|
|
540
|
+
Exchange.ws.market.pair_status &&
|
|
541
|
+
Exchange.ws.market.pair_status[pair] === 'opened' &&
|
|
542
|
+
Exchange.ws.market.market_history_dict &&
|
|
543
|
+
Exchange.ws.market.market_history_dict[pair] &&
|
|
544
|
+
Exchange.ws.market.market_history_dict[pair].length >= 0
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
if (pair_ready) {
|
|
548
|
+
// Pair is ready, try to get market history
|
|
549
|
+
Exchange.market_history_ws(pair, timeframe_in_ms, function (res) {
|
|
550
|
+
if (res.success) {
|
|
551
|
+
// Success - reset failure count
|
|
552
|
+
reset_ws_failure(ws_market_history_failed_pairs, Exchange_name, pair)
|
|
553
|
+
set_pair_data(exchange_pair_market_history, pair_exchange_market_history, Exchange_name, pair, res.body)
|
|
554
|
+
rate_resolve({ pair, failed: false })
|
|
555
|
+
} else {
|
|
556
|
+
console.error(`getting market history ws failed for ${Exchange_name} ${pair}:`, res)
|
|
557
|
+
delete_pair_data(exchange_pair_market_history, pair_exchange_market_history, Exchange_name, pair)
|
|
558
|
+
track_ws_failure(ws_market_history_failed_pairs, Exchange_name, pair)
|
|
559
|
+
rate_resolve({ pair, failed: true })
|
|
560
|
+
}
|
|
561
|
+
})
|
|
245
562
|
} else {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
563
|
+
// Pair not ready yet - check if we've exceeded timeout
|
|
564
|
+
const elapsed = Date.now() - pair_start_time
|
|
565
|
+
if (elapsed >= PAIR_READY_TIMEOUT_MS) {
|
|
566
|
+
// Timeout reached - mark as failed, will fall back to REST
|
|
567
|
+
console.log(`[WebSocket Market History] ${Exchange_name} ${pair}: Not ready after ${elapsed}ms (status: ${Exchange.ws.market.pair_status?.[pair] || 'unknown'})`)
|
|
568
|
+
rate_resolve({ pair, failed: true })
|
|
569
|
+
} else {
|
|
570
|
+
// Retry after a short delay
|
|
571
|
+
setTimeout(check_pair_ready, PAIR_CHECK_INTERVAL_MS)
|
|
252
572
|
}
|
|
253
573
|
}
|
|
254
|
-
|
|
255
|
-
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Start checking
|
|
577
|
+
check_pair_ready()
|
|
256
578
|
})
|
|
257
579
|
})
|
|
258
580
|
Promise.all(market_history_promises)
|
|
259
|
-
.then(() =>
|
|
581
|
+
.then((results) => {
|
|
582
|
+
// Check which pairs failed and fall back to REST for those pairs only
|
|
583
|
+
const failed_pairs = results.filter((r) => r && r.failed).map((r) => r.pair)
|
|
584
|
+
if (failed_pairs.length > 0 && Exchange.market_history) {
|
|
585
|
+
console.log(`[WebSocket Fallback] ${Exchange_name} market history: Falling back to REST for ${failed_pairs.length} pair(s): ${failed_pairs.join(', ')}`)
|
|
586
|
+
const rest_promises = failed_pairs.map((pair) => {
|
|
587
|
+
return new Promise((rest_resolve) => {
|
|
588
|
+
Exchange.market_history(pair, timeframe_in_ms, function (res) {
|
|
589
|
+
if (res.success) {
|
|
590
|
+
set_pair_data(exchange_pair_market_history, pair_exchange_market_history, Exchange_name, pair, res.body)
|
|
591
|
+
console.log(`[REST Fallback] ${Exchange_name} ${pair} market history: Successfully fetched via REST`)
|
|
592
|
+
} else {
|
|
593
|
+
console.error(`[REST Fallback] getting market history failed for ${Exchange_name} ${pair}:`, res)
|
|
594
|
+
delete_pair_data(exchange_pair_market_history, pair_exchange_market_history, Exchange_name, pair)
|
|
595
|
+
}
|
|
596
|
+
rest_resolve()
|
|
597
|
+
})
|
|
598
|
+
})
|
|
599
|
+
})
|
|
600
|
+
// Wait for REST fallback to complete before resolving
|
|
601
|
+
return Promise.all(rest_promises)
|
|
602
|
+
.catch((err) => {
|
|
603
|
+
console.error(`[REST Fallback] Error fetching market history for ${Exchange_name}:`, err)
|
|
604
|
+
})
|
|
605
|
+
.then(() => {
|
|
606
|
+
// Resolve after REST fallback completes
|
|
607
|
+
resolve()
|
|
608
|
+
})
|
|
609
|
+
} else {
|
|
610
|
+
resolve()
|
|
611
|
+
}
|
|
612
|
+
})
|
|
260
613
|
.catch((err) => {
|
|
261
614
|
console.error(`Error in market_history_ws for ${Exchange_name}:`, err)
|
|
262
615
|
resolve()
|
|
@@ -267,22 +620,64 @@ const get_exchanges_market_history = function (Exchanges, exchange_pair_dict, ti
|
|
|
267
620
|
}
|
|
268
621
|
check_status()
|
|
269
622
|
})
|
|
270
|
-
|
|
623
|
+
// Clean up promise reference when done (but keep connection open for reuse)
|
|
624
|
+
ws_promise.finally(() => {
|
|
625
|
+
setTimeout(() => {
|
|
626
|
+
if (ws_market_history_promises[ws_key] === ws_promise) {
|
|
627
|
+
delete ws_market_history_promises[ws_key]
|
|
628
|
+
// Also clean up wrapped promise if it exists
|
|
629
|
+
if (ws_market_history_promises_with_fallback[ws_key]) {
|
|
630
|
+
delete ws_market_history_promises_with_fallback[ws_key]
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}, 1000)
|
|
634
|
+
})
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// If WebSocket promise rejects (timeout/error), fall back to REST for all pairs
|
|
638
|
+
// Only create wrapper if it doesn't exist (avoid duplicate catch handlers on reused promises)
|
|
639
|
+
if (!ws_promise_with_fallback) {
|
|
640
|
+
ws_promise_with_fallback = ws_promise.catch((err) => {
|
|
641
|
+
console.error(`[WebSocket Failed] ${Exchange_name} market history: Falling back to REST for all pairs. Error:`, err.message)
|
|
642
|
+
// Fall back to REST for all pairs
|
|
643
|
+
if (Exchange.market_history) {
|
|
644
|
+
return Promise.all(
|
|
645
|
+
all_pairs.map((pair) => {
|
|
646
|
+
return new Promise((rest_resolve) => {
|
|
647
|
+
Exchange.market_history(pair, timeframe_in_ms, function (res) {
|
|
648
|
+
if (res.success) {
|
|
649
|
+
set_pair_data(exchange_pair_market_history, pair_exchange_market_history, Exchange_name, pair, res.body)
|
|
650
|
+
console.log(`[REST Fallback] ${Exchange_name} ${pair} market history: Successfully fetched via REST`)
|
|
651
|
+
} else {
|
|
652
|
+
console.error(`[REST Fallback] getting market history failed for ${Exchange_name} ${pair}:`, res)
|
|
653
|
+
delete_pair_data(exchange_pair_market_history, pair_exchange_market_history, Exchange_name, pair)
|
|
654
|
+
}
|
|
655
|
+
rest_resolve()
|
|
656
|
+
})
|
|
657
|
+
})
|
|
658
|
+
})
|
|
659
|
+
).then(() => {
|
|
660
|
+
// Resolve after REST fallback completes
|
|
661
|
+
return Promise.resolve()
|
|
662
|
+
})
|
|
663
|
+
}
|
|
664
|
+
// If no REST fallback available, still resolve to prevent Promise.all from failing
|
|
665
|
+
return Promise.resolve()
|
|
666
|
+
})
|
|
667
|
+
// Store wrapped promise for reuse
|
|
668
|
+
ws_market_history_promises_with_fallback[ws_key] = ws_promise_with_fallback
|
|
669
|
+
}
|
|
670
|
+
promises.push(ws_promise_with_fallback)
|
|
271
671
|
} else if (Exchange.market_history) {
|
|
272
|
-
let
|
|
672
|
+
let pairs_to_fetch = exchange_pair_dict[Exchange_name] || all_pairs
|
|
673
|
+
let market_history_promises = pairs_to_fetch.map((pair) => {
|
|
273
674
|
return new Promise((resolve) => {
|
|
274
675
|
Exchange.market_history(pair, timeframe_in_ms, function (res) {
|
|
275
676
|
if (res.success) {
|
|
276
|
-
|
|
277
|
-
_.set(pair_exchange_market_history, [pair, Exchange_name], res.body)
|
|
677
|
+
set_pair_data(exchange_pair_market_history, pair_exchange_market_history, Exchange_name, pair, res.body)
|
|
278
678
|
} else {
|
|
279
679
|
console.error(`getting market history failed for ${Exchange_name} ${pair}:`, res)
|
|
280
|
-
|
|
281
|
-
delete exchange_pair_market_history[Exchange_name][pair]
|
|
282
|
-
}
|
|
283
|
-
if (pair_exchange_market_history[pair]) {
|
|
284
|
-
delete pair_exchange_market_history[pair][Exchange_name]
|
|
285
|
-
}
|
|
680
|
+
delete_pair_data(exchange_pair_market_history, pair_exchange_market_history, Exchange_name, pair)
|
|
286
681
|
}
|
|
287
682
|
resolve()
|
|
288
683
|
})
|
|
@@ -330,16 +725,10 @@ const get_exchanges_ohlcv = function (Exchanges, exchange_pair_dict, from_in_ms,
|
|
|
330
725
|
if (Exchange.ohlcv) {
|
|
331
726
|
Exchange.ohlcv(pair, from_in_ms, to_in_ms, function (res) {
|
|
332
727
|
if (res.success) {
|
|
333
|
-
|
|
334
|
-
_.set(pair_exchange_ohlcv, [pair, Exchange_name], res.body)
|
|
728
|
+
set_pair_data(exchange_pair_ohlcv, pair_exchange_ohlcv, Exchange_name, pair, res.body)
|
|
335
729
|
} else {
|
|
336
730
|
console.error(`getting ohlcv failed for ${Exchange_name} ${pair}:`, res)
|
|
337
|
-
|
|
338
|
-
delete exchange_pair_ohlcv[Exchange_name][pair]
|
|
339
|
-
}
|
|
340
|
-
if (pair_exchange_ohlcv[pair]) {
|
|
341
|
-
delete pair_exchange_ohlcv[pair][Exchange_name]
|
|
342
|
-
}
|
|
731
|
+
delete_pair_data(exchange_pair_ohlcv, pair_exchange_ohlcv, Exchange_name, pair)
|
|
343
732
|
}
|
|
344
733
|
_.set(ohlcv_status, [Exchange_name + pair], true)
|
|
345
734
|
// Use a flag to prevent multiple callback calls
|