#[path = "common/mod.rs"]
mod common;
use common::{run_jobs, JobOutcome};
use digdigdig3::connector_manager::ExchangeHub;
use digdigdig3::{AccountType, ExchangeId, StreamEvent};
use futures_util::StreamExt;
use std::time::Duration;
use tokio::time::timeout;
const TARGETS: &[(ExchangeId, AccountType, &str)] = &[
(ExchangeId::Binance, AccountType::Spot, "BTCUSDT"),
(ExchangeId::OKX, AccountType::Spot, "BTC-USDT"),
(ExchangeId::Bybit, AccountType::Spot, "BTCUSDT"),
];
#[tokio::test]
#[ignore] async fn orderbook_delta_no_gaps() {
let labels: Vec<String> = TARGETS
.iter()
.map(|(id, _, _)| format!("{id:?}"))
.collect();
let results = run_jobs(
labels,
Duration::from_secs(45),
|_job_id, label| async move {
let (id, acct, sym_str) = TARGETS
.iter()
.find(|(id, _, _)| format!("{id:?}") == label)
.ok_or_else(|| format!("unknown target: {label}"))?;
let hub = ExchangeHub::new();
hub.connect_full(*id, &[*acct], false)
.await
.map_err(|e| e.to_string())?;
let ws = hub
.ws(*id, *acct)
.ok_or_else(|| format!("{label}: no ws handle"))?;
ws.subscribe(digdigdig3::SubscriptionRequest {
symbol: digdigdig3::Symbol::with_raw("", "", sym_str.to_string()),
stream_type: digdigdig3::StreamType::OrderbookDelta,
account_type: *acct,
depth: None,
update_speed_ms: None,
})
.await
.map_err(|e| format!("{label}: subscribe failed: {e}"))?;
let mut stream = ws.event_stream();
let mut last_seq: Option<u64> = None;
let mut gaps = 0u64;
let mut total = 0u64;
let mut seq_available = false;
let deadline = tokio::time::Instant::now() + Duration::from_secs(25);
while tokio::time::Instant::now() < deadline {
let remaining =
deadline.saturating_duration_since(tokio::time::Instant::now());
match timeout(remaining.max(Duration::from_millis(100)), stream.next()).await {
Ok(Some(Ok(StreamEvent::OrderbookDelta(d)))) => {
total += 1;
if let Some(seq) = d.last_update_id {
seq_available = true;
if let Some(prev) = last_seq {
let first = d.first_update_id.unwrap_or(seq);
if first > prev + 1 {
gaps += 1;
}
}
last_seq = Some(seq);
}
}
Ok(Some(Ok(_))) => {} Ok(Some(Err(_))) | Ok(None) => {}
Err(_) => break,
}
}
Ok((total, gaps, seq_available))
},
)
.await;
println!("\n=== orderbook_delta_no_gaps ===");
for r in &results {
match &r.outcome {
JobOutcome::Ok((total, gaps, seq_available)) => {
if *seq_available {
let gap_pct = if *total > 0 { gaps * 100 / total } else { 0 };
println!(
"{:<15} job#{:>3} total={total} gaps={gaps} gap%={gap_pct}%",
r.label, r.job_id.0
);
assert!(
*total > 0,
"{}: zero orderbook deltas received in 25s",
r.label
);
assert!(
gap_pct < 5,
"{}: gap rate too high ({gaps}/{total} = {gap_pct}%)",
r.label
);
} else {
println!(
"{:<15} job#{:>3} total={total} seq=N/A (gap detection deferred)",
r.label, r.job_id.0
);
assert!(
*total > 0,
"{}: zero orderbook deltas received in 25s (seq detection deferred)",
r.label
);
}
}
JobOutcome::TimedOut => {
panic!("{}: timed out waiting for orderbook deltas", r.label)
}
JobOutcome::Failed(e) => {
panic!("{}: {e}", r.label)
}
}
}
}