Files
zeekayandHanzo Dev b8085b3ad7 fix(lx-trading-core): clippy -D warnings, and wire the arbitrage bot callback
With formatting fixed, the Rust SDK job advanced to `cargo clippy --all-targets
--all-features -- -D warnings` and failed there with 9 lib errors. Clearing those
exposed 21 more in examples/ that the lib failure had masked.

Library (9): unnecessary_sort_by x4 -> sort_by_key / Reverse; derivable_impls x2
-> #[derive(Default)] + #[default]; a hidden elided lifetime -> Timer<'_>;
field_reassign_with_default; and `VenueCapabilities::orderBook` -> `order_book`.
The `"orderBook"` at adapters/mod.rs:30 is a VENUE-NAME STRING LITERAL, not the
method, and is deliberately left alone.

Examples (21): 15 applied by rustfix (unused imports/variables, unnecessary f64
casts, clone-on-Copy). The 6 remaining were dead code, and one of them was a real
defect:

  ArbitrageBot::handle_opportunity was never called. It holds the bot's entire
  execution and stats path, so `router` was also never read and the example could
  only ever print a final report of zeros. LxFirstArbitrage::on_opportunity is the
  intended wire; start() now registers it. The callback is a sync
  Box<dyn Fn + Send + Sync>, so it spawns.

