1use std::time::Duration;
12
13use futures_util::StreamExt;
14use tokio::time::timeout;
15
16use digdigdig3::l3::open::crypto::cex::binance::{BinanceConnector, BinanceWebSocket};
17use digdigdig3::l3::open::crypto::cex::bybit::{BybitConnector, BybitWebSocket};
18use digdigdig3::l3::open::crypto::cex::okx::{OkxConnector, OkxWebSocket};
19use digdigdig3::l3::open::crypto::cex::hyperliquid::{HyperliquidConnector, HyperliquidWebSocket};
20use digdigdig3::l3::open::crypto::cex::deribit::{DeribitConnector, DeribitWebSocket};
21use digdigdig3::l3::open::crypto::cex::bitget::BitgetConnector;
22use digdigdig3::l3::open::crypto::cex::htx::{HtxConnector, HtxWebSocket};
23use digdigdig3::l3::open::crypto::cex::kucoin::{KuCoinConnector, KuCoinWebSocket};
24use digdigdig3::l3::open::crypto::cex::gateio::{GateioConnector, GateioWebSocket};
25use digdigdig3::l3::open::crypto::cex::bitfinex::{BitfinexConnector, BitfinexWebSocket};
26use digdigdig3::l3::open::crypto::cex::kraken::{KrakenConnector, KrakenWebSocket};
27use digdigdig3::l3::open::crypto::cex::gemini::{GeminiConnector, GeminiWebSocket};
28use digdigdig3::l3::open::crypto::cex::bitstamp::{BitstampConnector, BitstampWebSocket};
29use digdigdig3::l3::open::crypto::cex::upbit::UpbitConnector;
30use digdigdig3::l3::open::crypto::cex::crypto_com::{CryptoComConnector, CryptoComWebSocket};
31use digdigdig3::l3::open::crypto::cex::bingx::BingxConnector;
32use digdigdig3::l3::open::crypto::cex::coinbase::CoinbaseWebSocket;
33use digdigdig3::l3::open::crypto::dex::dydx::DydxConnector;
34
35use digdigdig3::core::{
36 AccountType, Symbol, StreamType, SubscriptionRequest,
37};
38use digdigdig3::core::traits::WebSocketConnector;
39
40fn abbrev<T: std::fmt::Debug>(val: &T) -> String {
44 let s = format!("{:?}", val);
45 if s.len() > 80 {
46 format!("{}…", &s[..80])
47 } else {
48 s
49 }
50}
51
52macro_rules! ok_rest {
54 ($method:expr, $vec:expr) => {{
55 let n = $vec.len();
56 if n > 0 {
57 println!(" OK: {} -> {} items, first: {}", $method, n, abbrev(&$vec[0]));
58 } else {
59 println!(" OK: {} -> 0 items (empty but no error)", $method);
60 }
61 (true, n)
62 }};
63}
64
65macro_rules! ok_rest_single {
66 ($method:expr, $val:expr) => {{
67 println!(" OK: {} -> {}", $method, abbrev(&$val));
68 (true, 1usize)
69 }};
70}
71
72macro_rules! fail_rest {
73 ($method:expr, $err:expr) => {{
74 println!(" FAIL: {} -> {}", $method, $err);
75 (false, 0usize)
76 }};
77}
78
79struct RestTally {
82 exchange: String,
83 tested: usize,
84 passed: usize,
85 failed: usize,
86}
87
88struct WsTally {
89 exchange: String,
90 channels: usize,
91 subscribed: usize,
92 events: usize,
93 parse_errors: usize,
94 zero_event_channels: Vec<String>,
95}
96
97async fn test_binance_rest() -> RestTally {
102 println!("\n── Binance REST ─────────────────────────────────────────────");
103 let mut tally = RestTally { exchange: "Binance".into(), tested: 0, passed: 0, failed: 0 };
104
105 let conn = match BinanceConnector::new(None, false).await {
106 Ok(c) => c,
107 Err(e) => {
108 println!(" FAIL: connector init -> {}", e);
109 tally.failed += 1;
110 tally.tested += 1;
111 return tally;
112 }
113 };
114
115 tally.tested += 1;
117 match conn.get_open_interest("BTCUSDT").await {
118 Ok(v) => { let (p, _) = ok_rest_single!("get_open_interest(BTCUSDT)", v); tally.passed += p as usize; }
119 Err(e) => { fail_rest!("get_open_interest(BTCUSDT)", e); tally.failed += 1; }
120 }
121
122 tally.tested += 1;
124 match conn.get_premium_index(Some("BTCUSDT")).await {
125 Ok(v) => { let (p, _) = ok_rest_single!("get_premium_index(BTCUSDT)", v); tally.passed += p as usize; }
126 Err(e) => { fail_rest!("get_premium_index(BTCUSDT)", e); tally.failed += 1; }
127 }
128
129 tally.tested += 1;
131 match conn.get_force_orders(Some("BTCUSDT"), None, None, None, Some(10)).await {
132 Ok(v) => { let (p, _) = ok_rest!("get_force_orders(BTCUSDT)", v); tally.passed += p as usize; }
133 Err(e) => {
134 let msg = e.to_string();
135 if msg.contains("key") || msg.contains("signature") || msg.contains("apiKey") {
136 println!(" SKIPPED: get_force_orders -> needs API key");
137 } else {
138 fail_rest!("get_force_orders(BTCUSDT)", e);
139 tally.failed += 1;
140 }
141 }
142 }
143
144 tally.tested += 1;
146 match conn.get_top_long_short_account_ratio("BTCUSDT", "1h", Some(10), None, None).await {
147 Ok(v) => { let (p, _) = ok_rest!("get_top_long_short_account_ratio", v); tally.passed += p as usize; }
148 Err(e) => { fail_rest!("get_top_long_short_account_ratio", e); tally.failed += 1; }
149 }
150
151 tally.tested += 1;
153 match conn.get_open_interest_history("BTCUSDT", "1h", Some(10), None, None).await {
154 Ok(v) => { let (p, _) = ok_rest!("get_open_interest_history", v); tally.passed += p as usize; }
155 Err(e) => { fail_rest!("get_open_interest_history", e); tally.failed += 1; }
156 }
157
158 tally.tested += 1;
160 match conn.get_basis_history("BTCUSDT", "PERPETUAL", "5m", Some(5), None, None).await {
161 Ok(v) => { let (p, _) = ok_rest_single!("get_basis_history(BTCUSDT, PERPETUAL, 5m)", v); tally.passed += p as usize; }
162 Err(e) => { fail_rest!("get_basis_history", e); tally.failed += 1; }
163 }
164
165 tally.tested += 1;
167 match conn.get_open_interest_cm("BTCUSD_PERP").await {
168 Ok(v) => { let (p, _) = ok_rest_single!("get_open_interest_cm(BTCUSD_PERP)", v); tally.passed += p as usize; }
169 Err(e) => { fail_rest!("get_open_interest_cm", e); tally.failed += 1; }
170 }
171
172 tally.tested += 1;
174 match conn.get_funding_rate_history("BTCUSDT", None, None, Some(5)).await {
175 Ok(v) => { let (p, _) = ok_rest!("get_funding_rate_history(BTCUSDT, 5)", v); tally.passed += p as usize; }
176 Err(e) => { fail_rest!("get_funding_rate_history", e); tally.failed += 1; }
177 }
178
179 tally
180}
181
182async fn test_bybit_rest() -> RestTally {
183 println!("\n── Bybit REST ───────────────────────────────────────────────");
184 let mut tally = RestTally { exchange: "Bybit".into(), tested: 0, passed: 0, failed: 0 };
185
186 let conn = match BybitConnector::public(false).await {
187 Ok(c) => c,
188 Err(e) => {
189 println!(" FAIL: connector init -> {}", e);
190 tally.failed += 1;
191 tally.tested += 1;
192 return tally;
193 }
194 };
195
196 tally.tested += 1;
198 match conn.get_open_interest("linear", "BTCUSDT", "1h", Some(10), None, None).await {
199 Ok(v) => { let (p, _) = ok_rest!("get_open_interest(linear, BTCUSDT, 1h)", v); tally.passed += p as usize; }
200 Err(e) => { fail_rest!("get_open_interest", e); tally.failed += 1; }
201 }
202
203 tally.tested += 1;
205 match conn.get_long_short_ratio("linear", "BTCUSDT", "1h", Some(10)).await {
206 Ok(v) => { let (p, _) = ok_rest!("get_long_short_ratio(linear, BTCUSDT, 1h)", v); tally.passed += p as usize; }
207 Err(e) => { fail_rest!("get_long_short_ratio", e); tally.failed += 1; }
208 }
209
210 tally.tested += 1;
212 match conn.get_mark_price_kline("linear", "BTCUSDT", "60", Some(10), None, None).await {
213 Ok(v) => { let (p, _) = ok_rest!("get_mark_price_kline(linear, BTCUSDT, 60min)", v); tally.passed += p as usize; }
214 Err(e) => { fail_rest!("get_mark_price_kline", e); tally.failed += 1; }
215 }
216
217 tally.tested += 1;
219 match conn.get_risk_limit("linear", "BTCUSDT").await {
220 Ok(v) => { let (p, _) = ok_rest_single!("get_risk_limit(linear, BTCUSDT)", v); tally.passed += p as usize; }
221 Err(e) => { fail_rest!("get_risk_limit", e); tally.failed += 1; }
222 }
223
224 tally.tested += 1;
226 match conn.get_delivery_price("linear", "BTCUSDT", Some(5)).await {
227 Ok(v) => { let (p, _) = ok_rest_single!("get_delivery_price(linear, BTCUSDT)", v); tally.passed += p as usize; }
228 Err(e) => { fail_rest!("get_delivery_price", e); tally.failed += 1; }
229 }
230
231 tally.tested += 1;
233 match conn.get_institutional_loan_products().await {
234 Ok(v) => { let (p, _) = ok_rest_single!("get_institutional_loan_products()", v); tally.passed += p as usize; }
235 Err(e) => { fail_rest!("get_institutional_loan_products", e); tally.failed += 1; }
236 }
237
238 tally.tested += 1;
240 match conn.get_funding_rate_history("linear", "BTCUSDT", None, None, Some(5)).await {
241 Ok(v) => { let (p, _) = ok_rest!("get_funding_rate_history(linear, BTCUSDT, 5)", v); tally.passed += p as usize; }
242 Err(e) => { fail_rest!("get_funding_rate_history", e); tally.failed += 1; }
243 }
244
245 tally
246}
247
248async fn test_okx_rest() -> RestTally {
249 println!("\n── OKX REST ─────────────────────────────────────────────────");
250 let mut tally = RestTally { exchange: "OKX".into(), tested: 0, passed: 0, failed: 0 };
251
252 let conn = match OkxConnector::public(false).await {
253 Ok(c) => c,
254 Err(e) => {
255 println!(" FAIL: connector init -> {}", e);
256 tally.failed += 1;
257 tally.tested += 1;
258 return tally;
259 }
260 };
261
262 tally.tested += 1;
264 match conn.get_open_interest("SWAP", None, Some("BTC-USDT-SWAP")).await {
265 Ok(v) => { let (p, _) = ok_rest!("get_open_interest(SWAP, BTC-USDT-SWAP)", v); tally.passed += p as usize; }
266 Err(e) => { fail_rest!("get_open_interest", e); tally.failed += 1; }
267 }
268
269 tally.tested += 1;
271 match conn.get_long_short_ratio("BTC", Some("1H"), None, None, Some(10)).await {
272 Ok(v) => { let (p, _) = ok_rest!("get_long_short_ratio(BTC, 1H)", v); tally.passed += p as usize; }
273 Err(e) => { fail_rest!("get_long_short_ratio", e); tally.failed += 1; }
274 }
275
276 tally.tested += 1;
278 match conn.get_liquidation_orders("SWAP", Some("BTC-USDT"), Some("BTC-USDT-SWAP"), Some("filled"), None, None, Some(10)).await {
279 Ok(v) => { let (p, _) = ok_rest!("get_liquidation_orders(SWAP, BTC-USDT-SWAP)", v); tally.passed += p as usize; }
280 Err(e) => { fail_rest!("get_liquidation_orders", e); tally.failed += 1; }
281 }
282
283 tally.tested += 1;
285 match conn.get_mark_price("BTC-USDT-SWAP", "SWAP").await {
286 Ok(v) => { let (p, _) = ok_rest_single!("get_mark_price(BTC-USDT-SWAP)", v); tally.passed += p as usize; }
287 Err(e) => { fail_rest!("get_mark_price", e); tally.failed += 1; }
288 }
289
290 tally.tested += 1;
292 match conn.get_position_tiers("SWAP", "isolated", None, Some("BTC-USD"), Some("BTC-USD-SWAP"), None, None).await {
293 Ok(v) => { let (p, _) = ok_rest_single!("get_position_tiers(SWAP, isolated, BTC-USD-SWAP)", v); tally.passed += p as usize; }
294 Err(e) => { fail_rest!("get_position_tiers", e); tally.failed += 1; }
295 }
296
297 tally.tested += 1;
299 match conn.get_funding_rate_history("BTC-USDT-SWAP", None, None, Some(5)).await {
300 Ok(v) => { let (p, _) = ok_rest!("get_funding_rate_history(BTC-USDT-SWAP, 5)", v); tally.passed += p as usize; }
301 Err(e) => { fail_rest!("get_funding_rate_history", e); tally.failed += 1; }
302 }
303
304 tally
305}
306
307async fn test_hyperliquid_rest() -> RestTally {
308 println!("\n── Hyperliquid REST ─────────────────────────────────────────");
309 let mut tally = RestTally { exchange: "Hyperliquid".into(), tested: 0, passed: 0, failed: 0 };
310
311 let conn = match HyperliquidConnector::public(false).await {
312 Ok(c) => c,
313 Err(e) => {
314 println!(" FAIL: connector init -> {}", e);
315 tally.failed += 1;
316 tally.tested += 1;
317 return tally;
318 }
319 };
320
321 tally.tested += 1;
323 match conn.get_meta_and_asset_ctxs().await {
324 Ok(v) => {
325 let summary = if v.is_array() {
326 format!("[{} elements]", v.as_array().map(|a| a.len()).unwrap_or(0))
327 } else {
328 abbrev(&v)
329 };
330 println!(" OK: get_meta_and_asset_ctxs -> {}", summary);
331 tally.passed += 1;
332 }
333 Err(e) => { fail_rest!("get_meta_and_asset_ctxs", e); tally.failed += 1; }
334 }
335
336 tally.tested += 1;
338 match conn.get_predicted_fundings().await {
339 Ok(v) => {
340 let summary = if v.is_array() {
341 format!("[{} entries]", v.as_array().map(|a| a.len()).unwrap_or(0))
342 } else {
343 abbrev(&v)
344 };
345 println!(" OK: get_predicted_fundings -> {}", summary);
346 tally.passed += 1;
347 }
348 Err(e) => { fail_rest!("get_predicted_fundings", e); tally.failed += 1; }
349 }
350
351 tally.tested += 1;
353 match conn.get_spot_meta_and_asset_ctxs().await {
354 Ok(v) => {
355 let summary = if v.is_array() {
356 format!("[{} elements]", v.as_array().map(|a| a.len()).unwrap_or(0))
357 } else {
358 abbrev(&v)
359 };
360 println!(" OK: get_spot_meta_and_asset_ctxs -> {}", summary);
361 tally.passed += 1;
362 }
363 Err(e) => { fail_rest!("get_spot_meta_and_asset_ctxs", e); tally.failed += 1; }
364 }
365
366 tally.tested += 1;
368 let now_ms = std::time::SystemTime::now()
369 .duration_since(std::time::UNIX_EPOCH)
370 .map(|d| d.as_millis() as i64)
371 .unwrap_or(0);
372 match conn.get_non_funding_ledger_updates(
373 "0x0000000000000000000000000000000000000000",
374 0,
375 Some(now_ms),
376 ).await {
377 Ok(v) => {
378 let summary = if v.is_array() {
379 format!("[{} entries]", v.as_array().map(|a| a.len()).unwrap_or(0))
380 } else {
381 abbrev(&v)
382 };
383 println!(" OK: get_non_funding_ledger_updates(zero_addr) -> {}", summary);
384 tally.passed += 1;
385 }
386 Err(e) => { fail_rest!("get_non_funding_ledger_updates", e); tally.failed += 1; }
387 }
388
389 tally.tested += 1;
391 match conn.get_vault_details("0xa15099a30bbf2e68942d6f4c43d70d04faeab0a0").await {
392 Ok(v) => {
393 let summary = if v.is_null() { "null (unknown vault)".to_string() } else { abbrev(&v) };
394 println!(" OK: get_vault_details(HLP) -> {}", summary);
395 tally.passed += 1;
396 }
397 Err(e) => { fail_rest!("get_vault_details", e); tally.failed += 1; }
398 }
399
400 tally
401}
402
403async fn test_deribit_rest() -> RestTally {
404 println!("\n── Deribit REST ─────────────────────────────────────────────");
405 let mut tally = RestTally { exchange: "Deribit".into(), tested: 0, passed: 0, failed: 0 };
406
407 let conn = match DeribitConnector::public(false).await {
408 Ok(c) => c,
409 Err(e) => {
410 println!(" FAIL: connector init -> {}", e);
411 tally.failed += 1;
412 tally.tested += 1;
413 return tally;
414 }
415 };
416
417 tally.tested += 1;
419 match conn.get_index_price("btc_usd").await {
420 Ok(v) => { let (p, _) = ok_rest_single!("get_index_price(btc_usd)", v); tally.passed += p as usize; }
421 Err(e) => { fail_rest!("get_index_price", e); tally.failed += 1; }
422 }
423
424 tally.tested += 1;
426 match conn.get_historical_volatility("BTC").await {
427 Ok(v) => { let (p, _) = ok_rest_single!("get_historical_volatility(BTC)", v); tally.passed += p as usize; }
428 Err(e) => { fail_rest!("get_historical_volatility", e); tally.failed += 1; }
429 }
430
431 tally.tested += 1;
433 let now_ms = std::time::SystemTime::now()
434 .duration_since(std::time::UNIX_EPOCH)
435 .map(|d| d.as_millis() as i64)
436 .unwrap_or(0);
437 let start_ms = now_ms - 24 * 3600 * 1000;
438 match conn.get_funding_rate_history("BTC-PERPETUAL", start_ms, now_ms).await {
439 Ok(v) => { let (p, _) = ok_rest_single!("get_funding_rate_history(BTC-PERPETUAL, 24h)", v); tally.passed += p as usize; }
440 Err(e) => { fail_rest!("get_funding_rate_history", e); tally.failed += 1; }
441 }
442
443 tally
444}
445
446async fn test_bitget_rest() -> RestTally {
447 println!("\n── Bitget REST ──────────────────────────────────────────────");
448 let mut tally = RestTally { exchange: "Bitget".into(), tested: 0, passed: 0, failed: 0 };
449
450 let conn = match BitgetConnector::public().await {
451 Ok(c) => c,
452 Err(e) => {
453 println!(" FAIL: connector init -> {}", e);
454 tally.failed += 1;
455 tally.tested += 1;
456 return tally;
457 }
458 };
459
460 tally.tested += 1;
462 match conn.get_futures_open_interest("BTCUSDT", "USDT-FUTURES").await {
463 Ok(v) => { let (p, _) = ok_rest_single!("get_futures_open_interest(BTCUSDT, USDT-FUTURES)", v); tally.passed += p as usize; }
464 Err(e) => { fail_rest!("get_futures_open_interest", e); tally.failed += 1; }
465 }
466
467 tally.tested += 1;
469 match conn.get_futures_market_fills("BTCUSDT", "USDT-FUTURES", Some(10)).await {
470 Ok(v) => { let (p, _) = ok_rest_single!("get_futures_market_fills(BTCUSDT, USDT-FUTURES)", v); tally.passed += p as usize; }
471 Err(e) => { fail_rest!("get_futures_market_fills", e); tally.failed += 1; }
472 }
473
474 tally.tested += 1;
476 match conn.get_futures_mark_candles("BTCUSDT", "USDT-FUTURES", "1H", None, None, Some(5)).await {
477 Ok(v) => { let (p, _) = ok_rest_single!("get_futures_mark_candles(BTCUSDT, USDT-FUTURES, 1H)", v); tally.passed += p as usize; }
478 Err(e) => { fail_rest!("get_futures_mark_candles", e); tally.failed += 1; }
479 }
480
481 tally
482}
483
484async fn test_htx_rest() -> RestTally {
485 println!("\n── HTX REST ─────────────────────────────────────────────────");
486 let mut tally = RestTally { exchange: "HTX".into(), tested: 0, passed: 0, failed: 0 };
487
488 let conn = match HtxConnector::public(false).await {
489 Ok(c) => c,
490 Err(e) => {
491 println!(" FAIL: connector init -> {}", e);
492 tally.failed += 1;
493 tally.tested += 1;
494 return tally;
495 }
496 };
497
498 tally.tested += 1;
500 match conn.get_open_interest(Some("BTC-USDT")).await {
501 Ok(v) => { let (p, _) = ok_rest_single!("get_open_interest(BTC-USDT)", v); tally.passed += p as usize; }
502 Err(e) => { fail_rest!("get_open_interest", e); tally.failed += 1; }
503 }
504
505 tally.tested += 1;
507 match conn.get_mark_price("BTC-USDT").await {
508 Ok(v) => { let (p, _) = ok_rest_single!("get_mark_price(BTC-USDT)", v); tally.passed += p as usize; }
509 Err(e) => { fail_rest!("get_mark_price", e); tally.failed += 1; }
510 }
511
512 tally.tested += 1;
514 match conn.get_elite_account_ratio("BTC-USDT", "1hour").await {
515 Ok(v) => { let (p, _) = ok_rest_single!("get_elite_account_ratio(BTC-USDT, 1hour)", v); tally.passed += p as usize; }
516 Err(e) => { fail_rest!("get_elite_account_ratio", e); tally.failed += 1; }
517 }
518
519 tally.tested += 1;
521 match conn.get_historical_funding_rate("BTC-USDT", Some(1), Some(5)).await {
522 Ok(v) => { let (p, _) = ok_rest_single!("get_historical_funding_rate(BTC-USDT)", v); tally.passed += p as usize; }
523 Err(e) => { fail_rest!("get_historical_funding_rate", e); tally.failed += 1; }
524 }
525
526 tally
527}
528
529async fn test_kucoin_rest() -> RestTally {
530 println!("\n── KuCoin REST ──────────────────────────────────────────────");
531 let mut tally = RestTally { exchange: "KuCoin".into(), tested: 0, passed: 0, failed: 0 };
532
533 let conn = match KuCoinConnector::public(false).await {
534 Ok(c) => c,
535 Err(e) => {
536 println!(" FAIL: connector init -> {}", e);
537 tally.failed += 1;
538 tally.tested += 1;
539 return tally;
540 }
541 };
542
543 tally.tested += 1;
545 match conn.get_risk_limit("XBTUSDTM").await {
546 Ok(v) => { let (p, _) = ok_rest_single!("get_risk_limit(XBTUSDTM)", v); tally.passed += p as usize; }
547 Err(e) => { fail_rest!("get_risk_limit", e); tally.failed += 1; }
548 }
549
550 tally.tested += 1;
552 let now_ms = std::time::SystemTime::now()
553 .duration_since(std::time::UNIX_EPOCH)
554 .map(|d| d.as_millis() as i64)
555 .unwrap_or(0);
556 let from_ms = now_ms - 86_400_000;
557 match conn.get_historical_funding_rates("XBTUSDTM", Some(from_ms), Some(now_ms)).await {
558 Ok(v) => { let (p, _) = ok_rest_single!("get_historical_funding_rates(XBTUSDTM)", v); tally.passed += p as usize; }
559 Err(e) => { fail_rest!("get_historical_funding_rates", e); tally.failed += 1; }
560 }
561
562 tally
563}
564
565async fn test_gateio_rest() -> RestTally {
566 println!("\n── Gate.io REST ─────────────────────────────────────────────");
567 let mut tally = RestTally { exchange: "Gate.io".into(), tested: 0, passed: 0, failed: 0 };
568
569 let conn = match GateioConnector::public(false).await {
570 Ok(c) => c,
571 Err(e) => {
572 println!(" FAIL: connector init -> {}", e);
573 tally.failed += 1;
574 tally.tested += 1;
575 return tally;
576 }
577 };
578
579 tally.tested += 1;
581 match conn.get_contract_stats("BTC_USDT", None, None, Some("1h"), Some(5)).await {
582 Ok(v) => { let (p, _) = ok_rest_single!("get_contract_stats(BTC_USDT, 1h)", v); tally.passed += p as usize; }
583 Err(e) => { fail_rest!("get_contract_stats", e); tally.failed += 1; }
584 }
585
586 tally.tested += 1;
588 match conn.get_insurance_fund(Some(5)).await {
589 Ok(v) => { let (p, _) = ok_rest_single!("get_insurance_fund(5)", v); tally.passed += p as usize; }
590 Err(e) => { fail_rest!("get_insurance_fund", e); tally.failed += 1; }
591 }
592
593 tally
594}
595
596async fn test_dydx_rest() -> RestTally {
597 println!("\n── dYdX REST ────────────────────────────────────────────────");
598 let mut tally = RestTally { exchange: "dYdX".into(), tested: 0, passed: 0, failed: 0 };
599
600 let conn = match DydxConnector::public(false).await {
601 Ok(c) => c,
602 Err(e) => {
603 println!(" FAIL: connector init -> {}", e);
604 tally.failed += 1;
605 tally.tested += 1;
606 return tally;
607 }
608 };
609
610 tally.tested += 1;
612 match conn.get_markets().await {
613 Ok(v) => { let (p, _) = ok_rest_single!("get_markets()", v); tally.passed += p as usize; }
614 Err(e) => { fail_rest!("get_markets", e); tally.failed += 1; }
615 }
616
617 tally.tested += 1;
619 match conn.get_historical_funding("BTC-USD", Some(5)).await {
620 Ok(v) => { let (p, _) = ok_rest!("get_historical_funding(BTC-USD, 5)", v); tally.passed += p as usize; }
621 Err(e) => { fail_rest!("get_historical_funding", e); tally.failed += 1; }
622 }
623
624 tally
625}
626
627async fn test_lighter_rest() -> RestTally {
628 println!("\n── Lighter REST ─────────────────────────────────────────────");
629 println!(" SKIP: Lighter mainnet TCP unreachable from this host (geo/firewall) — skipping all REST");
630 RestTally { exchange: "Lighter".into(), tested: 0, passed: 0, failed: 0 }
631}
632
633async fn test_bitfinex_rest() -> RestTally {
634 println!("\n── Bitfinex REST ────────────────────────────────────────────");
635 let mut tally = RestTally { exchange: "Bitfinex".into(), tested: 0, passed: 0, failed: 0 };
636
637 let conn = match BitfinexConnector::public(false).await {
638 Ok(c) => c,
639 Err(e) => {
640 println!(" FAIL: connector init -> {}", e);
641 tally.failed += 1;
642 tally.tested += 1;
643 return tally;
644 }
645 };
646
647 tally.tested += 1;
649 match conn.get_derivative_status_history("tBTCF0:USTF0", None, None, Some(3), None).await {
650 Ok(v) => { let (p, _) = ok_rest_single!("get_derivative_status_history(tBTCF0:USTF0, 3)", v); tally.passed += p as usize; }
651 Err(e) => { fail_rest!("get_derivative_status_history", e); tally.failed += 1; }
652 }
653
654 tally.tested += 1;
656 match conn.get_funding_stats("fUSD", Some(3), None, None).await {
657 Ok(v) => { let (p, _) = ok_rest_single!("get_funding_stats(fUSD, 3)", v); tally.passed += p as usize; }
658 Err(e) => { fail_rest!("get_funding_stats", e); tally.failed += 1; }
659 }
660
661 tally
662}
663
664async fn test_kraken_rest() -> RestTally {
665 println!("\n── Kraken REST ──────────────────────────────────────────────");
666 let mut tally = RestTally { exchange: "Kraken".into(), tested: 0, passed: 0, failed: 0 };
667
668 let conn = match KrakenConnector::public(false).await {
669 Ok(c) => c,
670 Err(e) => {
671 println!(" FAIL: connector init -> {}", e);
672 tally.failed += 1;
673 tally.tested += 1;
674 return tally;
675 }
676 };
677
678 tally.tested += 1;
680 match conn.get_futures_open_interest(Some("PF_XBTUSD")).await {
681 Ok(v) => { let (p, _) = ok_rest_single!("get_futures_open_interest(PF_XBTUSD)", v); tally.passed += p as usize; }
682 Err(e) => { fail_rest!("get_futures_open_interest", e); tally.failed += 1; }
683 }
684
685 tally
686}
687
688async fn test_gemini_rest() -> RestTally {
689 println!("\n── Gemini REST ──────────────────────────────────────────────");
690 let mut tally = RestTally { exchange: "Gemini".into(), tested: 0, passed: 0, failed: 0 };
691
692 let conn = match GeminiConnector::public(false).await {
693 Ok(c) => c,
694 Err(e) => {
695 println!(" FAIL: connector init -> {}", e);
696 tally.failed += 1;
697 tally.tested += 1;
698 return tally;
699 }
700 };
701
702 tally.tested += 1;
704 match conn.get_trades_with_breaks("btcusd", Some(3), None).await {
705 Ok(v) => { let (p, _) = ok_rest_single!("get_trades_with_breaks(btcusd, 3)", v); tally.passed += p as usize; }
706 Err(e) => { fail_rest!("get_trades_with_breaks", e); tally.failed += 1; }
707 }
708
709 tally
710}
711
712async fn test_bitstamp_rest() -> RestTally {
713 println!("\n── Bitstamp REST ────────────────────────────────────────────");
714 let mut tally = RestTally { exchange: "Bitstamp".into(), tested: 0, passed: 0, failed: 0 };
715
716 let conn = match BitstampConnector::public().await {
717 Ok(c) => c,
718 Err(e) => {
719 println!(" FAIL: connector init -> {}", e);
720 tally.failed += 1;
721 tally.tested += 1;
722 return tally;
723 }
724 };
725
726 tally.tested += 1;
728 match conn.get_markets().await {
729 Ok(v) => { let (p, _) = ok_rest_single!("get_markets()", v); tally.passed += p as usize; }
730 Err(e) => { fail_rest!("get_markets", e); tally.failed += 1; }
731 }
732
733 tally
734}
735
736async fn test_upbit_rest() -> RestTally {
737 println!("\n── Upbit REST ───────────────────────────────────────────────");
738 let mut tally = RestTally { exchange: "Upbit".into(), tested: 0, passed: 0, failed: 0 };
739
740 let conn = match UpbitConnector::public().await {
741 Ok(c) => c,
742 Err(e) => {
743 println!(" FAIL: connector init -> {}", e);
744 tally.failed += 1;
745 tally.tested += 1;
746 return tally;
747 }
748 };
749
750 tally.tested += 1;
752 match conn.get_markets_with_warnings().await {
753 Ok(v) => {
754 println!(" OK: get_markets_with_warnings() -> {} items", v.len());
755 tally.passed += 1;
756 }
757 Err(e) => { fail_rest!("get_markets_with_warnings", e); tally.failed += 1; }
758 }
759
760 tally
761}
762
763async fn test_crypto_com_rest() -> RestTally {
764 println!("\n── Crypto.com REST ──────────────────────────────────────────");
765 let mut tally = RestTally { exchange: "Crypto.com".into(), tested: 0, passed: 0, failed: 0 };
766
767 let conn = match CryptoComConnector::public(false).await {
768 Ok(c) => c,
769 Err(e) => {
770 println!(" FAIL: connector init -> {}", e);
771 tally.failed += 1;
772 tally.tested += 1;
773 return tally;
774 }
775 };
776
777 tally.tested += 1;
779 match conn.get_expired_settlement_price("PERPETUAL_SWAP").await {
780 Ok(v) => { let (p, _) = ok_rest_single!("get_expired_settlement_price(PERPETUAL_SWAP)", v); tally.passed += p as usize; }
781 Err(e) => { fail_rest!("get_expired_settlement_price", e); tally.failed += 1; }
782 }
783
784 tally.tested += 1;
786 match conn.get_insurance("BTCUSD-PERP").await {
787 Ok(v) => { let (p, _) = ok_rest_single!("get_insurance(BTCUSD-PERP)", v); tally.passed += p as usize; }
788 Err(e) => { fail_rest!("get_insurance", e); tally.failed += 1; }
789 }
790
791 tally
792}
793
794async fn test_bingx_rest() -> RestTally {
795 println!("\n── BingX REST ───────────────────────────────────────────────");
796 let mut tally = RestTally { exchange: "BingX".into(), tested: 0, passed: 0, failed: 0 };
797
798 let conn = match BingxConnector::public(false).await {
799 Ok(c) => c,
800 Err(e) => {
801 println!(" FAIL: connector init -> {}", e);
802 tally.failed += 1;
803 tally.tested += 1;
804 return tally;
805 }
806 };
807
808 tally.tested += 1;
810 match conn.swap_open_interest("BTC-USDT").await {
811 Ok(v) => { let (p, _) = ok_rest_single!("swap_open_interest(BTC-USDT)", v); tally.passed += p as usize; }
812 Err(e) => { fail_rest!("swap_open_interest", e); tally.failed += 1; }
813 }
814
815 tally.tested += 1;
817 match conn.swap_premium_index(Some("BTC-USDT")).await {
818 Ok(v) => { let (p, _) = ok_rest_single!("swap_premium_index(BTC-USDT)", v); tally.passed += p as usize; }
819 Err(e) => { fail_rest!("swap_premium_index", e); tally.failed += 1; }
820 }
821
822 tally
823}
824
825fn test_mexc_note() -> RestTally {
826 println!("\n── MEXC REST ────────────────────────────────────────────────");
827 println!(" SKIPPED: MEXC geo-blocked from this IP (confirmed by E2D agent)");
828 RestTally { exchange: "MEXC".into(), tested: 0, passed: 0, failed: 0 }
829}
830
831async fn ws_listen<W>(
838 ws: &W,
839 request: SubscriptionRequest,
840 duration: Duration,
841 channel_label: &str,
842) -> (bool, usize, usize, String)
843where
844 W: WebSocketConnector,
845{
846 match ws.subscribe(request).await {
847 Err(e) => {
848 println!(" FAIL subscribe {} -> {}", channel_label, e);
849 return (false, 0, 0, channel_label.to_string());
850 }
851 Ok(_) => {}
852 }
853
854 let mut stream = ws.event_stream();
855 let mut count = 0usize;
856 let mut errors = 0usize;
857
858 let result = timeout(duration, async {
859 while let Some(item) = stream.next().await {
860 match item {
861 Ok(_) => count += 1,
862 Err(_) => errors += 1,
863 }
864 }
865 }).await;
866
867 let _ = result;
868
869 println!(
870 " CH {} -> events={}, errors={}{}",
871 channel_label,
872 count,
873 errors,
874 if count == 0 { " [ZERO EVENTS]" } else { "" }
875 );
876
877 (true, count, errors, channel_label.to_string())
878}
879
880async fn test_binance_ws() -> WsTally {
881 println!("\n── Binance WS ───────────────────────────────────────────────");
882 let mut tally = WsTally {
883 exchange: "Binance".into(),
884 channels: 0,
885 subscribed: 0,
886 events: 0,
887 parse_errors: 0,
888 zero_event_channels: Vec::new(),
889 };
890
891 let duration = Duration::from_secs(5);
892 let btc_futures = Symbol::new("BTC", "USDT");
893
894 {
896 tally.channels += 1;
897 let ws = match BinanceWebSocket::new(None, false, AccountType::FuturesCross).await {
898 Ok(w) => w,
899 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
900 };
901 if ws.connect(AccountType::FuturesCross).await.is_ok() {
902 let req = SubscriptionRequest::new(btc_futures.clone(), StreamType::Liquidation);
903 let (ok, n, err, label) = ws_listen(&ws, req, duration, "btcusdt@forceOrder").await;
904 if ok { tally.subscribed += 1; }
905 tally.events += n;
906 tally.parse_errors += err;
907 if ok && n == 0 { tally.zero_event_channels.push(label); }
908 let _ = ws.disconnect().await;
909 } else {
910 println!(" FAIL: Binance WS connect (futures)");
911 }
912 }
913
914 {
916 tally.channels += 1;
917 let ws = match BinanceWebSocket::new(None, false, AccountType::FuturesCross).await {
918 Ok(w) => w,
919 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
920 };
921 if ws.connect(AccountType::FuturesCross).await.is_ok() {
922 let req = SubscriptionRequest::new(btc_futures.clone(), StreamType::AggTrade);
923 let (ok, n, err, label) = ws_listen(&ws, req, duration, "btcusdt@aggTrade").await;
924 if ok { tally.subscribed += 1; }
925 tally.events += n;
926 tally.parse_errors += err;
927 if ok && n == 0 { tally.zero_event_channels.push(label); }
928 let _ = ws.disconnect().await;
929 }
930 }
931
932 {
934 tally.channels += 1;
935 let ws = match BinanceWebSocket::new(None, false, AccountType::FuturesCross).await {
936 Ok(w) => w,
937 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
938 };
939 if ws.connect(AccountType::FuturesCross).await.is_ok() {
940 let req = SubscriptionRequest::new(btc_futures.clone(), StreamType::MarkPriceKline { interval: "1m".to_string() });
941 let (ok, n, err, label) = ws_listen(&ws, req, duration, "btcusdt@markPriceKline_1m").await;
942 if ok { tally.subscribed += 1; }
943 tally.events += n;
944 tally.parse_errors += err;
945 if ok && n == 0 { tally.zero_event_channels.push(label); }
946 let _ = ws.disconnect().await;
947 }
948 }
949
950 {
952 tally.channels += 1;
953 let ws = match BinanceWebSocket::new(None, false, AccountType::FuturesCross).await {
954 Ok(w) => w,
955 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
956 };
957 if ws.connect(AccountType::FuturesCross).await.is_ok() {
958 let req = SubscriptionRequest::new(Symbol::empty(), StreamType::CompositeIndex);
959 let (ok, n, err, label) = ws_listen(&ws, req, duration, "!compositeIndex@arr").await;
960 if ok { tally.subscribed += 1; }
961 tally.events += n;
962 tally.parse_errors += err;
963 if ok && n == 0 { tally.zero_event_channels.push(label); }
964 let _ = ws.disconnect().await;
965 }
966 }
967
968 {
970 tally.channels += 1;
971 let ws = match BinanceWebSocket::new(None, false, AccountType::FuturesCross).await {
972 Ok(w) => w,
973 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
974 };
975 if ws.connect(AccountType::FuturesCross).await.is_ok() {
976 let req = SubscriptionRequest::new(Symbol::empty(), StreamType::Liquidation);
978 let (ok, n, err, label) = ws_listen(&ws, req, duration, "!forceOrder@arr (global)").await;
979 if ok { tally.subscribed += 1; }
980 tally.events += n;
981 tally.parse_errors += err;
982 if ok && n == 0 { tally.zero_event_channels.push(label); }
983 let _ = ws.disconnect().await;
984 }
985 }
986
987 tally
988}
989
990async fn test_bybit_ws() -> WsTally {
991 println!("\n── Bybit WS ─────────────────────────────────────────────────");
992 let mut tally = WsTally {
993 exchange: "Bybit".into(),
994 channels: 0,
995 subscribed: 0,
996 events: 0,
997 parse_errors: 0,
998 zero_event_channels: Vec::new(),
999 };
1000
1001 let duration = Duration::from_secs(5);
1002 let btc = Symbol::new("BTC", "USDT");
1003
1004 {
1006 tally.channels += 1;
1007 let ws = match BybitWebSocket::new(None, false, AccountType::FuturesCross).await {
1008 Ok(w) => w,
1009 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1010 };
1011 if ws.connect(AccountType::FuturesCross).await.is_ok() {
1012 let req = SubscriptionRequest::new(btc.clone(), StreamType::Ticker);
1013 let (ok, n, err, label) = ws_listen(&ws, req, duration, "tickers.BTCUSDT(linear)").await;
1014 if ok { tally.subscribed += 1; }
1015 tally.events += n;
1016 tally.parse_errors += err;
1017 if ok && n == 0 { tally.zero_event_channels.push(label); }
1018 let _ = ws.disconnect().await;
1019 }
1020 }
1021
1022 {
1024 tally.channels += 1;
1025 let ws = match BybitWebSocket::new(None, false, AccountType::FuturesCross).await {
1026 Ok(w) => w,
1027 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1028 };
1029 if ws.connect(AccountType::FuturesCross).await.is_ok() {
1030 let req = SubscriptionRequest::new(btc.clone(), StreamType::Liquidation);
1031 let (ok, n, err, label) = ws_listen(&ws, req, duration, "liquidation.BTCUSDT").await;
1032 if ok { tally.subscribed += 1; }
1033 tally.events += n;
1034 tally.parse_errors += err;
1035 if ok && n == 0 { tally.zero_event_channels.push(label); }
1036 let _ = ws.disconnect().await;
1037 }
1038 }
1039
1040 {
1042 tally.channels += 1;
1043 let ws = match BybitWebSocket::new(None, false, AccountType::FuturesCross).await {
1044 Ok(w) => w,
1045 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1046 };
1047 if ws.connect(AccountType::FuturesCross).await.is_ok() {
1048 let req = SubscriptionRequest::new(Symbol::new("USDT", ""), StreamType::InsuranceFund);
1049 let (ok, n, err, label) = ws_listen(&ws, req, duration, "insurance.USDT").await;
1050 if ok { tally.subscribed += 1; }
1051 tally.events += n;
1052 tally.parse_errors += err;
1053 if ok && n == 0 { tally.zero_event_channels.push(label); }
1054 let _ = ws.disconnect().await;
1055 }
1056 }
1057
1058 {
1061 tally.channels += 1;
1062 let ws = match BybitWebSocket::new(None, false, AccountType::FuturesCross).await {
1063 Ok(w) => w,
1064 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1065 };
1066 if ws.connect(AccountType::FuturesCross).await.is_ok() {
1067 let req = SubscriptionRequest::new(Symbol::new("USDT", ""), StreamType::RiskLimit);
1069 let (ok, n, err, label) = ws_listen(&ws, req, duration, "adlAlert.USDT").await;
1070 if ok { tally.subscribed += 1; }
1071 tally.events += n;
1072 tally.parse_errors += err;
1073 if ok && n == 0 { tally.zero_event_channels.push(label); }
1074 let _ = ws.disconnect().await;
1075 }
1076 }
1077
1078 tally
1079}
1080
1081async fn test_okx_ws() -> WsTally {
1082 println!("\n── OKX WS ───────────────────────────────────────────────────");
1083 let mut tally = WsTally {
1084 exchange: "OKX".into(),
1085 channels: 0,
1086 subscribed: 0,
1087 events: 0,
1088 parse_errors: 0,
1089 zero_event_channels: Vec::new(),
1090 };
1091
1092 let duration = Duration::from_secs(5);
1093 let btc_swap = Symbol::new("BTC", "USDT");
1094
1095 {
1097 tally.channels += 1;
1098 let ws = match OkxWebSocket::new(None, false, AccountType::Spot).await {
1099 Ok(w) => w,
1100 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1101 };
1102 let mut req = SubscriptionRequest::new(btc_swap.clone(), StreamType::Ticker);
1103 req.account_type = AccountType::FuturesCross;
1104 if ws.connect(AccountType::FuturesCross).await.is_ok() {
1105 let (ok, n, err, label) = ws_listen(&ws, req, duration, "tickers BTC-USDT-SWAP").await;
1106 if ok { tally.subscribed += 1; }
1107 tally.events += n;
1108 tally.parse_errors += err;
1109 if ok && n == 0 { tally.zero_event_channels.push(label); }
1110 let _ = ws.disconnect().await;
1111 }
1112 }
1113
1114 {
1116 tally.channels += 1;
1117 let ws = match OkxWebSocket::new(None, false, AccountType::Spot).await {
1118 Ok(w) => w,
1119 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1120 };
1121 let mut req = SubscriptionRequest::new(btc_swap.clone(), StreamType::Liquidation);
1122 req.account_type = AccountType::FuturesCross;
1123 if ws.connect(AccountType::FuturesCross).await.is_ok() {
1124 let (ok, n, err, label) = ws_listen(&ws, req, duration, "liquidation-orders BTC-USDT-SWAP").await;
1125 if ok { tally.subscribed += 1; }
1126 tally.events += n;
1127 tally.parse_errors += err;
1128 if ok && n == 0 { tally.zero_event_channels.push(label); }
1129 let _ = ws.disconnect().await;
1130 }
1131 }
1132
1133 {
1135 tally.channels += 1;
1136 let ws = match OkxWebSocket::new(None, false, AccountType::Spot).await {
1137 Ok(w) => w,
1138 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1139 };
1140 let mut req = SubscriptionRequest::new(btc_swap.clone(), StreamType::IndexPrice);
1141 req.account_type = AccountType::Spot;
1142 if ws.connect(AccountType::Spot).await.is_ok() {
1143 let (ok, n, err, label) = ws_listen(&ws, req, duration, "index-tickers BTC-USDT").await;
1144 if ok { tally.subscribed += 1; }
1145 tally.events += n;
1146 tally.parse_errors += err;
1147 if ok && n == 0 { tally.zero_event_channels.push(label); }
1148 let _ = ws.disconnect().await;
1149 }
1150 }
1151
1152 {
1154 tally.channels += 1;
1155 let ws = match OkxWebSocket::new(None, false, AccountType::Spot).await {
1156 Ok(w) => w,
1157 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1158 };
1159 let mut req = SubscriptionRequest::new(btc_swap.clone(), StreamType::MarkPriceKline { interval: "1m".to_string() });
1160 req.account_type = AccountType::FuturesCross;
1161 if ws.connect(AccountType::FuturesCross).await.is_ok() {
1162 let (ok, n, err, label) = ws_listen(&ws, req, duration, "mark-price-candle1m BTC-USDT-SWAP").await;
1163 if ok { tally.subscribed += 1; }
1164 tally.events += n;
1165 tally.parse_errors += err;
1166 if ok && n == 0 { tally.zero_event_channels.push(label); }
1167 let _ = ws.disconnect().await;
1168 }
1169 }
1170
1171 {
1173 tally.channels += 1;
1174 let ws = match OkxWebSocket::new(None, false, AccountType::Spot).await {
1175 Ok(w) => w,
1176 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1177 };
1178 let mut req = SubscriptionRequest::new(btc_swap.clone(), StreamType::BlockTrade);
1179 req.account_type = AccountType::FuturesCross;
1180 if ws.connect(AccountType::FuturesCross).await.is_ok() {
1181 let (ok, n, err, label) = ws_listen(&ws, req, duration, "block-trades BTC-USDT-SWAP").await;
1182 if ok { tally.subscribed += 1; }
1183 tally.events += n;
1184 tally.parse_errors += err;
1185 if ok && n == 0 { tally.zero_event_channels.push(label); }
1186 let _ = ws.disconnect().await;
1187 }
1188 }
1189
1190 {
1192 tally.channels += 1;
1193 let ws = match OkxWebSocket::new(None, false, AccountType::Spot).await {
1194 Ok(w) => w,
1195 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1196 };
1197 let mut req = SubscriptionRequest::new(Symbol::new("BTC", "USD"), StreamType::SettlementEvent);
1199 req.account_type = AccountType::FuturesCross;
1200 if ws.connect(AccountType::FuturesCross).await.is_ok() {
1201 let (ok, n, err, label) = ws_listen(&ws, req, duration, "estimated-price BTC-USD (OPTIONS)").await;
1202 if ok { tally.subscribed += 1; }
1203 tally.events += n;
1204 tally.parse_errors += err;
1205 if ok && n == 0 { tally.zero_event_channels.push(label); }
1206 let _ = ws.disconnect().await;
1207 }
1208 }
1209
1210 tally
1211}
1212
1213async fn test_hyperliquid_ws() -> WsTally {
1214 println!("\n── Hyperliquid WS ───────────────────────────────────────────");
1215 let mut tally = WsTally {
1216 exchange: "Hyperliquid".into(),
1217 channels: 0,
1218 subscribed: 0,
1219 events: 0,
1220 parse_errors: 0,
1221 zero_event_channels: Vec::new(),
1222 };
1223
1224 let duration = Duration::from_secs(5);
1225 let btc = Symbol::new("BTC", "");
1226
1227 {
1229 tally.channels += 1;
1230 let ws = HyperliquidWebSocket::new(false);
1231 if ws.connect(AccountType::FuturesCross).await.is_ok() {
1232 let req = SubscriptionRequest::new(btc.clone(), StreamType::Ticker);
1233 match ws.subscribe(req).await {
1234 Ok(_) => {
1235 tally.subscribed += 1;
1236 let mut stream = ws.event_stream();
1237 let mut n = 0usize;
1238 let mut errors = 0usize;
1239 let _ = timeout(duration, async {
1240 while let Some(item) = stream.next().await {
1241 match item { Ok(_) => n += 1, Err(_) => errors += 1 }
1242 }
1243 }).await;
1244 tally.events += n;
1245 tally.parse_errors += errors;
1246 let label = "activeAssetCtx BTC".to_string();
1247 println!(
1248 " CH {} -> events={}, errors={}{}",
1249 label, n, errors,
1250 if n == 0 { " [ZERO EVENTS]" } else { "" }
1251 );
1252 if n == 0 { tally.zero_event_channels.push(label); }
1253 }
1254 Err(e) => {
1255 println!(" FAIL subscribe activeAssetCtx BTC -> {} [known: mutex deadlock in HL WS]", e);
1256 }
1257 }
1258 let _ = ws.disconnect().await;
1259 } else {
1260 println!(" FAIL: Hyperliquid WS connect");
1261 }
1262 }
1263
1264 {
1266 tally.channels += 1;
1267 let ws = HyperliquidWebSocket::new(false);
1268 if ws.connect(AccountType::FuturesCross).await.is_ok() {
1269 match ws.subscribe_all_mids().await {
1270 Ok(_) => {
1271 tally.subscribed += 1;
1272 let mut stream = ws.event_stream();
1273 let mut n = 0usize;
1274 let mut errors = 0usize;
1275 let _ = timeout(duration, async {
1276 while let Some(item) = stream.next().await {
1277 match item { Ok(_) => n += 1, Err(_) => errors += 1 }
1278 }
1279 }).await;
1280 tally.events += n;
1281 tally.parse_errors += errors;
1282 let label = "allMids".to_string();
1283 println!(
1284 " CH {} -> events={}, errors={}{}",
1285 label, n, errors,
1286 if n == 0 { " [ZERO EVENTS]" } else { "" }
1287 );
1288 if n == 0 { tally.zero_event_channels.push(label); }
1289 }
1290 Err(e) => {
1291 println!(" FAIL subscribe allMids -> {}", e);
1292 }
1293 }
1294 let _ = ws.disconnect().await;
1295 } else {
1296 println!(" FAIL: Hyperliquid WS connect (allMids)");
1297 }
1298 }
1299
1300 tally
1301}
1302
1303async fn test_deribit_ws() -> WsTally {
1304 println!("\n── Deribit WS ───────────────────────────────────────────────");
1305 let mut tally = WsTally {
1306 exchange: "Deribit".into(),
1307 channels: 0,
1308 subscribed: 0,
1309 events: 0,
1310 parse_errors: 0,
1311 zero_event_channels: Vec::new(),
1312 };
1313
1314 let duration = Duration::from_secs(5);
1315 let btc_perp = Symbol::new("BTC", "PERPETUAL");
1316
1317 {
1319 tally.channels += 1;
1320 let ws = match DeribitWebSocket::new(None, false, AccountType::FuturesCross).await {
1321 Ok(w) => w,
1322 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1323 };
1324 if ws.connect(AccountType::FuturesCross).await.is_ok() {
1325 let req = SubscriptionRequest::new(btc_perp.clone(), StreamType::Ticker);
1326 let (ok, n, err, label) = ws_listen(&ws, req, duration, "ticker.BTC-PERPETUAL.raw").await;
1327 if ok { tally.subscribed += 1; }
1328 tally.events += n;
1329 tally.parse_errors += err;
1330 if ok && n == 0 { tally.zero_event_channels.push(label); }
1331 let _ = ws.disconnect().await;
1332 } else {
1333 println!(" FAIL: Deribit WS connect");
1334 }
1335 }
1336
1337 {
1339 tally.channels += 1;
1340 let ws = match DeribitWebSocket::new(None, false, AccountType::FuturesCross).await {
1341 Ok(w) => w,
1342 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1343 };
1344 if ws.connect(AccountType::FuturesCross).await.is_ok() {
1345 let req = SubscriptionRequest::new(Symbol::new("BTC", ""), StreamType::VolatilityIndex);
1346 match ws.subscribe(req).await {
1347 Ok(_) => {
1348 tally.subscribed += 1;
1349 let mut stream = ws.event_stream();
1350 let mut n = 0usize;
1351 let mut errors = 0usize;
1352 let _ = timeout(duration, async {
1353 while let Some(item) = stream.next().await {
1354 match item { Ok(_) => n += 1, Err(_) => errors += 1 }
1355 }
1356 }).await;
1357 tally.events += n;
1358 tally.parse_errors += errors;
1359 let label = "deribit_volatility_index.btc_usd".to_string();
1360 println!(
1361 " CH {} -> events={}, errors={}{}",
1362 label, n, errors,
1363 if n == 0 { " [ZERO EVENTS]" } else { "" }
1364 );
1365 if n == 0 { tally.zero_event_channels.push(label); }
1366 }
1367 Err(e) => println!(" FAIL subscribe deribit_volatility_index.btc_usd -> {}", e),
1368 }
1369 let _ = ws.disconnect().await;
1370 }
1371 }
1372
1373 {
1375 tally.channels += 1;
1376 let ws = match DeribitWebSocket::new(None, false, AccountType::FuturesCross).await {
1377 Ok(w) => w,
1378 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1379 };
1380 let _ = ws.disconnect().await;
1385 }
1386
1387 {
1389 tally.channels += 1;
1390 let ws = match DeribitWebSocket::new(None, false, AccountType::FuturesCross).await {
1391 Ok(w) => w,
1392 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1393 };
1394 if ws.connect(AccountType::FuturesCross).await.is_ok() {
1395 let req = SubscriptionRequest::new(Symbol::empty(), StreamType::BlockTrade);
1396 match ws.subscribe(req).await {
1397 Ok(_) => {
1398 tally.subscribed += 1;
1399 let mut stream = ws.event_stream();
1400 let mut n = 0usize;
1401 let mut errors = 0usize;
1402 let _ = timeout(duration, async {
1403 while let Some(item) = stream.next().await {
1404 match item { Ok(_) => n += 1, Err(_) => errors += 1 }
1405 }
1406 }).await;
1407 tally.events += n;
1408 tally.parse_errors += errors;
1409 let label = "block_trade_confirmations".to_string();
1410 println!(
1411 " CH {} -> events={}, errors={}{}",
1412 label, n, errors,
1413 if n == 0 { " [ZERO EVENTS]" } else { "" }
1414 );
1415 if n == 0 { tally.zero_event_channels.push(label); }
1416 }
1417 Err(e) => println!(" FAIL subscribe block_trade_confirmations -> {}", e),
1418 }
1419 let _ = ws.disconnect().await;
1420 }
1421 }
1422
1423 tally
1424}
1425
1426async fn test_htx_ws() -> WsTally {
1427 println!("\n── HTX WS ───────────────────────────────────────────────────");
1428 let mut tally = WsTally {
1429 exchange: "HTX".into(),
1430 channels: 0,
1431 subscribed: 0,
1432 events: 0,
1433 parse_errors: 0,
1434 zero_event_channels: Vec::new(),
1435 };
1436
1437 let duration = Duration::from_secs(10);
1441
1442 {
1444 tally.channels += 1;
1445 let ws_result = HtxWebSocket::new(None, false, AccountType::FuturesCross);
1446 let ws = match ws_result {
1447 Ok(w) => w,
1448 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1449 };
1450 if ws.connect(AccountType::FuturesCross).await.is_ok() {
1451 let req = SubscriptionRequest::new(
1452 Symbol::new("BTC", "USDT"),
1453 StreamType::Kline { interval: "1min".to_string() },
1454 );
1455 let (ok, n, err, label) = ws_listen(&ws, req, duration, "market.BTC-USDT.kline.1min").await;
1456 if ok { tally.subscribed += 1; }
1457 tally.events += n;
1458 tally.parse_errors += err;
1459 if ok && n == 0 { tally.zero_event_channels.push(label); }
1460 let _ = ws.disconnect().await;
1461 } else {
1462 println!(" FAIL: HTX WS connect");
1463 }
1464 }
1465
1466 tally
1467}
1468
1469async fn test_kucoin_ws() -> WsTally {
1470 println!("\n── KuCoin WS ────────────────────────────────────────────────");
1471 let mut tally = WsTally {
1472 exchange: "KuCoin".into(),
1473 channels: 0,
1474 subscribed: 0,
1475 events: 0,
1476 parse_errors: 0,
1477 zero_event_channels: Vec::new(),
1478 };
1479
1480 let duration = Duration::from_secs(5);
1481
1482 {
1484 tally.channels += 1;
1485 let ws_result = KuCoinWebSocket::new(None, false, AccountType::FuturesCross).await;
1486 let ws = match ws_result {
1487 Ok(w) => w,
1488 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1489 };
1490 if ws.connect(AccountType::FuturesCross).await.is_ok() {
1491 let mut req = SubscriptionRequest::new(
1493 Symbol::new("BTC", "USDT"),
1494 StreamType::IndexPrice,
1495 );
1496 req.account_type = AccountType::FuturesCross;
1497 let (ok, n, err, label) = ws_listen(&ws, req, duration, "/contractMarket/indexPrice:XBTUSDTM").await;
1498 if ok { tally.subscribed += 1; }
1499 tally.events += n;
1500 tally.parse_errors += err;
1501 if ok && n == 0 { tally.zero_event_channels.push(label); }
1502 let _ = ws.disconnect().await;
1503 } else {
1504 println!(" FAIL: KuCoin WS connect");
1505 }
1506 }
1507
1508 tally
1509}
1510
1511async fn test_gateio_ws() -> WsTally {
1512 println!("\n── Gate.io WS ───────────────────────────────────────────────");
1513 let mut tally = WsTally {
1514 exchange: "Gate.io".into(),
1515 channels: 0,
1516 subscribed: 0,
1517 events: 0,
1518 parse_errors: 0,
1519 zero_event_channels: Vec::new(),
1520 };
1521
1522 let duration = Duration::from_secs(10);
1526
1527 {
1529 tally.channels += 1;
1530 let ws_result = GateioWebSocket::new(None, false, AccountType::FuturesCross).await;
1531 let ws = match ws_result {
1532 Ok(w) => w,
1533 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1534 };
1535 if ws.connect(AccountType::FuturesCross).await.is_ok() {
1536 let req = SubscriptionRequest::new(
1537 Symbol::new("BTC", "USDT"),
1538 StreamType::Kline { interval: "1m".to_string() },
1539 );
1540 let (ok, n, err, label) = ws_listen(&ws, req, duration, "futures.candlesticks BTC_USDT 1m").await;
1541 if ok { tally.subscribed += 1; }
1542 tally.events += n;
1543 tally.parse_errors += err;
1544 if ok && n == 0 { tally.zero_event_channels.push(label); }
1545 let _ = ws.disconnect().await;
1546 } else {
1547 println!(" FAIL: Gate.io WS connect");
1548 }
1549 }
1550
1551 tally
1552}
1553
1554async fn test_crypto_com_ws() -> WsTally {
1555 println!("\n── Crypto.com WS ────────────────────────────────────────────");
1556 let mut tally = WsTally {
1557 exchange: "Crypto.com".into(),
1558 channels: 0,
1559 subscribed: 0,
1560 events: 0,
1561 parse_errors: 0,
1562 zero_event_channels: Vec::new(),
1563 };
1564
1565 let duration = Duration::from_secs(5);
1566 let btcusd_perp = Symbol::new("BTCUSD", "PERP");
1567
1568 {
1570 tally.channels += 1;
1571 let ws = CryptoComWebSocket::new(None, false);
1572 if ws.connect().await.is_ok() {
1574 let req = SubscriptionRequest::new(btcusd_perp.clone(), StreamType::PredictedFunding);
1575 let (ok, n, err, label) = ws_listen(&ws, req, duration, "estimatedfunding.BTCUSD-PERP").await;
1576 if ok { tally.subscribed += 1; }
1577 tally.events += n;
1578 tally.parse_errors += err;
1579 if ok && n == 0 { tally.zero_event_channels.push(label); }
1580 let _ = ws.disconnect().await;
1581 } else {
1582 println!(" FAIL: Crypto.com WS connect");
1583 }
1584 }
1585
1586 {
1588 tally.channels += 1;
1589 let ws = CryptoComWebSocket::new(None, false);
1590 if ws.connect().await.is_ok() {
1591 let req = SubscriptionRequest::new(btcusd_perp.clone(), StreamType::SettlementEvent);
1592 let (ok, n, err, label) = ws_listen(&ws, req, duration, "settlement.BTCUSD-PERP").await;
1593 if ok { tally.subscribed += 1; }
1594 tally.events += n;
1595 tally.parse_errors += err;
1596 if ok && n == 0 { tally.zero_event_channels.push(label); }
1597 let _ = ws.disconnect().await;
1598 }
1599 }
1600
1601 tally
1602}
1603
1604async fn test_bitfinex_ws() -> WsTally {
1605 println!("\n── Bitfinex WS ──────────────────────────────────────────────");
1606 let mut tally = WsTally {
1607 exchange: "Bitfinex".into(),
1608 channels: 0,
1609 subscribed: 0,
1610 events: 0,
1611 parse_errors: 0,
1612 zero_event_channels: Vec::new(),
1613 };
1614
1615 let duration = Duration::from_secs(5);
1616
1617 {
1619 tally.channels += 1;
1620 let ws_result = BitfinexWebSocket::new(None, false, AccountType::Spot).await;
1621 let ws = match ws_result {
1622 Ok(w) => w,
1623 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1624 };
1625 if ws.connect(AccountType::Spot).await.is_ok() {
1626 match ws.subscribe_l3_book(Symbol::new("BTC", "USD"), 25).await {
1627 Ok(_) => {
1628 tally.subscribed += 1;
1629 let mut stream = ws.event_stream();
1630 let mut n = 0usize;
1631 let mut errors = 0usize;
1632 let _ = timeout(duration, async {
1633 while let Some(item) = stream.next().await {
1634 match item { Ok(_) => n += 1, Err(_) => errors += 1 }
1635 }
1636 }).await;
1637 tally.events += n;
1638 tally.parse_errors += errors;
1639 let label = "book R0 tBTCUSD (L3)".to_string();
1640 println!(
1641 " CH {} -> events={}, errors={}{}",
1642 label, n, errors,
1643 if n == 0 { " [ZERO EVENTS]" } else { "" }
1644 );
1645 if n == 0 { tally.zero_event_channels.push(label); }
1646 }
1647 Err(e) => println!(" FAIL subscribe book R0 tBTCUSD -> {}", e),
1648 }
1649 let _ = ws.disconnect().await;
1650 } else {
1651 println!(" FAIL: Bitfinex WS connect");
1652 }
1653 }
1654
1655 {
1657 tally.channels += 1;
1658 let ws_result = BitfinexWebSocket::new(None, false, AccountType::Spot).await;
1659 let ws = match ws_result {
1660 Ok(w) => w,
1661 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1662 };
1663 if ws.connect(AccountType::Spot).await.is_ok() {
1664 match ws.subscribe_funding_book("fUSD").await {
1665 Ok(_) => {
1666 tally.subscribed += 1;
1667 let mut stream = ws.event_stream();
1668 let mut n = 0usize;
1669 let mut errors = 0usize;
1670 let _ = timeout(duration, async {
1671 while let Some(item) = stream.next().await {
1672 match item { Ok(_) => n += 1, Err(_) => errors += 1 }
1673 }
1674 }).await;
1675 tally.events += n;
1676 tally.parse_errors += errors;
1677 let label = "funding book fUSD".to_string();
1678 println!(
1679 " CH {} -> events={}, errors={}{}",
1680 label, n, errors,
1681 if n == 0 { " [ZERO EVENTS]" } else { "" }
1682 );
1683 if n == 0 { tally.zero_event_channels.push(label); }
1684 }
1685 Err(e) => println!(" FAIL subscribe funding book fUSD -> {}", e),
1686 }
1687 let _ = ws.disconnect().await;
1688 }
1689 }
1690
1691 {
1694 tally.channels += 1;
1695 let ws_result = BitfinexWebSocket::new(None, false, AccountType::FuturesCross).await;
1696 let ws = match ws_result {
1697 Ok(w) => w,
1698 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1699 };
1700 if ws.connect(AccountType::FuturesCross).await.is_ok() {
1701 let mut req = SubscriptionRequest::new(Symbol::new("BTC", "USDT"), StreamType::FundingRate);
1704 req.account_type = AccountType::FuturesCross;
1705 let (ok, n, err, label) = ws_listen(&ws, req, duration, "status deriv:tBTCF0:USTF0").await;
1706 if ok { tally.subscribed += 1; }
1707 tally.events += n;
1708 tally.parse_errors += err;
1709 if ok && n == 0 { tally.zero_event_channels.push(label); }
1710 let _ = ws.disconnect().await;
1711 }
1712 }
1713
1714 tally
1715}
1716
1717async fn test_gemini_ws() -> WsTally {
1718 println!("\n── Gemini WS ────────────────────────────────────────────────");
1719 let mut tally = WsTally {
1720 exchange: "Gemini".into(),
1721 channels: 0,
1722 subscribed: 0,
1723 events: 0,
1724 parse_errors: 0,
1725 zero_event_channels: Vec::new(),
1726 };
1727
1728 let duration = Duration::from_secs(5);
1729
1730 {
1732 tally.channels += 1;
1733 let ws_result = GeminiWebSocket::new_market_data(false).await;
1734 let ws = match ws_result {
1735 Ok(w) => w,
1736 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1737 };
1738 if ws.connect().await.is_ok() {
1740 match ws.subscribe_auction(Symbol::new("BTC", "USD")).await {
1741 Ok(_) => {
1742 tally.subscribed += 1;
1743 let mut stream = <GeminiWebSocket as WebSocketConnector>::event_stream(&ws);
1745 let mut n = 0usize;
1746 let mut errors = 0usize;
1747 let _ = timeout(duration, async {
1748 while let Some(item) = stream.next().await {
1749 match item { Ok(_) => n += 1, Err(_) => errors += 1 }
1750 }
1751 }).await;
1752 tally.events += n;
1753 tally.parse_errors += errors;
1754 let label = "auction btcusd".to_string();
1755 println!(
1756 " CH {} -> events={}, errors={}{}",
1757 label, n, errors,
1758 if n == 0 { " [ZERO EVENTS]" } else { "" }
1759 );
1760 if n == 0 { tally.zero_event_channels.push(label); }
1761 }
1762 Err(e) => println!(" FAIL subscribe auction btcusd -> {}", e),
1763 }
1764 let _ = ws.disconnect().await;
1765 } else {
1766 println!(" FAIL: Gemini WS connect");
1767 }
1768 }
1769
1770 tally
1771}
1772
1773async fn test_bitstamp_ws() -> WsTally {
1774 println!("\n── Bitstamp WS ──────────────────────────────────────────────");
1775 let mut tally = WsTally {
1776 exchange: "Bitstamp".into(),
1777 channels: 0,
1778 subscribed: 0,
1779 events: 0,
1780 parse_errors: 0,
1781 zero_event_channels: Vec::new(),
1782 };
1783
1784 let duration = Duration::from_secs(5);
1785
1786 {
1788 tally.channels += 1;
1789 let ws = BitstampWebSocket::new();
1790 if ws.connect(AccountType::Spot).await.is_ok() {
1791 let req = SubscriptionRequest::new(Symbol::new("BTC", "USD"), StreamType::OrderbookL3);
1792 let (ok, n, err, label) = ws_listen(&ws, req, duration, "detail_order_book_btcusd").await;
1793 if ok { tally.subscribed += 1; }
1794 tally.events += n;
1795 tally.parse_errors += err;
1796 if ok && n == 0 { tally.zero_event_channels.push(label); }
1797 let _ = ws.disconnect().await;
1798 } else {
1799 println!(" FAIL: Bitstamp WS connect");
1800 }
1801 }
1802
1803 tally
1804}
1805
1806async fn test_coinbase_ws() -> WsTally {
1807 println!("\n── Coinbase WS ──────────────────────────────────────────────");
1808 let mut tally = WsTally {
1809 exchange: "Coinbase".into(),
1810 channels: 0,
1811 subscribed: 0,
1812 events: 0,
1813 parse_errors: 0,
1814 zero_event_channels: Vec::new(),
1815 };
1816
1817 let duration = Duration::from_secs(5);
1818
1819 tally.channels += 1;
1824 println!(" NOTE: rfq_matches/BlockTrade not subscriptable via standard StreamType trait on Coinbase WS.");
1825 println!(" NOTE: Parser exists for rfq_matches channel but no subscribe() mapping — architectural gap.");
1826
1827 {
1829 let ws_result = CoinbaseWebSocket::new(None).await;
1830 let ws = match ws_result {
1831 Ok(w) => w,
1832 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1833 };
1834 if ws.connect(AccountType::Spot).await.is_ok() {
1835 let req = SubscriptionRequest::new(Symbol::new("BTC", "USD"), StreamType::Ticker);
1836 let (ok, n, err, label) = ws_listen(&ws, req, duration, "ticker BTC-USD (connectivity check)").await;
1837 if ok { tally.subscribed += 1; }
1838 tally.events += n;
1839 tally.parse_errors += err;
1840 if ok && n == 0 { tally.zero_event_channels.push(label); }
1841 let _ = ws.disconnect().await;
1842 } else {
1843 println!(" FAIL: Coinbase WS connect");
1844 }
1845 }
1846
1847 tally
1848}
1849
1850async fn test_kraken_ws() -> WsTally {
1851 println!("\n── Kraken WS ────────────────────────────────────────────────");
1852 let mut tally = WsTally {
1853 exchange: "Kraken".into(),
1854 channels: 0,
1855 subscribed: 0,
1856 events: 0,
1857 parse_errors: 0,
1858 zero_event_channels: Vec::new(),
1859 };
1860
1861 let duration = Duration::from_secs(5);
1862
1863 {
1865 tally.channels += 1;
1866 let ws_result = KrakenWebSocket::new(None, AccountType::Spot).await;
1867 let ws = match ws_result {
1868 Ok(w) => w,
1869 Err(e) => { println!(" FAIL WS init -> {}", e); return tally; }
1870 };
1871 if ws.connect(AccountType::Spot).await.is_ok() {
1872 let req = SubscriptionRequest::new(Symbol::new("BTC", "USD"), StreamType::MarketWarning);
1874 let (ok, n, err, label) = ws_listen(&ws, req, duration, "instrument (MarketWarning)").await;
1875 if ok { tally.subscribed += 1; }
1876 tally.events += n;
1877 tally.parse_errors += err;
1878 if ok && n == 0 { tally.zero_event_channels.push(label); }
1879 let _ = ws.disconnect().await;
1880 } else {
1881 println!(" FAIL: Kraken WS connect");
1882 }
1883 }
1884
1885 tally
1886}
1887
1888fn print_rest_summary(tallies: &[RestTally]) {
1893 println!("\n╔══════════════════════════════════════════════════════════════════╗");
1894 println!("║ Section A — REST Trading-Metadata Tally ║");
1895 println!("╠═══════════════╦═════════╦═════════╦═════════╗");
1896 println!("║ Exchange ║ Tested ║ Passed ║ Failed ║");
1897 println!("╠═══════════════╬═════════╬═════════╬═════════╣");
1898 for t in tallies {
1899 println!(
1900 "║ {:13} ║ {:7} ║ {:7} ║ {:7} ║",
1901 t.exchange, t.tested, t.passed, t.failed
1902 );
1903 }
1904 println!("╚═══════════════╩═════════╩═════════╩═════════╝");
1905 let total_tested: usize = tallies.iter().map(|t| t.tested).sum();
1906 let total_passed: usize = tallies.iter().map(|t| t.passed).sum();
1907 let total_failed: usize = tallies.iter().map(|t| t.failed).sum();
1908 println!(
1909 " Total: tested={} passed={} failed={} (skipped={})",
1910 total_tested,
1911 total_passed,
1912 total_failed,
1913 total_tested.saturating_sub(total_passed + total_failed)
1914 );
1915}
1916
1917fn print_ws_summary(tallies: &[WsTally]) {
1918 println!("\n╔══════════════════════════════════════════════════════════════════╗");
1919 println!("║ Section B — WebSocket Channel Tally ║");
1920 println!("╠═══════════════╦══════╦══════╦══════════╦════════╗");
1921 println!("║ Exchange ║ Ch ║ SubOK║ Events ║ Errors ║");
1922 println!("╠═══════════════╬══════╬══════╬══════════╬════════╣");
1923 for t in tallies {
1924 println!(
1925 "║ {:13} ║ {:4} ║ {:4} ║ {:8} ║ {:6} ║",
1926 t.exchange, t.channels, t.subscribed, t.events, t.parse_errors
1927 );
1928 }
1929 println!("╚═══════════════╩══════╩══════╩══════════╩════════╝");
1930
1931 for t in tallies {
1932 if !t.zero_event_channels.is_empty() {
1933 println!(
1934 " WARN [{}] zero-event channels (parse fail or quiet market): {:?}",
1935 t.exchange, t.zero_event_channels
1936 );
1937 }
1938 }
1939}
1940
1941#[tokio::main]
1946async fn main() {
1947 println!("╔══════════════════════════════════════════════════════════════════╗");
1948 println!("║ e2e_metadata — Live Trading-Metadata Smoke Test ║");
1949 println!("╚══════════════════════════════════════════════════════════════════╝");
1950 println!("Hitting live exchange APIs — no keys required for public endpoints.");
1951 println!("WS channels run for 8 seconds each (sequential).");
1952
1953 println!("\n══════════════════ Section A: REST ══════════════════");
1955
1956 let binance_rest = test_binance_rest().await;
1957 let bybit_rest = test_bybit_rest().await;
1958 let okx_rest = test_okx_rest().await;
1959 let hl_rest = test_hyperliquid_rest().await;
1960 let deribit_rest = test_deribit_rest().await;
1961 let bitget_rest = test_bitget_rest().await;
1962 let htx_rest = test_htx_rest().await;
1963 let kucoin_rest = test_kucoin_rest().await;
1964 let gateio_rest = test_gateio_rest().await;
1965 let dydx_rest = test_dydx_rest().await;
1966 let lighter_rest = test_lighter_rest().await;
1967 let bitfinex_rest = test_bitfinex_rest().await;
1968 let kraken_rest = test_kraken_rest().await;
1969 let gemini_rest = test_gemini_rest().await;
1970 let bitstamp_rest = test_bitstamp_rest().await;
1971 let upbit_rest = test_upbit_rest().await;
1972 let crypto_com_rest = test_crypto_com_rest().await;
1973 let bingx_rest = test_bingx_rest().await;
1974 let mexc_note = test_mexc_note();
1975
1976 let rest_tallies = vec![
1977 binance_rest,
1978 bybit_rest,
1979 okx_rest,
1980 hl_rest,
1981 deribit_rest,
1982 bitget_rest,
1983 htx_rest,
1984 kucoin_rest,
1985 gateio_rest,
1986 dydx_rest,
1987 lighter_rest,
1988 bitfinex_rest,
1989 kraken_rest,
1990 gemini_rest,
1991 bitstamp_rest,
1992 upbit_rest,
1993 crypto_com_rest,
1994 bingx_rest,
1995 mexc_note,
1996 ];
1997
1998 println!("\n══════════════════ Section B: WebSocket ══════════════════");
2000 println!("(each channel listens 8 s — sequential to avoid port exhaustion)");
2001
2002 let binance_ws = test_binance_ws().await;
2003 let bybit_ws = test_bybit_ws().await;
2004 let okx_ws = test_okx_ws().await;
2005 let hl_ws = test_hyperliquid_ws().await;
2006 let deribit_ws = test_deribit_ws().await;
2007 let htx_ws = test_htx_ws().await;
2008 let kucoin_ws = test_kucoin_ws().await;
2009 let gateio_ws = test_gateio_ws().await;
2010 let crypto_com_ws = test_crypto_com_ws().await;
2011 let bitfinex_ws = test_bitfinex_ws().await;
2012 let gemini_ws = test_gemini_ws().await;
2013 let bitstamp_ws = test_bitstamp_ws().await;
2014 let coinbase_ws = test_coinbase_ws().await;
2015 let kraken_ws = test_kraken_ws().await;
2016
2017 let ws_tallies = vec![
2018 binance_ws,
2019 bybit_ws,
2020 okx_ws,
2021 hl_ws,
2022 deribit_ws,
2023 htx_ws,
2024 kucoin_ws,
2025 gateio_ws,
2026 crypto_com_ws,
2027 bitfinex_ws,
2028 gemini_ws,
2029 bitstamp_ws,
2030 coinbase_ws,
2031 kraken_ws,
2032 ];
2033
2034 println!("\n══════════════════ Section C: Summary ══════════════════");
2036 print_rest_summary(&rest_tallies);
2037 print_ws_summary(&ws_tallies);
2038
2039 println!("\nDone.");
2040}