The other five were vestigial fields carrying nothing: MakerState's two
Option<Order> (the loop builds OrderRequest and simulates fills directly, so they
were only ever None) and OrderResult::symbol (every result is for the one
request's symbol). Plus a manual checked division.

Verified in the order CI runs them:

    cargo fmt --check                                CLEAN
    cargo clippy --all-targets --all-features -D     CLEAN
    cargo test --all-features                        106 passed, 0 failed
    cargo build --release                            CLEAN

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-02 03:09:05 -07:00

351 lines
12 KiB
Rust

//! WebSocket Client Example
//!
//! This example demonstrates real-time market data streaming:
//! - Connecting to WebSocket feeds
//! - Subscribing to orderbook, trade, and ticker channels
//! - Processing streaming events
//! - Managing connection lifecycle
//!
//! # Running
//!
//! ```bash
//! cargo run --example websocket_client
//! ```
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use anyhow::Result;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use tokio::sync::mpsc;
use tokio::time::{interval, timeout};
use lx_trading::{
ws::{Fill, OrderUpdate, OrderbookUpdate, WsConfig, WsEvent},
PriceLevel, Side, Ticker, Trade,
};
/// Event statistics
struct EventStats {
orderbook_updates: AtomicU64,
trades: AtomicU64,
tickers: AtomicU64,
order_updates: AtomicU64,
fills: AtomicU64,
errors: AtomicU64,
}
impl EventStats {
fn new() -> Self {
Self {
orderbook_updates: AtomicU64::new(0),
trades: AtomicU64::new(0),
tickers: AtomicU64::new(0),
order_updates: AtomicU64::new(0),
fills: AtomicU64::new(0),
errors: AtomicU64::new(0),
}
}
fn print_summary(&self) {
println!("\n=== Event Statistics ===");
println!(
"Orderbook Updates: {}",
self.orderbook_updates.load(Ordering::Relaxed)
);
println!("Trades: {}", self.trades.load(Ordering::Relaxed));
println!("Tickers: {}", self.tickers.load(Ordering::Relaxed));
println!(
"Order Updates: {}",
self.order_updates.load(Ordering::Relaxed)
);
println!("Fills: {}", self.fills.load(Ordering::Relaxed));
println!("Errors: {}", self.errors.load(Ordering::Relaxed));
}
}
/// Process an orderbook update
fn handle_orderbook_update(update: &OrderbookUpdate, stats: &EventStats) {
stats.orderbook_updates.fetch_add(1, Ordering::Relaxed);
let update_type = if update.is_snapshot {
"SNAPSHOT"
} else {
"DELTA"
};
println!(
"[ORDERBOOK] {} {} | Bids: {} | Asks: {}",
update.symbol,
update_type,
update.bids.len(),
update.asks.len()
);
// Show best levels
if let (Some(best_bid), Some(best_ask)) = (update.bids.first(), update.asks.first()) {
println!(
" Best: {} @ {} | {} @ {}",
best_bid.quantity, best_bid.price, best_ask.quantity, best_ask.price
);
}
}
/// Process a trade
fn handle_trade(trade: &Trade, stats: &EventStats) {
stats.trades.fetch_add(1, Ordering::Relaxed);
let side_str = match trade.side {
Side::Buy => "BUY ",
Side::Sell => "SELL",
};
println!(
"[TRADE] {} {} {} @ {} | Fee: {} {}",
trade.symbol, side_str, trade.quantity, trade.price, trade.fee.amount, trade.fee.asset
);
}
/// Process a ticker update
fn handle_ticker(ticker: &Ticker, stats: &EventStats) {
stats.tickers.fetch_add(1, Ordering::Relaxed);
let spread = match (ticker.bid, ticker.ask) {
(Some(b), Some(a)) => Some(a - b),
_ => None,
};
println!(
"[TICKER] {} | Bid: {:?} | Ask: {:?} | Spread: {:?}",
ticker.symbol, ticker.bid, ticker.ask, spread
);
}
/// Process an order update
fn handle_order_update(update: &OrderUpdate, stats: &EventStats) {
stats.order_updates.fetch_add(1, Ordering::Relaxed);
println!(
"[ORDER] {} | Status: {:?} | Filled: {} | ID: {}",
update.symbol, update.status, update.filled_quantity, update.order_id
);
}
/// Process a fill notification
fn handle_fill(fill: &Fill, stats: &EventStats) {
stats.fills.fetch_add(1, Ordering::Relaxed);
let side_str = match fill.side {
Side::Buy => "BUY ",
Side::Sell => "SELL",
};
println!(
"[FILL] {} {} {} @ {} | Order: {}",
fill.symbol, side_str, fill.quantity, fill.price, fill.order_id
);
}
/// Simulate WebSocket events for demonstration
async fn simulate_events(tx: mpsc::Sender<WsEvent>, running: Arc<AtomicBool>) {
use rand::{rngs::StdRng, Rng, SeedableRng};
let mut interval = interval(Duration::from_millis(200));
let mut rng = StdRng::from_entropy();
let base_price = dec!(50000);
let mut price_offset = dec!(0);
while running.load(Ordering::Relaxed) {
interval.tick().await;
// Simulate price movement
let change: f64 = rng.gen_range(-0.0005..0.0005);
price_offset += Decimal::try_from(change * 50000.0).unwrap_or_default();
let current_price = base_price + price_offset;
// Generate random event type
let event_type: u8 = rng.gen_range(0..5);
let event = match event_type {
0 => {
// Orderbook update
let is_snapshot = rng.gen_bool(0.1);
WsEvent::OrderbookUpdate(OrderbookUpdate {
symbol: "BTC-USDC".to_string(),
venue: "lx_dex".to_string(),
is_snapshot,
bids: vec![
PriceLevel::new(
current_price - dec!(5),
Decimal::try_from(rng.gen_range(0.1..2.0)).unwrap_or_default(),
),
PriceLevel::new(
current_price - dec!(10),
Decimal::try_from(rng.gen_range(0.5..3.0)).unwrap_or_default(),
),
],
asks: vec![
PriceLevel::new(
current_price + dec!(5),
Decimal::try_from(rng.gen_range(0.1..2.0)).unwrap_or_default(),
),
PriceLevel::new(
current_price + dec!(10),
Decimal::try_from(rng.gen_range(0.5..3.0)).unwrap_or_default(),
),
],
timestamp: chrono::Utc::now().timestamp_millis(),
sequence: rng.gen(),
})
}
1 => {
// Trade
WsEvent::Trade(Trade {
trade_id: format!("trade-{}", rng.gen::<u64>()),
order_id: format!("order-{}", rng.gen::<u64>()),
symbol: "BTC-USDC".to_string(),
venue: "lx_dex".to_string(),
side: if rng.gen_bool(0.5) {
Side::Buy
} else {
Side::Sell
},
price: current_price,
quantity: Decimal::try_from(rng.gen_range(0.01..0.5)).unwrap_or_default(),
fee: lx_trading::Fee {
asset: "USDC".to_string(),
amount: Decimal::try_from(rng.gen_range(0.01..1.0)).unwrap_or_default(),
rate: Some(dec!(0.001)),
},
timestamp: chrono::Utc::now().timestamp_millis(),
is_maker: rng.gen_bool(0.5),
})
}
2 => {
// Ticker
WsEvent::Ticker(Ticker {
symbol: "BTC-USDC".to_string(),
venue: "lx_dex".to_string(),
bid: Some(current_price - dec!(5)),
ask: Some(current_price + dec!(5)),
last: Some(current_price),
volume_24h: Some(dec!(1000)),
high_24h: Some(base_price + dec!(500)),
low_24h: Some(base_price - dec!(500)),
change_24h: Some((price_offset / base_price) * dec!(100)),
timestamp: chrono::Utc::now().timestamp_millis(),
})
}
_ => continue,
};
if tx.send(event).await.is_err() {
break;
}
}
}
#[tokio::main]
async fn main() -> Result<()> {
println!("=== LX Trading SDK - WebSocket Client Example ===\n");
// Create WebSocket configuration
let ws_config = WsConfig::new("wss://ws.dex.lux.network/stream", "lx_dex");
println!("WebSocket Configuration:");
println!(" URL: {}", ws_config.url);
println!(" Venue: {}", ws_config.venue);
println!(" Auto Reconnect: {}", ws_config.auto_reconnect);
println!(" Ping Interval: {:?}", ws_config.ping_interval);
println!();
// Initialize statistics
let stats = Arc::new(EventStats::new());
let running = Arc::new(AtomicBool::new(true));
// Create event channel
let (tx, mut rx) = mpsc::channel::<WsEvent>(1024);
// Start event simulator (in production, this would be actual WebSocket connection)
let running_clone = running.clone();
let simulator = tokio::spawn(async move {
simulate_events(tx, running_clone).await;
});
println!("Subscriptions:");
println!(" - Orderbook (BTC-USDC)");
println!(" - Trades (BTC-USDC)");
println!(" - Ticker (BTC-USDC)");
println!();
println!("Processing events for 5 seconds...\n");
// Process events with timeout
let stats_clone = stats.clone();
let event_processor = tokio::spawn(async move {
let timeout_duration = Duration::from_secs(5);
let result = timeout(timeout_duration, async {
while let Some(event) = rx.recv().await {
match event {
WsEvent::OrderbookUpdate(update) => {
handle_orderbook_update(&update, &stats_clone);
}
WsEvent::Trade(trade) => {
handle_trade(&trade, &stats_clone);
}
WsEvent::Ticker(ticker) => {
handle_ticker(&ticker, &stats_clone);
}
WsEvent::OrderUpdate(update) => {
handle_order_update(&update, &stats_clone);
}
WsEvent::Fill(fill) => {
handle_fill(&fill, &stats_clone);
}
WsEvent::Connected { venue } => {
println!("[CONNECTED] {}", venue);
}
WsEvent::Disconnected { venue, reason } => {
println!("[DISCONNECTED] {} - {}", venue, reason);
}
WsEvent::Error { venue, message } => {
stats_clone.errors.fetch_add(1, Ordering::Relaxed);
println!("[ERROR] {} - {}", venue, message);
}
WsEvent::Subscribed { channel, symbol } => {
println!("[SUBSCRIBED] {}:{}", channel, symbol);
}
WsEvent::Unsubscribed { channel, symbol } => {
println!("[UNSUBSCRIBED] {}:{}", channel, symbol);
}
}
}
})
.await;
if result.is_err() {
println!("\nTimeout reached, stopping event processing...");
}
});
// Wait for processor
let _ = event_processor.await;
// Stop simulator
running.store(false, Ordering::Relaxed);
let _ = simulator.await;
// Print statistics
stats.print_summary();
println!("\n=== Example Complete ===");
println!("\nKey concepts demonstrated:");
println!("1. WebSocket configuration");
println!("2. Channel subscriptions (orderbook, trades, ticker)");
println!("3. Event processing patterns");
println!("4. Statistics collection");
Ok(())
}