diff --git a/benches/book_updates.rs b/benches/book_updates.rs index 2d67efb..f97d049 100644 --- a/benches/book_updates.rs +++ b/benches/book_updates.rs @@ -15,17 +15,14 @@ use std::time::Instant; fn bench_book_creation(c: &mut Criterion) { c.bench_function("book_creation", |b| { b.iter(|| { - let _book = OrderBook::new( - black_box("test_token".to_string()), - black_box(100), - ); + let _book = OrderBook::new(black_box("test_token".to_string()), black_box(100)); }); }); } fn bench_delta_application(c: &mut Criterion) { let mut book = OrderBook::new("test_token".to_string(), 100); - + // Pre-populate with some levels for i in 1..=10 { let price = Decimal::from(50 + i) / Decimal::from(100); @@ -57,7 +54,7 @@ fn bench_delta_application(c: &mut Criterion) { fn bench_best_price_lookup(c: &mut Criterion) { let mut book = OrderBook::new("test_token".to_string(), 100); - + // Pre-populate with levels for i in 1..=20 { let price = Decimal::from(50 + i) / Decimal::from(100); @@ -84,7 +81,7 @@ fn bench_best_price_lookup(c: &mut Criterion) { fn bench_book_snapshot(c: &mut Criterion) { let mut book = OrderBook::new("test_token".to_string(), 100); - + // Pre-populate with levels for i in 1..=50 { let price = Decimal::from(50 + i) / Decimal::from(100); @@ -108,7 +105,7 @@ fn bench_book_snapshot(c: &mut Criterion) { fn bench_market_impact_calculation(c: &mut Criterion) { let mut book = OrderBook::new("test_token".to_string(), 100); - + // Pre-populate with levels for i in 1..=30 { let price = Decimal::from(50 + i) / Decimal::from(100); @@ -135,7 +132,7 @@ fn bench_high_frequency_updates(c: &mut Criterion) { b.iter(|| { let mut book = OrderBook::new("test_token".to_string(), 100); let start_time = Instant::now(); - + // Simulate high-frequency updates for i in 1..=1000 { let price = Decimal::from(500 + (i % 100)) / Decimal::from(1000); @@ -149,14 +146,14 @@ fn bench_high_frequency_updates(c: &mut Criterion) { sequence: i, }; book.apply_delta(delta).unwrap(); - + // Check prices every 10 updates if i % 10 == 0 { let _bid = book.best_bid(); let _ask = book.best_ask(); } } - + let duration = start_time.elapsed(); black_box(duration); }); @@ -166,17 +163,17 @@ fn bench_high_frequency_updates(c: &mut Criterion) { fn bench_concurrent_access(c: &mut Criterion) { use std::sync::Arc; use tokio::sync::RwLock; - + c.bench_function("concurrent_access", |b| { b.iter(|| { let book = Arc::new(RwLock::new(OrderBook::new("test_token".to_string(), 100))); let book_clone = book.clone(); - + // Simulate concurrent reads and writes let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { let mut tasks = Vec::new(); - + // Spawn writer tasks for i in 1..=10 { let book = book.clone(); @@ -194,7 +191,7 @@ fn bench_concurrent_access(c: &mut Criterion) { book.apply_delta(delta).unwrap(); })); } - + // Spawn reader tasks for _ in 0..20 { let book = book_clone.clone(); @@ -204,7 +201,7 @@ fn bench_concurrent_access(c: &mut Criterion) { let _ask = book.best_ask(); })); } - + // Wait for all tasks for task in tasks { let _ = task.await; @@ -224,4 +221,4 @@ criterion_group!( bench_high_frequency_updates, bench_concurrent_access, ); -criterion_main!(benches); \ No newline at end of file +criterion_main!(benches); diff --git a/benches/comparison_benchmarks.rs b/benches/comparison_benchmarks.rs index 6c89986..41ff2e1 100644 --- a/benches/comparison_benchmarks.rs +++ b/benches/comparison_benchmarks.rs @@ -1,5 +1,5 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use polyfill_rs::{OrderArgs, Side, OrderBookImpl}; +use polyfill_rs::{OrderArgs, OrderBookImpl, Side}; use rust_decimal::Decimal; use std::str::FromStr; @@ -14,7 +14,7 @@ fn benchmark_create_order_eip712(c: &mut Criterion) { Decimal::from_str("100.0").unwrap(), Side::BUY, ); - + // Simulate the computational work of order creation black_box(order_args) }) @@ -24,7 +24,7 @@ fn benchmark_create_order_eip712(c: &mut Criterion) { // Benchmark: JSON parsing (simulate market data parsing) fn benchmark_json_parsing(c: &mut Criterion) { let sample_json = r#"{"data":[{"condition_id":"test","question":"Test Question","description":"Test Description","end_date_iso":"2024-01-01T00:00:00Z","game_start_time":"2024-01-01T00:00:00Z","image":"","icon":"","active":true,"closed":false,"archived":false,"accepting_orders":true,"minimum_order_size":"1.0","minimum_tick_size":"0.01","market_slug":"test","seconds_delay":0,"fpmm":"0x123","rewards":{"min_size":"1.0","max_spread":"0.1"},"tokens":[{"token_id":"123","outcome":"Yes","price":"0.5","winner":false}]}]}"#; - + c.bench_function("json_parsing_markets", |b| { b.iter(|| { // This benchmarks JSON parsing and deserialization @@ -39,12 +39,12 @@ fn benchmark_order_book_operations(c: &mut Criterion) { c.bench_function("order_book_updates", |b| { b.iter(|| { let mut book = OrderBookImpl::new("test_token".to_string(), 100); - + // Simulate rapid order book updates for i in 0..1000 { let price = Decimal::from_str(&format!("0.{:04}", 5000 + (i % 100))).unwrap(); let size = Decimal::from_str("100.0").unwrap(); - + let bid_delta = polyfill_rs::OrderDelta { token_id: "test_token".to_string(), timestamp: chrono::Utc::now(), @@ -53,10 +53,10 @@ fn benchmark_order_book_operations(c: &mut Criterion) { size, sequence: i as u64, }; - + let _ = book.apply_delta(bid_delta); } - + black_box(book) }) }); @@ -65,24 +65,28 @@ fn benchmark_order_book_operations(c: &mut Criterion) { // Benchmark: Fast order book operations fn benchmark_fast_operations(c: &mut Criterion) { let mut book = OrderBookImpl::new("test_token".to_string(), 100); - + // Pre-populate the book for i in 0..50 { let price = Decimal::from_str(&format!("0.{:04}", 5000 + i)).unwrap(); let size = Decimal::from_str("100.0").unwrap(); - + let delta = polyfill_rs::OrderDelta { token_id: "test_token".to_string(), timestamp: chrono::Utc::now(), - side: if i % 2 == 0 { polyfill_rs::Side::BUY } else { polyfill_rs::Side::SELL }, + side: if i % 2 == 0 { + polyfill_rs::Side::BUY + } else { + polyfill_rs::Side::SELL + }, price, size, sequence: i as u64, }; - + let _ = book.apply_delta(delta); } - + c.bench_function("fast_spread_mid_calculations", |b| { b.iter(|| { // These use fixed-point arithmetic internally diff --git a/benches/fill_processing.rs b/benches/fill_processing.rs index f12da73..782ae98 100644 --- a/benches/fill_processing.rs +++ b/benches/fill_processing.rs @@ -16,11 +16,7 @@ use std::time::Instant; fn bench_fill_engine_creation(c: &mut Criterion) { c.bench_function("fill_engine_creation", |b| { b.iter(|| { - let _engine = FillEngine::new( - black_box(dec!(1)), - black_box(dec!(5)), - black_box(10), - ); + let _engine = FillEngine::new(black_box(dec!(1)), black_box(dec!(5)), black_box(10)); }); }); } @@ -28,7 +24,7 @@ fn bench_fill_engine_creation(c: &mut Criterion) { fn bench_market_order_execution(c: &mut Criterion) { let mut engine = FillEngine::new(dec!(1), dec!(5), 10); let mut book = OrderBook::new("test_token".to_string(), 100); - + // Pre-populate book with levels for i in 1..=20 { let price = Decimal::from(50 + i) / Decimal::from(100); @@ -52,7 +48,7 @@ fn bench_market_order_execution(c: &mut Criterion) { slippage_tolerance: Some(dec!(1.0)), client_id: Some("bench_order".to_string()), }; - + let _result = engine.execute_market_order(&request, &book); }); }); @@ -60,7 +56,7 @@ fn bench_market_order_execution(c: &mut Criterion) { fn bench_fill_processor(c: &mut Criterion) { let mut processor = FillProcessor::new(1000); - + c.bench_function("fill_processor", |b| { b.iter(|| { let fill = FillEvent { @@ -75,7 +71,7 @@ fn bench_fill_processor(c: &mut Criterion) { taker_address: alloy_primitives::Address::ZERO, fee: black_box(dec!(0.1)), }; - + processor.process_fill(fill).unwrap(); }); }); @@ -83,7 +79,7 @@ fn bench_fill_processor(c: &mut Criterion) { fn bench_market_impact_calculation(c: &mut Criterion) { let mut book = OrderBook::new("test_token".to_string(), 100); - + // Pre-populate with realistic order book for i in 1..=30 { let price = Decimal::from(50 + i) / Decimal::from(100); @@ -113,7 +109,7 @@ fn bench_high_frequency_fills(c: &mut Criterion) { let mut engine = FillEngine::new(dec!(1), dec!(2), 5); let mut book = OrderBook::new("test_token".to_string(), 100); let start_time = Instant::now(); - + // Simulate high-frequency fill processing for i in 1..=100 { // Add some market depth @@ -128,7 +124,7 @@ fn bench_high_frequency_fills(c: &mut Criterion) { sequence: i, }; book.apply_delta(delta).unwrap(); - + // Execute market orders if i % 5 == 0 { let request = MarketOrderRequest { @@ -138,11 +134,11 @@ fn bench_high_frequency_fills(c: &mut Criterion) { slippage_tolerance: Some(dec!(1.0)), client_id: Some(format!("order_{}", i)), }; - + let _result = engine.execute_market_order(&request, &book); } } - + let duration = start_time.elapsed(); black_box(duration); }); @@ -151,7 +147,7 @@ fn bench_high_frequency_fills(c: &mut Criterion) { fn bench_fill_statistics(c: &mut Criterion) { let mut engine = FillEngine::new(dec!(1), dec!(5), 10); - + // Add some fills for i in 1..=100 { let request = MarketOrderRequest { @@ -161,7 +157,7 @@ fn bench_fill_statistics(c: &mut Criterion) { slippage_tolerance: Some(dec!(1.0)), client_id: Some(format!("order_{}", i)), }; - + let mut book = OrderBook::new("test_token".to_string(), 100); book.apply_delta(OrderDelta { token_id: "test_token".to_string(), @@ -170,8 +166,9 @@ fn bench_fill_statistics(c: &mut Criterion) { price: dec!(0.5), size: dec!(100), sequence: i, - }).unwrap(); - + }) + .unwrap(); + let _result = engine.execute_market_order(&request, &book); } @@ -191,4 +188,4 @@ criterion_group!( bench_high_frequency_fills, bench_fill_statistics, ); -criterion_main!(benches); \ No newline at end of file +criterion_main!(benches); diff --git a/benches/network_benchmarks.rs b/benches/network_benchmarks.rs index aa512d9..fd85016 100644 --- a/benches/network_benchmarks.rs +++ b/benches/network_benchmarks.rs @@ -7,12 +7,12 @@ use tokio::runtime::Runtime; // Benchmark: Real network request to get simplified markets fn benchmark_real_simplified_markets(c: &mut Criterion) { let rt = Runtime::new().unwrap(); - + c.bench_function("real_fetch_simplified_markets", |b| { b.iter(|| { rt.block_on(async { let client = ClobClient::new("https://clob.polymarket.com"); - + // This is the real network request + JSON parsing let result = client.get_sampling_simplified_markets(None).await; black_box(result) @@ -24,12 +24,12 @@ fn benchmark_real_simplified_markets(c: &mut Criterion) { // Benchmark: Real network request to get full markets fn benchmark_real_markets(c: &mut Criterion) { let rt = Runtime::new().unwrap(); - + c.bench_function("real_fetch_markets", |b| { b.iter(|| { rt.block_on(async { let client = ClobClient::new("https://clob.polymarket.com"); - + // This is the real network request + JSON parsing let result = client.get_sampling_markets(None).await; black_box(result) @@ -41,33 +41,33 @@ fn benchmark_real_markets(c: &mut Criterion) { // Benchmark: Real order creation (requires API credentials) fn benchmark_real_order_creation(c: &mut Criterion) { let rt = Runtime::new().unwrap(); - + // Skip if no credentials available let private_key = std::env::var("POLYMARKET_PRIVATE_KEY").ok(); if private_key.is_none() { println!("Skipping order creation benchmark - no POLYMARKET_PRIVATE_KEY env var"); return; } - + c.bench_function("real_create_order_eip712", |b| { b.iter(|| { rt.block_on(async { let client = ClobClient::new("https://clob.polymarket.com"); - + // Set up credentials if let Ok(_key) = std::env::var("POLYMARKET_PRIVATE_KEY") { // This would require implementing credential setup // let creds = ApiCredentials::from_private_key(&key)?; // client.set_credentials(creds); } - + let order_args = OrderArgs::new( "test_token_id", Decimal::from_str("0.75").unwrap(), Decimal::from_str("100.0").unwrap(), Side::BUY, ); - + // This is the real EIP-712 signing + network request let result = client.create_order(&order_args, None, None, None).await; black_box(result) diff --git a/examples/advanced_network_optimizations.rs b/examples/advanced_network_optimizations.rs index a71ce6f..634db25 100644 --- a/examples/advanced_network_optimizations.rs +++ b/examples/advanced_network_optimizations.rs @@ -6,32 +6,35 @@ use tokio::time::{sleep, Duration}; async fn main() -> Result<(), Box> { println!("šŸš€ Advanced Network Optimizations - polyfill-rs"); println!("==============================================="); - + // Use the best-performing configuration (Internet) let client = ClobClient::new_internet("https://clob.polymarket.com"); - + println!("šŸ“Š Test 1: Connection Pre-warming"); println!("================================="); - + // Test without pre-warming let start = Instant::now(); let _ = client.get_server_time().await; let cold_start = start.elapsed(); println!(" ā„ļø Cold start: {:?}", cold_start); - + // Test with pre-warming let client_warm = ClobClient::new_internet("https://clob.polymarket.com"); let _ = client_warm.prewarm_connections().await; - + let start = Instant::now(); let _ = client_warm.get_server_time().await; let warm_start = start.elapsed(); println!(" šŸ”„ Warm start: {:?}", warm_start); - println!(" šŸ“ˆ Improvement: {:.1}x faster", cold_start.as_millis() as f64 / warm_start.as_millis() as f64); - + println!( + " šŸ“ˆ Improvement: {:.1}x faster", + cold_start.as_millis() as f64 / warm_start.as_millis() as f64 + ); + println!("\nšŸ“Š Test 2: Request Batching Simulation"); println!("====================================="); - + // Sequential requests let start = Instant::now(); for _ in 0..5 { @@ -39,18 +42,21 @@ async fn main() -> Result<(), Box> { } let sequential_time = start.elapsed(); println!(" šŸ“ Sequential: 5 requests in {:?}", sequential_time); - + // Parallel requests (simulating batching) let start = Instant::now(); let futures = (0..5).map(|_| client.get_server_time()); let _results: Vec<_> = futures_util::future::join_all(futures).await; let parallel_time = start.elapsed(); println!(" ⚔ Parallel: 5 requests in {:?}", parallel_time); - println!(" šŸ“ˆ Improvement: {:.1}x faster", sequential_time.as_millis() as f64 / parallel_time.as_millis() as f64); - + println!( + " šŸ“ˆ Improvement: {:.1}x faster", + sequential_time.as_millis() as f64 / parallel_time.as_millis() as f64 + ); + println!("\nšŸ“Š Test 3: Circuit Breaker Pattern"); println!("================================="); - + struct SimpleCircuitBreaker { failure_count: u32, failure_threshold: u32, @@ -58,14 +64,14 @@ async fn main() -> Result<(), Box> { last_failure: Option, state: CircuitState, } - + #[derive(Debug, PartialEq)] enum CircuitState { Closed, // Normal operation Open, // Failing, reject requests HalfOpen, // Testing if service recovered } - + impl SimpleCircuitBreaker { fn new() -> Self { Self { @@ -76,7 +82,7 @@ async fn main() -> Result<(), Box> { state: CircuitState::Closed, } } - + fn can_execute(&mut self) -> bool { match self.state { CircuitState::Closed => true, @@ -91,30 +97,30 @@ async fn main() -> Result<(), Box> { } else { false } - } + }, CircuitState::HalfOpen => true, } } - + fn on_success(&mut self) { self.failure_count = 0; self.state = CircuitState::Closed; } - + fn on_failure(&mut self) { self.failure_count += 1; self.last_failure = Some(Instant::now()); - + if self.failure_count >= self.failure_threshold { self.state = CircuitState::Open; } } } - + let mut circuit_breaker = SimpleCircuitBreaker::new(); let mut successful_requests = 0; let mut rejected_requests = 0; - + // Simulate some requests with circuit breaker for i in 0..10 { if circuit_breaker.can_execute() { @@ -125,13 +131,13 @@ async fn main() -> Result<(), Box> { if i < 3 { println!(" āœ… Request {} succeeded", i + 1); } - } + }, Err(_) => { circuit_breaker.on_failure(); if i < 3 { println!(" āŒ Request {} failed", i + 1); } - } + }, } } else { rejected_requests += 1; @@ -139,21 +145,24 @@ async fn main() -> Result<(), Box> { println!(" 🚫 Request {} rejected by circuit breaker", i + 1); } } - + // Small delay between requests sleep(Duration::from_millis(100)).await; } - - println!(" šŸ“Š Results: {} successful, {} rejected", successful_requests, rejected_requests); - + + println!( + " šŸ“Š Results: {} successful, {} rejected", + successful_requests, rejected_requests + ); + println!("\nšŸ“Š Test 4: Adaptive Timeout Strategy"); println!("==================================="); - + struct AdaptiveTimeout { recent_times: Vec, max_samples: usize, } - + impl AdaptiveTimeout { fn new() -> Self { Self { @@ -161,27 +170,27 @@ async fn main() -> Result<(), Box> { max_samples: 10, } } - + fn add_sample(&mut self, duration: Duration) { self.recent_times.push(duration); if self.recent_times.len() > self.max_samples { self.recent_times.remove(0); } } - + fn get_adaptive_timeout(&self) -> Duration { if self.recent_times.is_empty() { return Duration::from_millis(5000); // Default } - + let avg = self.recent_times.iter().sum::() / self.recent_times.len() as u32; // Set timeout to 3x average response time avg * 3 } } - + let mut adaptive_timeout = AdaptiveTimeout::new(); - + // Collect some samples for i in 0..5 { let start = Instant::now(); @@ -193,10 +202,10 @@ async fn main() -> Result<(), Box> { } } } - + let recommended_timeout = adaptive_timeout.get_adaptive_timeout(); println!(" šŸŽÆ Recommended timeout: {:?}", recommended_timeout); - + println!("\nšŸŽÆ Advanced Optimization Summary"); println!("==============================="); println!("Implemented Optimizations:"); @@ -204,7 +213,7 @@ async fn main() -> Result<(), Box> { println!(" āœ… Request parallelization (batching simulation)"); println!(" āœ… Circuit breaker pattern (prevents cascade failures)"); println!(" āœ… Adaptive timeouts (dynamic based on network conditions)"); - + println!("\nFurther Optimizations Available:"); println!(" šŸ”§ Custom DNS resolver with caching"); println!(" šŸ”§ Connection affinity (sticky connections)"); @@ -212,12 +221,12 @@ async fn main() -> Result<(), Box> { println!(" šŸ”§ Geographical load balancing"); println!(" šŸ”§ WebSocket connections for real-time data"); println!(" šŸ”§ HTTP/3 (QUIC) when supported"); - + println!("\nšŸ“ˆ Expected Network Improvements:"); println!(" • 10-30% latency reduction from optimized HTTP client"); println!(" • 50-80% improvement in connection reuse scenarios"); println!(" • Better resilience during network instability"); println!(" • Adaptive performance based on network conditions"); - + Ok(()) } diff --git a/examples/authenticated_benchmark.rs b/examples/authenticated_benchmark.rs index 5d2ef47..9b19970 100644 --- a/examples/authenticated_benchmark.rs +++ b/examples/authenticated_benchmark.rs @@ -7,10 +7,10 @@ use std::time::Instant; async fn main() -> Result<(), Box> { // Load environment variables from .env file dotenv::dotenv().ok(); - + println!("šŸ” Authenticated Network Benchmark - Real Order Creation"); println!("======================================================="); - + // API credentials from .env file let _api_key = std::env::var("POLYMARKET_API_KEY") .map_err(|_| "POLYMARKET_API_KEY not found in .env file")?; @@ -18,17 +18,17 @@ async fn main() -> Result<(), Box> { .map_err(|_| "POLYMARKET_SECRET not found in .env file")?; let _passphrase = std::env::var("POLYMARKET_PASSPHRASE") .map_err(|_| "POLYMARKET_PASSPHRASE not found in .env file")?; - + println!("āœ… Loaded API credentials from .env file"); - + let client = ClobClient::new_internet("https://clob.polymarket.com"); - + println!("šŸ”‘ Setting up API credentials..."); - + // Test 1: API Key Creation/Derivation (part of the 266.5ms benchmark) println!("\nšŸ“Š Test 1: API Key Setup"); println!("========================"); - + let mut setup_times = Vec::new(); for i in 0..3 { let start = Instant::now(); @@ -36,33 +36,33 @@ async fn main() -> Result<(), Box> { Ok(_creds) => { let duration = start.elapsed(); setup_times.push(duration); - println!(" Run {}: āœ… API key setup in {:?}", i+1, duration); - + println!(" Run {}: āœ… API key setup in {:?}", i + 1, duration); + // Set the credentials for order creation // Note: We'd need to properly set up the client with these creds break; - } + }, Err(e) => { let duration = start.elapsed(); setup_times.push(duration); - println!(" Run {}: āŒ Error in {:?}: {}", i+1, duration, e); - } + println!(" Run {}: āŒ Error in {:?}: {}", i + 1, duration, e); + }, } } - + if !setup_times.is_empty() { let avg = setup_times.iter().sum::() / setup_times.len() as u32; println!(" šŸ“ˆ API setup average: {:?}", avg); } - + // Test 2: Order Creation with EIP-712 (the real 266.5ms test) println!("\nšŸ“Š Test 2: Order Creation + EIP-712 Signing"); println!("==========================================="); println!("Target: polymarket-rs-client 266.5ms ± 28.6ms"); - + // We need a real token ID for a valid order let token_id = "21742633143463906290569050155826241533067272736897614950488156847949938836455"; - + let mut order_times = Vec::new(); for i in 0..5 { let order_args = OrderArgs::new( @@ -71,56 +71,70 @@ async fn main() -> Result<(), Box> { Decimal::from_str("1.0").unwrap(), // Minimum size Side::BUY, ); - + let start = Instant::now(); match client.create_order(&order_args, None, None, None).await { Ok(order) => { let duration = start.elapsed(); order_times.push(duration); - println!(" Run {}: āœ… Order created in {:?}", i+1, duration); - + println!(" Run {}: āœ… Order created in {:?}", i + 1, duration); + // Immediately cancel to clean up // Note: We'd need the proper cancel method here - println!(" šŸ“ Order ID: {} (would cancel immediately)", - format!("{:?}", order).chars().take(50).collect::()); - } + println!( + " šŸ“ Order ID: {} (would cancel immediately)", + format!("{:?}", order).chars().take(50).collect::() + ); + }, Err(e) => { let duration = start.elapsed(); order_times.push(duration); - println!(" Run {}: āŒ Error in {:?}: {}", i+1, duration, e); - + println!(" Run {}: āŒ Error in {:?}: {}", i + 1, duration, e); + // Even errors give us timing info about how far we got if duration.as_millis() > 50 { println!(" šŸ’” Error occurred after network round-trip, timing still valid"); } - } + }, } } - + if !order_times.is_empty() { let avg = order_times.iter().sum::() / order_times.len() as u32; let min = order_times.iter().min().unwrap(); let max = order_times.iter().max().unwrap(); let std_dev = { let mean = avg.as_millis() as f64; - let variance = order_times.iter() + let variance = order_times + .iter() .map(|t| (t.as_millis() as f64 - mean).powi(2)) - .sum::() / order_times.len() as f64; + .sum::() + / order_times.len() as f64; variance.sqrt() }; - + println!("\n šŸ“Š Order Creation Results:"); - println!(" šŸ“ˆ polyfill-rs: {:.1}ms ± {:.1}ms", avg.as_millis(), std_dev); + println!( + " šŸ“ˆ polyfill-rs: {:.1}ms ± {:.1}ms", + avg.as_millis(), + std_dev + ); println!(" šŸ“Š Range: {:?} - {:?}", min, max); - println!(" šŸ†š vs original (266.5ms): {:.1}x {}", - 266.5 / avg.as_millis() as f64, - if avg.as_millis() < 267 { "faster" } else { "slower" }); + println!( + " šŸ†š vs original (266.5ms): {:.1}x {}", + 266.5 / avg.as_millis() as f64, + if avg.as_millis() < 267 { + "faster" + } else { + "slower" + } + ); } - + // Test 3: Compare with Market Data (for context) println!("\nšŸ“Š Test 3: Market Data (for comparison)"); println!("======================================"); - + let mut market_times = Vec::new(); for i in 0..3 { let start = Instant::now(); @@ -129,41 +143,50 @@ async fn main() -> Result<(), Box> { let duration = start.elapsed(); market_times.push(duration); if i < 2 { - println!(" Run {}: āœ… {} markets in {:?}", i+1, markets.data.len(), duration); + println!( + " Run {}: āœ… {} markets in {:?}", + i + 1, + markets.data.len(), + duration + ); } - } + }, Err(e) => { let duration = start.elapsed(); market_times.push(duration); if i < 2 { - println!(" Run {}: āŒ Error in {:?}: {}", i+1, duration, e); + println!(" Run {}: āŒ Error in {:?}: {}", i + 1, duration, e); } - } + }, } } - + if !market_times.is_empty() { let avg = market_times.iter().sum::() / market_times.len() as u32; println!(" šŸ“ˆ Market data average: {:?}", avg); - println!(" šŸ†š vs original (404.5ms): {:.1}x faster", 404.5 / avg.as_millis() as f64); + println!( + " šŸ†š vs original (404.5ms): {:.1}x faster", + 404.5 / avg.as_millis() as f64 + ); } - + println!("\nšŸŽÆ Authenticated Benchmark Summary"); println!("================================="); println!("Real Production Performance:"); - + if !order_times.is_empty() { let order_avg = order_times.iter().sum::() / order_times.len() as u32; println!(" • Order creation: {:?} (vs 266.5ms baseline)", order_avg); } - + if !market_times.is_empty() { - let market_avg = market_times.iter().sum::() / market_times.len() as u32; + let market_avg = + market_times.iter().sum::() / market_times.len() as u32; println!(" • Market data: {:?} (vs 404.5ms baseline)", market_avg); } - + println!("\nThis gives us the REAL production numbers to compare!"); println!("Network optimizations + EIP-712 signing performance combined."); - + Ok(()) } diff --git a/examples/benchmark_demo.rs b/examples/benchmark_demo.rs index 668533b..ddbe0b9 100644 --- a/examples/benchmark_demo.rs +++ b/examples/benchmark_demo.rs @@ -7,107 +7,116 @@ use std::time::Instant; async fn main() -> Result<(), Box> { println!("šŸš€ polyfill-rs Performance Benchmark Demo"); println!("=========================================="); - + let client = ClobClient::new("https://clob.polymarket.com"); - + // Benchmark 1: Order creation and EIP-712 signing (computational cost) println!("\nšŸ“Š Benchmark 1: Order Creation + EIP-712 Signing"); println!("------------------------------------------------"); - + let order_args = OrderArgs::new( "test_token_id", Decimal::from_str("0.75")?, Decimal::from_str("100.0")?, Side::BUY, ); - + let mut order_times = Vec::new(); for i in 0..10 { let start = Instant::now(); - + // This measures the computational cost of order creation and signing // Note: Will fail without proper credentials, but we're measuring the CPU work let _result = client.create_order(&order_args, None, None, None).await; - + let duration = start.elapsed(); order_times.push(duration); - + if i == 0 { println!(" First run: {:?}", duration); } } - + let avg_order_time = order_times.iter().sum::() / order_times.len() as u32; let min_order_time = order_times.iter().min().unwrap(); let max_order_time = order_times.iter().max().unwrap(); - + println!(" Average: {:?}", avg_order_time); println!(" Range: {:?} - {:?}", min_order_time, max_order_time); - println!(" šŸ“ˆ vs baseline (266.5ms): {:.1}x faster", - 266.5 / avg_order_time.as_millis() as f64); - + println!( + " šŸ“ˆ vs baseline (266.5ms): {:.1}x faster", + 266.5 / avg_order_time.as_millis() as f64 + ); + // Benchmark 2: Market data fetching and parsing println!("\nšŸ“Š Benchmark 2: Fetch + Parse Simplified Markets"); println!("-----------------------------------------------"); - + let mut fetch_times = Vec::new(); for i in 0..5 { let start = Instant::now(); - + match client.get_sampling_simplified_markets(None).await { Ok(markets) => { let duration = start.elapsed(); fetch_times.push(duration); - + if i == 0 { - println!(" āœ… Fetched {} markets in {:?}", markets.data.len(), duration); + println!( + " āœ… Fetched {} markets in {:?}", + markets.data.len(), + duration + ); } - } + }, Err(e) => { let duration = start.elapsed(); println!(" āš ļø Network error (expected): {} in {:?}", e, duration); // Still count the time for computational work done before network failure fetch_times.push(duration); - } + }, } } - + if !fetch_times.is_empty() { - let avg_fetch_time = fetch_times.iter().sum::() / fetch_times.len() as u32; + let avg_fetch_time = + fetch_times.iter().sum::() / fetch_times.len() as u32; let min_fetch_time = fetch_times.iter().min().unwrap(); let max_fetch_time = fetch_times.iter().max().unwrap(); - + println!(" Average: {:?}", avg_fetch_time); println!(" Range: {:?} - {:?}", min_fetch_time, max_fetch_time); - println!(" šŸ“ˆ vs baseline (404.5ms): {:.1}x faster", - 404.5 / avg_fetch_time.as_millis() as f64); + println!( + " šŸ“ˆ vs baseline (404.5ms): {:.1}x faster", + 404.5 / avg_fetch_time.as_millis() as f64 + ); } - + // Benchmark 3: Memory efficiency demonstration println!("\nšŸ“Š Benchmark 3: Memory Usage Analysis"); println!("------------------------------------"); - + println!(" šŸ”§ Memory optimizations in polyfill-rs:"); println!(" • Fixed-point arithmetic (u32/i64 vs Decimal)"); println!(" • Zero-allocation order book updates"); println!(" • Compact data structures"); println!(" • Cache-aligned memory layouts"); println!(" šŸ“ˆ Expected: ~10x less memory vs baseline (15.9MB)"); - + // Demonstrate order book efficiency println!("\nšŸ“Š Benchmark 4: Order Book Performance"); println!("------------------------------------"); - + use polyfill_rs::OrderBookImpl; - + let mut book = OrderBookImpl::new("demo_token".to_string(), 100); let start = Instant::now(); - + // Simulate rapid order book updates for i in 0..10000 { let price = Decimal::from_str(&format!("0.{:04}", 5000 + (i % 1000)))?; let size = Decimal::from_str("100.0")?; - + // These operations use fixed-point math internally let bid_delta = polyfill_rs::OrderDelta { token_id: "demo_token".to_string(), @@ -125,16 +134,18 @@ async fn main() -> Result<(), Box> { size, sequence: (i + 10000) as u64, }; - + let _ = book.apply_delta(bid_delta); let _ = book.apply_delta(ask_delta); } - + let book_duration = start.elapsed(); println!(" ⚔ 20,000 order book updates in {:?}", book_duration); - println!(" šŸ“Š Rate: {:.0} updates/second", - 20000.0 / book_duration.as_secs_f64()); - + println!( + " šŸ“Š Rate: {:.0} updates/second", + 20000.0 / book_duration.as_secs_f64() + ); + // Fast operations let start = Instant::now(); for _ in 0..100000 { @@ -142,8 +153,11 @@ async fn main() -> Result<(), Box> { let _ = book.mid_price_fast(); } let fast_ops_duration = start.elapsed(); - println!(" ⚔ 200,000 fast spread/mid calculations in {:?}", fast_ops_duration); - + println!( + " ⚔ 200,000 fast spread/mid calculations in {:?}", + fast_ops_duration + ); + println!("\nšŸŽÆ Summary"); println!("========="); println!("polyfill-rs delivers significant performance improvements through:"); @@ -154,6 +168,6 @@ async fn main() -> Result<(), Box> { println!(); println!("šŸ”¬ Run `cargo bench` for detailed criterion benchmarks"); println!("šŸ“Š Run `./scripts/benchmark_comparison.sh` for comprehensive analysis"); - + Ok(()) } diff --git a/examples/comprehensive_demo.rs b/examples/comprehensive_demo.rs index 47d050c..9ff84d4 100644 --- a/examples/comprehensive_demo.rs +++ b/examples/comprehensive_demo.rs @@ -10,36 +10,40 @@ //! - Rate limiting and performance optimizations use polyfill_rs::{ - // Core client types - ClobClient, PolyfillClient, OrderArgs, Side, OrderType, - // Order book management book::{OrderBook, OrderBookManager}, - - // Streaming capabilities - stream::{WebSocketStream, StreamManager}, - - // Fill execution - fill::{FillEngine, FillProcessor}, - - // Types and structures - types::*, - + // Error handling errors::{PolyfillError, Result}, - + + // Fill execution + fill::{FillEngine, FillProcessor}, + + // Streaming capabilities + stream::{StreamManager, WebSocketStream}, + + // Types and structures + types::*, + // Utility functions - utils::{crypto, math, retry, time, url, rate_limit, address}, - + utils::{address, crypto, math, rate_limit, retry, time, url}, + // Configuration ClientConfig, + // Core client types + ClobClient, + OrderArgs, + OrderType, + + PolyfillClient, + Side, }; use rust_decimal::Decimal; use rust_decimal_macros::dec; use std::time::Duration; use tokio::time::sleep; -use tracing::{error, info, debug}; +use tracing::{debug, error, info}; /// Comprehensive demo showcasing all polyfill-rs functionality #[allow(dead_code)] @@ -91,40 +95,40 @@ impl PolyfillDemo { pub fn new() -> Result { // Create basic client let client = ClobClient::new("https://clob.polymarket.com"); - + // Create advanced client with configuration let _config = ClientConfig { base_url: "https://clob.polymarket.com".to_string(), - chain_id: 137, // Polygon - private_key: None, // Would be set in production - api_credentials: None, // Would be set in production + chain_id: 137, // Polygon + private_key: None, // Would be set in production + api_credentials: None, // Would be set in production max_slippage: Some(dec!(0.01)), // 1% max slippage - fee_rate: Some(dec!(0.02)), // 2% fee rate + fee_rate: Some(dec!(0.02)), // 2% fee rate timeout: Some(Duration::from_secs(30)), max_connections: Some(100), }; - + let advanced_client = PolyfillClient::new("https://clob.polymarket.com"); - + // Create order book manager let book_manager = OrderBookManager::new(100); - + // Create fill engine let fill_engine = FillEngine::new( - dec!(1.0), // Min fill size - dec!(2.0), // Max slippage 2% - 5, // 5 bps fee rate + dec!(1.0), // Min fill size + dec!(2.0), // Max slippage 2% + 5, // 5 bps fee rate ); - + // Create fill processor let fill_processor = FillProcessor::new(1000); - + // Create stream manager let stream_manager = StreamManager::new(); - + // Create rate limiter (100 requests per second) let rate_limiter = rate_limit::TokenBucket::new(100, 100); - + Ok(Self { client, advanced_client, @@ -140,24 +144,24 @@ impl PolyfillDemo { /// Demo 1: Basic API Operations pub async fn demo_basic_api_operations(&mut self) -> Result<()> { info!("=== Demo 1: Basic API Operations ==="); - + // Test connectivity let is_ok = self.client.get_ok().await; info!("API connectivity: {}", is_ok); self.stats.api_calls += 1; - + // Get server time match self.client.get_server_time().await { Ok(timestamp) => { info!("Server time: {}", timestamp); self.stats.api_calls += 1; - } + }, Err(e) => { error!("Failed to get server time: {}", e); self.stats.errors += 1; - } + }, } - + // Get sampling markets match self.client.get_sampling_markets(None).await { Ok(markets) => { @@ -166,32 +170,36 @@ impl PolyfillDemo { info!(" Market: {} - {}", market.question, market.market_slug); } self.stats.api_calls += 1; - } + }, Err(e) => { error!("Failed to get markets: {}", e); self.stats.errors += 1; - } + }, } - + Ok(()) } /// Demo 2: Order Book Operations pub async fn demo_order_book_operations(&mut self) -> Result<()> { info!("=== Demo 2: Order Book Operations ==="); - + // Example token ID (you would use a real one in production) let token_id = "12345"; - + // Get order book from API match self.client.get_order_book(token_id).await { Ok(order_book) => { - info!("Order book for token {}: {} bids, {} asks", - token_id, order_book.bids.len(), order_book.asks.len()); - + info!( + "Order book for token {}: {} bids, {} asks", + token_id, + order_book.bids.len(), + order_book.asks.len() + ); + // Create local order book let mut local_book = OrderBook::new(token_id.to_string(), 50); - + // Apply order book data to local book for (i, bid) in order_book.bids.iter().enumerate() { local_book.apply_delta(OrderDelta { @@ -203,7 +211,7 @@ impl PolyfillDemo { sequence: i as u64, })?; } - + for (i, ask) in order_book.asks.iter().enumerate() { local_book.apply_delta(OrderDelta { token_id: token_id.to_string(), @@ -214,19 +222,29 @@ impl PolyfillDemo { sequence: (order_book.bids.len() + i) as u64, })?; } - + // Get analytics let analytics = local_book.analytics(); info!("Book analytics:"); - info!(" Bid levels: {}, Ask levels: {}", analytics.bid_count, analytics.ask_count); - info!(" Total bid size: {}, Total ask size: {}", analytics.total_bid_size, analytics.total_ask_size); + info!( + " Bid levels: {}, Ask levels: {}", + analytics.bid_count, analytics.ask_count + ); + info!( + " Total bid size: {}, Total ask size: {}", + analytics.total_bid_size, analytics.total_ask_size + ); if let Some(spread) = analytics.spread { - info!(" Spread: {} ({:.2}%)", spread, analytics.spread_pct.unwrap_or(dec!(0))); + info!( + " Spread: {} ({:.2}%)", + spread, + analytics.spread_pct.unwrap_or(dec!(0)) + ); } if let Some(mid) = analytics.mid_price { info!(" Mid price: {}", mid); } - + // Calculate market impact if let Some(impact) = local_book.calculate_market_impact(Side::BUY, dec!(100.0)) { info!("Market impact for 100 size buy:"); @@ -234,103 +252,98 @@ impl PolyfillDemo { info!(" Impact: {:.2}%", impact.impact_pct); info!(" Total cost: {}", impact.total_cost); } - + self.stats.api_calls += 1; - } + }, Err(e) => { error!("Failed to get order book: {}", e); self.stats.errors += 1; - } + }, } - + Ok(()) } /// Demo 3: Market Data Operations pub async fn demo_market_data_operations(&mut self) -> Result<()> { info!("=== Demo 3: Market Data Operations ==="); - + let token_id = "12345"; - + // Get midpoint match self.client.get_midpoint(token_id).await { Ok(midpoint) => { info!("Midpoint for {}: {}", token_id, midpoint.mid); self.stats.api_calls += 1; - } + }, Err(e) => { error!("Failed to get midpoint: {}", e); self.stats.errors += 1; - } + }, } - + // Get spread match self.client.get_spread(token_id).await { Ok(spread) => { info!("Spread for {}: {}", token_id, spread.spread); self.stats.api_calls += 1; - } + }, Err(e) => { error!("Failed to get spread: {}", e); self.stats.errors += 1; - } + }, } - + // Get price for both sides for side in [Side::BUY, Side::SELL] { match self.client.get_price(token_id, side).await { Ok(price) => { info!("{} price for {}: {}", side.as_str(), token_id, price.price); self.stats.api_calls += 1; - } + }, Err(e) => { error!("Failed to get {} price: {}", side.as_str(), e); self.stats.errors += 1; - } + }, } } - + // Get tick size match self.client.get_tick_size(token_id).await { Ok(tick_size) => { info!("Tick size for {}: {}", token_id, tick_size); self.stats.api_calls += 1; - } + }, Err(e) => { error!("Failed to get tick size: {}", e); self.stats.errors += 1; - } + }, } - + // Get neg risk match self.client.get_neg_risk(token_id).await { Ok(neg_risk) => { info!("Neg risk for {}: {}", token_id, neg_risk); self.stats.api_calls += 1; - } + }, Err(e) => { error!("Failed to get neg risk: {}", e); self.stats.errors += 1; - } + }, } - + Ok(()) } /// Demo 4: Order Creation and Management pub async fn demo_order_operations(&mut self) -> Result<()> { info!("=== Demo 4: Order Creation and Management ==="); - + // Create order arguments - let order_args = OrderArgs::new( - "12345", - dec!(0.75), - dec!(100.0), - Side::BUY, - ); - + let order_args = OrderArgs::new("12345", dec!(0.75), dec!(100.0), Side::BUY); + info!("Created order args: {:?}", order_args); - + // Create market order request let market_order = MarketOrderRequest { token_id: "12345".to_string(), @@ -339,9 +352,9 @@ impl PolyfillDemo { slippage_tolerance: Some(dec!(1.0)), // 1% slippage client_id: Some("demo_market_order".to_string()), }; - + info!("Created market order request: {:?}", market_order); - + // Create limit order request let limit_order = OrderRequest { token_id: "12345".to_string(), @@ -352,21 +365,21 @@ impl PolyfillDemo { expiration: None, client_id: Some("demo_limit_order".to_string()), }; - + info!("Created limit order request: {:?}", limit_order); - + self.stats.orders_processed += 2; - + Ok(()) } /// Demo 5: Fill Execution pub async fn demo_fill_execution(&mut self) -> Result<()> { info!("=== Demo 5: Fill Execution ==="); - + // Create a mock order book for testing let mut book = OrderBook::new("12345".to_string(), 50); - + // Add some liquidity for i in 1..=5 { book.apply_delta(OrderDelta { @@ -378,7 +391,7 @@ impl PolyfillDemo { sequence: i, })?; } - + for i in 1..=5 { book.apply_delta(OrderDelta { token_id: "12345".to_string(), @@ -389,9 +402,9 @@ impl PolyfillDemo { sequence: i + 10, })?; } - + info!("Created order book with liquidity"); - + // Execute market order let market_order = MarketOrderRequest { token_id: "12345".to_string(), @@ -400,9 +413,11 @@ impl PolyfillDemo { slippage_tolerance: Some(dec!(2.0)), client_id: Some("demo_market_buy".to_string()), }; - - let fill_result = self.fill_engine.execute_market_order(&market_order, &book)?; - + + let fill_result = self + .fill_engine + .execute_market_order(&market_order, &book)?; + info!("Market order execution result:"); info!(" Status: {:?}", fill_result.status); info!(" Total size: {}", fill_result.total_size); @@ -410,14 +425,14 @@ impl PolyfillDemo { info!(" Total cost: {}", fill_result.total_cost); info!(" Fees: {}", fill_result.fees); info!(" Number of fills: {}", fill_result.fills.len()); - + // Process fills for fill in &fill_result.fills { self.fill_processor.process_fill(fill.clone())?; self.stats.fills_processed += 1; self.stats.total_volume += fill.size; } - + // Execute limit order let limit_order = OrderRequest { token_id: "12345".to_string(), @@ -428,45 +443,48 @@ impl PolyfillDemo { expiration: None, client_id: Some("demo_limit_sell".to_string()), }; - + let limit_result = self.fill_engine.execute_limit_order(&limit_order, &book)?; - + info!("Limit order execution result:"); info!(" Status: {:?}", limit_result.status); info!(" Total size: {}", limit_result.total_size); info!(" Average price: {}", limit_result.average_price); - + self.stats.orders_processed += 2; - + Ok(()) } /// Demo 6: Utility Functions pub async fn demo_utility_functions(&mut self) -> Result<()> { info!("=== Demo 6: Utility Functions ==="); - + // Time utilities info!("Time utilities:"); info!(" Current timestamp (secs): {}", time::now_secs()); info!(" Current timestamp (millis): {}", time::now_millis()); info!(" Current timestamp (micros): {}", time::now_micros()); - + // Math utilities info!("Math utilities:"); let price = dec!(0.7534); let tick_size = dec!(0.01); let rounded_price = math::round_to_tick(price, tick_size); - info!(" Price: {}, Tick size: {}, Rounded: {}", price, tick_size, rounded_price); - + info!( + " Price: {}, Tick size: {}, Rounded: {}", + price, tick_size, rounded_price + ); + let notional = math::notional(price, dec!(100.0)); info!(" Notional value: {}", notional); - + let spread_pct = math::spread_pct(dec!(0.75), dec!(0.76)); info!(" Spread percentage: {:?}", spread_pct); - + let mid_price = math::mid_price(dec!(0.75), dec!(0.76)); info!(" Mid price: {:?}", mid_price); - + // Address utilities info!("Address utilities:"); let address = "0x1234567890123456789012345678901234567890"; @@ -474,32 +492,36 @@ impl PolyfillDemo { Ok(addr) => info!(" Parsed address: {:?}", addr), Err(e) => error!(" Failed to parse address: {}", e), } - + let token_id = "12345"; match address::validate_token_id(token_id) { Ok(_) => info!(" Valid token ID: {}", token_id), Err(e) => error!(" Invalid token ID: {}", e), } - + // URL utilities info!("URL utilities:"); let endpoint = url::build_endpoint("https://api.example.com", "/v1/orders")?; info!(" Built endpoint: {}", endpoint); - + // Rate limiting info!("Rate limiting:"); for i in 0..5 { let allowed = self.rate_limiter.try_consume(); - info!(" Request {}: {}", i + 1, if allowed { "ALLOWED" } else { "RATE LIMITED" }); + info!( + " Request {}: {}", + i + 1, + if allowed { "ALLOWED" } else { "RATE LIMITED" } + ); } - + Ok(()) } /// Demo 7: Error Handling and Retry Logic pub async fn demo_error_handling(&mut self) -> Result<()> { info!("=== Demo 7: Error Handling and Retry Logic ==="); - + // Demonstrate retry logic let retry_config = retry::RetryConfig { max_attempts: 3, @@ -508,53 +530,59 @@ impl PolyfillDemo { backoff_factor: 2.0, jitter: true, }; - + let operation = || async { // Simulate a potentially failing operation if rand::random::() { Ok("Success!") } else { - Err(PolyfillError::network("Simulated network error", std::io::Error::other("Simulated error"))) + Err(PolyfillError::network( + "Simulated network error", + std::io::Error::other("Simulated error"), + )) } }; - + match retry::with_retry(&retry_config, operation).await { Ok(result) => { info!("Retry operation succeeded: {}", result); - } + }, Err(e) => { error!("Retry operation failed after all attempts: {}", e); self.stats.errors += 1; - } + }, } - + // Demonstrate error types info!("Error types demonstration:"); - + let api_error = PolyfillError::api(400, "Bad Request"); info!(" API Error: {:?}", api_error); - - let network_error = PolyfillError::network("Connection timeout", std::io::Error::new(std::io::ErrorKind::TimedOut, "Connection timeout")); + + let network_error = PolyfillError::network( + "Connection timeout", + std::io::Error::new(std::io::ErrorKind::TimedOut, "Connection timeout"), + ); info!(" Network Error: {:?}", network_error); - + let parse_error = PolyfillError::parse("Invalid JSON", None); info!(" Parse Error: {:?}", parse_error); - + let config_error = PolyfillError::config("Invalid configuration"); info!(" Config Error: {:?}", config_error); - + Ok(()) } /// Demo 8: Streaming Capabilities (Mock) pub async fn demo_streaming_capabilities(&mut self) -> Result<()> { info!("=== Demo 8: Streaming Capabilities ==="); - + // Create a mock WebSocket stream let _stream = WebSocketStream::new("wss://stream.polymarket.com"); - + info!("Created WebSocket stream"); - + // Simulate subscription let subscription = WssSubscription { auth: WssAuth { @@ -567,12 +595,14 @@ impl PolyfillDemo { asset_ids: Some(vec!["12345".to_string(), "67890".to_string()]), channel_type: "USER".to_string(), }; - + info!("Created subscription: {:?}", subscription); - + // Simulate receiving stream messages let messages = vec![ - StreamMessage::Heartbeat { timestamp: chrono::Utc::now() }, + StreamMessage::Heartbeat { + timestamp: chrono::Utc::now(), + }, StreamMessage::BookUpdate { data: OrderDelta { token_id: "12345".to_string(), @@ -581,7 +611,7 @@ impl PolyfillDemo { price: dec!(0.75), size: dec!(100.0), sequence: 1, - } + }, }, StreamMessage::Trade { data: FillEvent { @@ -595,14 +625,14 @@ impl PolyfillDemo { maker_address: alloy_primitives::Address::ZERO, taker_address: alloy_primitives::Address::ZERO, fee: dec!(0.375), - } + }, }, ]; - + for message in messages { info!("Received stream message: {:?}", message); self.stats.stream_messages += 1; - + // Process message based on type match &message { StreamMessage::BookUpdate { data } => { @@ -611,31 +641,35 @@ impl PolyfillDemo { error!(" Failed to apply book update: {}", e); self.stats.errors += 1; } - } + }, StreamMessage::Trade { data } => { - info!(" Processing trade: {} {} @ {}", - data.side.as_str(), data.size, data.price); + info!( + " Processing trade: {} {} @ {}", + data.side.as_str(), + data.size, + data.price + ); if let Err(e) = self.fill_processor.process_fill(data.clone()) { error!(" Failed to process fill: {}", e); self.stats.errors += 1; } - } + }, StreamMessage::Heartbeat { timestamp } => { debug!(" Received heartbeat at: {}", timestamp); - } + }, _ => { info!(" Unhandled message type"); - } + }, } } - + Ok(()) } /// Demo 9: Performance and Analytics pub async fn demo_performance_analytics(&mut self) -> Result<()> { info!("=== Demo 9: Performance and Analytics ==="); - + // Get fill engine statistics let fill_stats = self.fill_engine.get_stats(); info!("Fill engine statistics:"); @@ -643,7 +677,7 @@ impl PolyfillDemo { info!(" Total fills: {}", fill_stats.total_fills); info!(" Total volume: {}", fill_stats.total_volume); info!(" Total fees: {}", fill_stats.total_fees); - + // Get fill processor statistics let processor_stats = self.fill_processor.get_stats(); info!("Fill processor statistics:"); @@ -652,7 +686,7 @@ impl PolyfillDemo { info!(" Pending volume: {}", processor_stats.pending_volume); info!(" Processed fills: {}", processor_stats.processed_fills); info!(" Processed volume: {}", processor_stats.processed_volume); - + // Get demo statistics info!("Demo statistics:"); info!(" API calls: {}", self.stats.api_calls); @@ -661,50 +695,51 @@ impl PolyfillDemo { info!(" Stream messages: {}", self.stats.stream_messages); info!(" Errors: {}", self.stats.errors); info!(" Total volume: {}", self.stats.total_volume); - + // Calculate error rate - let total_operations = self.stats.api_calls + self.stats.orders_processed + self.stats.stream_messages; + let total_operations = + self.stats.api_calls + self.stats.orders_processed + self.stats.stream_messages; let error_rate = if total_operations > 0 { (self.stats.errors as f64 / total_operations as f64) * 100.0 } else { 0.0 }; info!(" Error rate: {:.2}%", error_rate); - + Ok(()) } /// Run all demos pub async fn run_all_demos(&mut self) -> Result<()> { info!("Starting comprehensive polyfill-rs demo..."); - + // Run all demo sections self.demo_basic_api_operations().await?; sleep(Duration::from_millis(500)).await; - + self.demo_order_book_operations().await?; sleep(Duration::from_millis(500)).await; - + self.demo_market_data_operations().await?; sleep(Duration::from_millis(500)).await; - + self.demo_order_operations().await?; sleep(Duration::from_millis(500)).await; - + self.demo_fill_execution().await?; sleep(Duration::from_millis(500)).await; - + self.demo_utility_functions().await?; sleep(Duration::from_millis(500)).await; - + self.demo_error_handling().await?; sleep(Duration::from_millis(500)).await; - + self.demo_streaming_capabilities().await?; sleep(Duration::from_millis(500)).await; - + self.demo_performance_analytics().await?; - + info!("Comprehensive demo completed successfully!"); Ok(()) } @@ -714,18 +749,18 @@ impl PolyfillDemo { async fn main() -> Result<()> { // Initialize logging tracing_subscriber::fmt::init(); - + info!("Polyfill-rs Comprehensive Demo"); info!("=============================="); - + // Create and run demo let mut demo = PolyfillDemo::new()?; - + if let Err(e) = demo.run_all_demos().await { error!("Demo failed: {}", e); std::process::exit(1); } - + info!("Demo completed successfully!"); Ok(()) -} \ No newline at end of file +} diff --git a/examples/memory_benchmark.rs b/examples/memory_benchmark.rs index d159e92..ef8550a 100644 --- a/examples/memory_benchmark.rs +++ b/examples/memory_benchmark.rs @@ -54,100 +54,125 @@ async fn main() -> Result<(), Box> { println!("Comparing with polymarket-rs-client baseline:"); println!(" 88,053 allocs, 81,823 frees, 15,945,966 bytes allocated"); println!(); - + // Load environment variables dotenv::dotenv().ok(); - + let client = ClobClient::new_internet("https://clob.polymarket.com"); - + // Test 1: Market Data Fetching Memory Usage println!("šŸ“Š Test 1: Market Data Fetching Memory"); println!("====================================="); - + // Reset and measure market data fetching reset_counters(); let start_stats = get_memory_stats(); - + let start_time = Instant::now(); let result = client.get_sampling_simplified_markets(None).await; let duration = start_time.elapsed(); - + let end_stats = get_memory_stats(); - + match result { Ok(markets) => { - println!("āœ… Fetched {} markets in {:?}", markets.data.len(), duration); - + println!( + "āœ… Fetched {} markets in {:?}", + markets.data.len(), + duration + ); + let (bytes_allocated, allocs, deallocs) = ( end_stats.0 - start_stats.0, end_stats.1 - start_stats.1, end_stats.2 - start_stats.2, ); - + println!("šŸ“ˆ polyfill-rs memory usage:"); - println!(" {} allocs, {} frees, {} bytes allocated", allocs, deallocs, bytes_allocated); - println!("šŸ“Š vs baseline (15,945,966 bytes): {:.1}x less memory", - 15_945_966.0 / bytes_allocated as f64); - println!("šŸ“Š vs baseline ({} allocs): {:.1}x fewer allocations", - 88_053, 88_053.0 / allocs as f64); - } + println!( + " {} allocs, {} frees, {} bytes allocated", + allocs, deallocs, bytes_allocated + ); + println!( + "šŸ“Š vs baseline (15,945,966 bytes): {:.1}x less memory", + 15_945_966.0 / bytes_allocated as f64 + ); + println!( + "šŸ“Š vs baseline ({} allocs): {:.1}x fewer allocations", + 88_053, + 88_053.0 / allocs as f64 + ); + }, Err(e) => { println!("āŒ Error: {}", e); println!("āš ļø Still measuring memory usage of error handling..."); - + let (bytes_allocated, allocs, deallocs) = ( end_stats.0 - start_stats.0, end_stats.1 - start_stats.1, end_stats.2 - start_stats.2, ); - + println!("šŸ“ˆ Memory usage (even with error):"); - println!(" {} allocs, {} frees, {} bytes allocated", allocs, deallocs, bytes_allocated); - } + println!( + " {} allocs, {} frees, {} bytes allocated", + allocs, deallocs, bytes_allocated + ); + }, } - + // Test 2: Order Book Memory Efficiency println!("\nšŸ“Š Test 2: Order Book Memory Efficiency"); println!("======================================"); - + reset_counters(); let start_stats = get_memory_stats(); - + // Create order book and populate it let mut book = OrderBookImpl::new("test_token".to_string(), 100); - + // Add many orders to test memory efficiency for i in 0..1000 { let price = Decimal::from_str(&format!("0.{:04}", 5000 + (i % 100))).unwrap(); let size = Decimal::from_str("100.0").unwrap(); - + let delta = polyfill_rs::OrderDelta { token_id: "test_token".to_string(), timestamp: chrono::Utc::now(), - side: if i % 2 == 0 { polyfill_rs::Side::BUY } else { polyfill_rs::Side::SELL }, + side: if i % 2 == 0 { + polyfill_rs::Side::BUY + } else { + polyfill_rs::Side::SELL + }, price, size, sequence: i as u64, }; - + let _ = book.apply_delta(delta); } - + let end_stats = get_memory_stats(); let (bytes_allocated, allocs, deallocs) = ( end_stats.0 - start_stats.0, end_stats.1 - start_stats.1, end_stats.2 - start_stats.2, ); - + println!("šŸ“ˆ Order book (1000 updates):"); - println!(" {} allocs, {} frees, {} bytes allocated", allocs, deallocs, bytes_allocated); - println!("šŸ“Š Per update: {:.1} bytes/update", bytes_allocated as f64 / 1000.0); - + println!( + " {} allocs, {} frees, {} bytes allocated", + allocs, deallocs, bytes_allocated + ); + println!( + "šŸ“Š Per update: {:.1} bytes/update", + bytes_allocated as f64 / 1000.0 + ); + // Test 3: JSON Parsing Memory println!("\nšŸ“Š Test 3: JSON Parsing Memory Usage"); println!("==================================="); - + let sample_json = r#"{ "data": [ { @@ -168,69 +193,83 @@ async fn main() -> Result<(), Box> { } ] }"#; - + reset_counters(); let start_stats = get_memory_stats(); - + // Parse JSON 1000 times to measure memory usage for _ in 0..1000 { let _: Result = serde_json::from_str(sample_json); } - + let end_stats = get_memory_stats(); let (bytes_allocated, allocs, deallocs) = ( end_stats.0 - start_stats.0, end_stats.1 - start_stats.1, end_stats.2 - start_stats.2, ); - + println!("šŸ“ˆ JSON parsing (1000 operations):"); - println!(" {} allocs, {} frees, {} bytes allocated", allocs, deallocs, bytes_allocated); - println!("šŸ“Š Per parse: {:.1} bytes/parse", bytes_allocated as f64 / 1000.0); - + println!( + " {} allocs, {} frees, {} bytes allocated", + allocs, deallocs, bytes_allocated + ); + println!( + "šŸ“Š Per parse: {:.1} bytes/parse", + bytes_allocated as f64 / 1000.0 + ); + // Test 4: Fixed-point vs Decimal Memory println!("\nšŸ“Š Test 4: Fixed-point vs Decimal Memory"); println!("======================================="); - + // Test Decimal operations reset_counters(); let start_stats = get_memory_stats(); - + let mut decimals = Vec::new(); for i in 0..1000 { let decimal = Decimal::from_str(&format!("0.{:04}", i)).unwrap(); decimals.push(decimal); } - + let end_stats = get_memory_stats(); let decimal_memory = end_stats.0 - start_stats.0; let decimal_allocs = end_stats.1 - start_stats.1; - + println!("šŸ“ˆ Decimal operations (1000 values):"); - println!(" {} allocs, {} bytes allocated", decimal_allocs, decimal_memory); - + println!( + " {} allocs, {} bytes allocated", + decimal_allocs, decimal_memory + ); + // Test fixed-point operations reset_counters(); let start_stats = get_memory_stats(); - + let mut fixed_points = Vec::new(); for i in 0..1000 { let fixed_point = (i as u32) * 10000; // Scale factor of 10000 fixed_points.push(fixed_point); } - + let end_stats = get_memory_stats(); let fixed_memory = end_stats.0 - start_stats.0; let fixed_allocs = end_stats.1 - start_stats.1; - + println!("šŸ“ˆ Fixed-point operations (1000 values):"); - println!(" {} allocs, {} bytes allocated", fixed_allocs, fixed_memory); - + println!( + " {} allocs, {} bytes allocated", + fixed_allocs, fixed_memory + ); + if decimal_memory > 0 && fixed_memory > 0 { - println!("šŸ“Š Fixed-point vs Decimal: {:.1}x less memory", - decimal_memory as f64 / fixed_memory as f64); + println!( + "šŸ“Š Fixed-point vs Decimal: {:.1}x less memory", + decimal_memory as f64 / fixed_memory as f64 + ); } - + println!("\nšŸŽÆ Memory Benchmark Summary"); println!("=========================="); println!("Key Findings:"); @@ -238,8 +277,8 @@ async fn main() -> Result<(), Box> { println!(" • Fixed-point arithmetic: Significantly less memory than Decimal"); println!(" • JSON parsing: Efficient deserialization"); println!(" • Network operations: Memory usage dominated by response size"); - + println!("\nNote: These are ACTUAL measured values, not estimates!"); - + Ok(()) } diff --git a/examples/network_optimization_test.rs b/examples/network_optimization_test.rs index bd4f01c..6e2a5da 100644 --- a/examples/network_optimization_test.rs +++ b/examples/network_optimization_test.rs @@ -5,18 +5,24 @@ use std::time::Instant; async fn main() -> Result<(), Box> { println!("šŸš€ Network Optimization Test - polyfill-rs"); println!("==========================================="); - + // Test different client configurations let clients = vec![ ("Standard", ClobClient::new("https://clob.polymarket.com")), - ("Colocated", ClobClient::new_colocated("https://clob.polymarket.com")), - ("Internet", ClobClient::new_internet("https://clob.polymarket.com")), + ( + "Colocated", + ClobClient::new_colocated("https://clob.polymarket.com"), + ), + ( + "Internet", + ClobClient::new_internet("https://clob.polymarket.com"), + ), ]; - + for (name, client) in clients { println!("\nšŸ“Š Testing {} Client Configuration", name); println!("{}=", "=".repeat(40 + name.len())); - + // Test 1: Server time (baseline latency) println!(" šŸ” Server Time Test:"); let mut times = Vec::new(); @@ -27,36 +33,42 @@ async fn main() -> Result<(), Box> { let duration = start.elapsed(); times.push(duration); if i < 2 { - println!(" Run {}: āœ… {} in {:?}", i+1, timestamp, duration); + println!(" Run {}: āœ… {} in {:?}", i + 1, timestamp, duration); } - } + }, Err(e) => { let duration = start.elapsed(); times.push(duration); if i < 2 { - println!(" Run {}: āŒ Error in {:?}: {}", i+1, duration, e); + println!(" Run {}: āŒ Error in {:?}: {}", i + 1, duration, e); } - } + }, } } - + if !times.is_empty() { let avg = times.iter().sum::() / times.len() as u32; let min = times.iter().min().unwrap(); let max = times.iter().max().unwrap(); let std_dev = { let mean = avg.as_millis() as f64; - let variance = times.iter() + let variance = times + .iter() .map(|t| (t.as_millis() as f64 - mean).powi(2)) - .sum::() / times.len() as f64; + .sum::() + / times.len() as f64; variance.sqrt() }; - - println!(" šŸ“ˆ Average: {:.1}ms ± {:.1}ms", avg.as_millis(), std_dev); + + println!( + " šŸ“ˆ Average: {:.1}ms ± {:.1}ms", + avg.as_millis(), + std_dev + ); println!(" šŸ“Š Range: {:?} - {:?}", min, max); println!(" 🌐 Best: {:?}", min); } - + // Test 2: Market data fetching println!(" šŸ” Market Data Test:"); let mut times = Vec::new(); @@ -67,29 +79,34 @@ async fn main() -> Result<(), Box> { let duration = start.elapsed(); times.push(duration); if i < 2 { - println!(" Run {}: āœ… {} markets in {:?}", i+1, markets.data.len(), duration); + println!( + " Run {}: āœ… {} markets in {:?}", + i + 1, + markets.data.len(), + duration + ); } - } + }, Err(e) => { let duration = start.elapsed(); times.push(duration); if i < 2 { - println!(" Run {}: āŒ Error in {:?}: {}", i+1, duration, e); + println!(" Run {}: āŒ Error in {:?}: {}", i + 1, duration, e); } - } + }, } } - + if !times.is_empty() { let avg = times.iter().sum::() / times.len() as u32; let min = times.iter().min().unwrap(); let max = times.iter().max().unwrap(); - + println!(" šŸ“ˆ Average: {:?}", avg); println!(" šŸ“Š Range: {:?} - {:?}", min, max); println!(" 🌐 Best: {:?}", min); } - + // Test 3: Connection reuse test println!(" šŸ” Connection Reuse Test:"); let start = Instant::now(); @@ -99,18 +116,18 @@ async fn main() -> Result<(), Box> { if i == 0 { println!(" First request: {:?}", start.elapsed()); } - } + }, Err(e) => { - println!(" Error on request {}: {}", i+1, e); + println!(" Error on request {}: {}", i + 1, e); break; - } + }, } } let total_time = start.elapsed(); println!(" šŸ“ˆ 5 requests total: {:?}", total_time); println!(" šŸ“Š Average per request: {:?}", total_time / 5); } - + println!("\nšŸŽÆ Network Optimization Summary"); println!("==============================="); println!("HTTP Client Optimizations Applied:"); @@ -119,17 +136,17 @@ async fn main() -> Result<(), Box> { println!(" • HTTP/2 with keep-alive"); println!(" • Optimized timeouts for different environments"); println!(" • Compression enabled/disabled based on use case"); - + println!("\nConfiguration Recommendations:"); println!(" • Colocated: Use for servers close to exchange"); println!(" • Internet: Use for retail/remote connections"); println!(" • Standard: Balanced settings for most use cases"); - + println!("\nAdditional Optimizations Available:"); println!(" • Custom DNS resolver"); println!(" • Connection pre-warming"); println!(" • Request batching"); println!(" • Circuit breaker patterns"); - + Ok(()) } diff --git a/examples/network_timing_test.rs b/examples/network_timing_test.rs index ac015ab..cff7290 100644 --- a/examples/network_timing_test.rs +++ b/examples/network_timing_test.rs @@ -5,13 +5,13 @@ use std::time::Instant; async fn main() -> Result<(), Box> { println!("🌐 Network Latency Test for polyfill-rs"); println!("======================================"); - + let client = ClobClient::new("https://clob.polymarket.com"); - + // Test 1: Simplified markets (comparable to original 404.5ms benchmark) println!("\nšŸ“Š Test 1: Simplified Markets"); println!("-----------------------------"); - + let mut times = Vec::new(); for i in 0..5 { let start = Instant::now(); @@ -19,31 +19,38 @@ async fn main() -> Result<(), Box> { Ok(markets) => { let duration = start.elapsed(); times.push(duration); - println!(" Run {}: āœ… {} markets in {:?}", i+1, markets.data.len(), duration); - } + println!( + " Run {}: āœ… {} markets in {:?}", + i + 1, + markets.data.len(), + duration + ); + }, Err(e) => { let duration = start.elapsed(); times.push(duration); - println!(" Run {}: āŒ Error in {:?}: {}", i+1, duration, e); - } + println!(" Run {}: āŒ Error in {:?}: {}", i + 1, duration, e); + }, } } - + if !times.is_empty() { let avg = times.iter().sum::() / times.len() as u32; let min = times.iter().min().unwrap(); let max = times.iter().max().unwrap(); - + println!(" šŸ“ˆ Average: {:?}", avg); println!(" šŸ“Š Range: {:?} - {:?}", min, max); - println!(" šŸ†š vs original (404.5ms): {:.1}x", - 404.5 / avg.as_millis() as f64); + println!( + " šŸ†š vs original (404.5ms): {:.1}x", + 404.5 / avg.as_millis() as f64 + ); } - + // Test 2: Full markets println!("\nšŸ“Š Test 2: Full Markets"); println!("----------------------"); - + let mut times = Vec::new(); for i in 0..3 { let start = Instant::now(); @@ -51,29 +58,34 @@ async fn main() -> Result<(), Box> { Ok(markets) => { let duration = start.elapsed(); times.push(duration); - println!(" Run {}: āœ… {} markets in {:?}", i+1, markets.data.len(), duration); - } + println!( + " Run {}: āœ… {} markets in {:?}", + i + 1, + markets.data.len(), + duration + ); + }, Err(e) => { let duration = start.elapsed(); times.push(duration); - println!(" Run {}: āŒ Error in {:?}: {}", i+1, duration, e); - } + println!(" Run {}: āŒ Error in {:?}: {}", i + 1, duration, e); + }, } } - + if !times.is_empty() { let avg = times.iter().sum::() / times.len() as u32; let min = times.iter().min().unwrap(); let max = times.iter().max().unwrap(); - + println!(" šŸ“ˆ Average: {:?}", avg); println!(" šŸ“Š Range: {:?} - {:?}", min, max); } - + // Test 3: Server time (lightweight endpoint) println!("\nšŸ“Š Test 3: Server Time (Lightweight)"); println!("-----------------------------------"); - + let mut times = Vec::new(); for i in 0..10 { let start = Instant::now(); @@ -82,27 +94,32 @@ async fn main() -> Result<(), Box> { let duration = start.elapsed(); times.push(duration); if i == 0 { - println!(" Run {}: āœ… Timestamp {} in {:?}", i+1, timestamp, duration); + println!( + " Run {}: āœ… Timestamp {} in {:?}", + i + 1, + timestamp, + duration + ); } - } + }, Err(e) => { let duration = start.elapsed(); times.push(duration); - println!(" Run {}: āŒ Error in {:?}: {}", i+1, duration, e); - } + println!(" Run {}: āŒ Error in {:?}: {}", i + 1, duration, e); + }, } } - + if !times.is_empty() { let avg = times.iter().sum::() / times.len() as u32; let min = times.iter().min().unwrap(); let max = times.iter().max().unwrap(); - + println!(" šŸ“ˆ Average: {:?}", avg); println!(" šŸ“Š Range: {:?} - {:?}", min, max); println!(" 🌐 Network baseline latency: ~{:?}", min); } - + println!("\nšŸŽÆ Summary"); println!("========="); println!("Network latency dominates end-to-end performance."); @@ -115,6 +132,6 @@ async fn main() -> Result<(), Box> { println!("• Run from same geographic location"); println!("• Use same network conditions"); println!("• Measure full end-to-end latency"); - + Ok(()) } diff --git a/examples/proper_auth_benchmark.rs b/examples/proper_auth_benchmark.rs index cb7f953..c0dc3de 100644 --- a/examples/proper_auth_benchmark.rs +++ b/examples/proper_auth_benchmark.rs @@ -5,12 +5,12 @@ use std::time::Instant; async fn main() -> Result<(), Box> { println!("šŸ” Proper Authenticated Benchmark - Real Performance"); println!("==================================================="); - + // Note: For a real benchmark, we'd need: // 1. A private key to initialize the signer // 2. Proper API credential setup // 3. Valid market/token IDs - + println!("āš ļø Authentication Setup Required"); println!("================================"); println!("To get real order creation benchmarks, we need:"); @@ -18,13 +18,13 @@ async fn main() -> Result<(), Box> { println!(" 2. Proper client initialization with credentials"); println!(" 3. Valid market context for orders"); println!(); - + // What we CAN measure: Network performance let client = ClobClient::new_internet("https://clob.polymarket.com"); - + println!("šŸ“Š What We CAN Measure: Network Performance"); println!("=========================================="); - + // Test 1: Basic connectivity (network baseline) println!("\nšŸ” Network Baseline Test:"); let mut baseline_times = Vec::new(); @@ -33,24 +33,30 @@ async fn main() -> Result<(), Box> { let result = client.get_server_time().await; let duration = start.elapsed(); baseline_times.push(duration); - + match result { Ok(timestamp) => { if i < 2 { - println!(" Run {}: āœ… Server time {} in {:?}", i+1, timestamp, duration); + println!( + " Run {}: āœ… Server time {} in {:?}", + i + 1, + timestamp, + duration + ); } - } + }, Err(e) => { if i < 2 { - println!(" Run {}: āŒ Error in {:?}: {}", i+1, duration, e); + println!(" Run {}: āŒ Error in {:?}: {}", i + 1, duration, e); } - } + }, } } - - let baseline_avg = baseline_times.iter().sum::() / baseline_times.len() as u32; + + let baseline_avg = + baseline_times.iter().sum::() / baseline_times.len() as u32; println!(" šŸ“ˆ Network baseline: {:?}", baseline_avg); - + // Test 2: Market data (what we successfully measured before) println!("\nšŸ” Market Data Performance:"); let mut market_times = Vec::new(); @@ -59,42 +65,57 @@ async fn main() -> Result<(), Box> { let result = client.get_sampling_simplified_markets(None).await; let duration = start.elapsed(); market_times.push(duration); - + match result { Ok(markets) => { if i < 2 { - println!(" Run {}: āœ… {} markets in {:?}", i+1, markets.data.len(), duration); + println!( + " Run {}: āœ… {} markets in {:?}", + i + 1, + markets.data.len(), + duration + ); } - } + }, Err(e) => { if i < 2 { - println!(" Run {}: āŒ Error in {:?}: {}", i+1, duration, e); + println!(" Run {}: āŒ Error in {:?}: {}", i + 1, duration, e); } - } + }, } } - + let market_avg = market_times.iter().sum::() / market_times.len() as u32; println!(" šŸ“ˆ Market data average: {:?}", market_avg); - println!(" šŸ†š vs original (404.5ms): {:.1}x faster", 404.5 / market_avg.as_millis() as f64); - + println!( + " šŸ†š vs original (404.5ms): {:.1}x faster", + 404.5 / market_avg.as_millis() as f64 + ); + println!("\nšŸŽÆ Realistic Performance Estimates"); println!("================================="); - + println!("Based on our network measurements:"); println!(" • Network baseline: {:?}", baseline_avg); - println!(" • Market data: {:?} (3.8x faster than original)", market_avg); + println!( + " • Market data: {:?} (3.8x faster than original)", + market_avg + ); println!(); - + println!("For order creation (266.5ms original):"); println!(" • Network component: ~{:?} (measured)", baseline_avg); println!(" • EIP-712 signing: ~5-20ms (typical crypto operation)"); println!(" • JSON serialization: ~1ms (measured separately)"); - println!(" • Estimated total: ~{:?} (vs 266.5ms original)", - baseline_avg + std::time::Duration::from_millis(15)); - println!(" • Estimated improvement: {:.1}x faster", - 266.5 / (baseline_avg.as_millis() + 15) as f64); - + println!( + " • Estimated total: ~{:?} (vs 266.5ms original)", + baseline_avg + std::time::Duration::from_millis(15) + ); + println!( + " • Estimated improvement: {:.1}x faster", + 266.5 / (baseline_avg.as_millis() + 15) as f64 + ); + println!("\nšŸ“Š Summary of Real Performance"); println!("============================="); println!("What we measured:"); @@ -103,11 +124,13 @@ async fn main() -> Result<(), Box> { println!(" āœ… Computational: microsecond-scale operations"); println!(); println!("What we estimate:"); - println!(" šŸ“Š Order creation: ~{:?} (vs 266.5ms = 2.2x faster)", - baseline_avg + std::time::Duration::from_millis(15)); + println!( + " šŸ“Š Order creation: ~{:?} (vs 266.5ms = 2.2x faster)", + baseline_avg + std::time::Duration::from_millis(15) + ); println!(" šŸ“Š All operations benefit from 11% network optimization"); println!(" šŸ“Š Connection reuse provides 70% improvement on subsequent calls"); println!(" šŸ“Š Request batching provides 200% improvement for parallel operations"); - + Ok(()) } diff --git a/examples/quick_demo.rs b/examples/quick_demo.rs index ed90dbd..9aa37de 100644 --- a/examples/quick_demo.rs +++ b/examples/quick_demo.rs @@ -3,23 +3,23 @@ //! This example demonstrates all available API endpoints in a simple, easy-to-run format. //! It can be run without authentication credentials and will test all public endpoints. -use polyfill_rs::{ClobClient, Side, Result, PolyfillError}; +use polyfill_rs::{ClobClient, PolyfillError, Result, Side}; use rust_decimal::Decimal; use tokio::time::{sleep, Duration}; -use tracing::{info, error, warn}; +use tracing::{error, info, warn}; /// Quick demo that tests all available endpoints #[tokio::main] async fn main() -> Result<()> { // Initialize logging tracing_subscriber::fmt::init(); - + info!("Polyfill-rs Quick Demo"); info!("======================"); - + // Create client let client = ClobClient::new("https://clob.polymarket.com"); - + // Test 1: Basic connectivity info!("\nTesting API Connectivity..."); match test_connectivity(&client).await { @@ -27,37 +27,37 @@ async fn main() -> Result<()> { Err(e) => { error!("API connectivity test failed: {}", e); return Err(e); - } + }, } - + // Test 2: Get a valid token ID from markets info!("\nGetting Market Data..."); let token_id = match get_valid_token_id(&client).await { Ok(id) => { info!("Found valid token ID: {}", id); id - } + }, Err(e) => { error!("Failed to get valid token ID: {}", e); return Err(e); - } + }, }; - + // Test 3: Test all market data endpoints info!("\nTesting Market Data Endpoints..."); test_market_data_endpoints(&client, &token_id).await?; - + // Test 4: Test error handling info!("\nTesting Error Handling..."); test_error_handling(&client).await?; - + // Test 5: Performance test info!("\nTesting Performance..."); test_performance(&client, &token_id).await?; - + info!("\nAll tests completed successfully!"); info!("The polyfill-rs client is working correctly with the Polymarket API."); - + Ok(()) } @@ -66,39 +66,43 @@ async fn test_connectivity(client: &ClobClient) -> Result<()> { // Test /ok endpoint let is_ok = client.get_ok().await; if !is_ok { - return Err(PolyfillError::network("API not responding", std::io::Error::other("API not responding"))); + return Err(PolyfillError::network( + "API not responding", + std::io::Error::other("API not responding"), + )); } info!(" /ok endpoint responding"); - + // Test /time endpoint let server_time = client.get_server_time().await?; info!(" Server time: {}", server_time); - + // Verify server time is reasonable (within last 24 hours) let current_time = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); - + let time_diff = server_time.abs_diff(current_time); - - if time_diff > 86400 { // 24 hours + + if time_diff > 86400 { + // 24 hours warn!(" Server time seems off (diff: {} seconds)", time_diff); } else { info!(" Server time is reasonable"); } - + Ok(()) } /// Get a valid token ID from the markets endpoint async fn get_valid_token_id(client: &ClobClient) -> Result { let markets = client.get_sampling_markets(None).await?; - + if markets.data.is_empty() { return Err(PolyfillError::api(404, "No markets found")); } - + // Find a market with active tokens for market in &markets.data { if market.active && !market.closed { @@ -113,8 +117,11 @@ async fn get_valid_token_id(client: &ClobClient) -> Result { } } } - - Err(PolyfillError::api(404, "No active markets with valid tokens found")) + + Err(PolyfillError::api( + 404, + "No active markets with valid tokens found", + )) } /// Test all market data endpoints @@ -122,42 +129,46 @@ async fn test_market_data_endpoints(client: &ClobClient, token_id: &str) -> Resu // Test order book info!(" Testing order book endpoint..."); let order_book = client.get_order_book(token_id).await?; - info!(" Order book: {} bids, {} asks", order_book.bids.len(), order_book.asks.len()); - + info!( + " Order book: {} bids, {} asks", + order_book.bids.len(), + order_book.asks.len() + ); + // Test midpoint info!(" Testing midpoint endpoint..."); let midpoint = client.get_midpoint(token_id).await?; info!(" Midpoint: {}", midpoint.mid); - + // Test spread info!(" Testing spread endpoint..."); let spread = client.get_spread(token_id).await?; info!(" Spread: {}", spread.spread); - + // Test buy price info!(" Testing buy price endpoint..."); let buy_price = client.get_price(token_id, Side::BUY).await?; info!(" Buy price: {}", buy_price.price); - + // Test sell price info!(" Testing sell price endpoint..."); let sell_price = client.get_price(token_id, Side::SELL).await?; info!(" Sell price: {}", sell_price.price); - + // Test tick size info!(" Testing tick size endpoint..."); let tick_size = client.get_tick_size(token_id).await?; info!(" Tick size: {}", tick_size); - + // Test neg risk info!(" Testing neg risk endpoint..."); let neg_risk = client.get_neg_risk(token_id).await?; info!(" Neg risk: {}", neg_risk); - + // Validate data consistency info!(" Validating data consistency..."); validate_market_data(&order_book, &midpoint, &spread, &buy_price, &sell_price)?; - + Ok(()) } @@ -175,37 +186,39 @@ fn validate_market_data( } else { info!(" Order book has liquidity"); } - + // Check that prices are positive if buy_price.price <= Decimal::ZERO { warn!(" Buy price is not positive: {}", buy_price.price); } else { info!(" Buy price is positive"); } - + if sell_price.price <= Decimal::ZERO { warn!(" Sell price is not positive: {}", sell_price.price); } else { info!(" Sell price is positive"); } - + // Check that spread is reasonable if spread.spread < Decimal::ZERO { warn!(" Spread is negative: {}", spread.spread); } else { info!(" Spread is non-negative"); } - + // Check that midpoint is between buy and sell prices (if both exist) if buy_price.price > Decimal::ZERO && sell_price.price > Decimal::ZERO { if midpoint.mid < buy_price.price || midpoint.mid > sell_price.price { - warn!(" Midpoint {} is not between buy {} and sell {}", - midpoint.mid, buy_price.price, sell_price.price); + warn!( + " Midpoint {} is not between buy {} and sell {}", + midpoint.mid, buy_price.price, sell_price.price + ); } else { info!(" Midpoint is between buy and sell prices"); } } - + Ok(()) } @@ -217,35 +230,33 @@ async fn test_error_handling(client: &ClobClient) -> Result<()> { match result { Ok(_) => { warn!(" Invalid token ID returned data instead of error"); - } - Err(e) => { - match e { - PolyfillError::Api { status, .. } => { - if status >= 400 { - info!(" Invalid token ID correctly returned error: {}", status); - } else { - warn!(" Unexpected status code for invalid token: {}", status); - } + }, + Err(e) => match e { + PolyfillError::Api { status, .. } => { + if status >= 400 { + info!(" Invalid token ID correctly returned error: {}", status); + } else { + warn!(" Unexpected status code for invalid token: {}", status); } - _ => { - info!(" Invalid token ID returned error: {:?}", e); - } - } - } + }, + _ => { + info!(" Invalid token ID returned error: {:?}", e); + }, + }, } - + // Test with empty token ID info!(" Testing empty token ID..."); let result = client.get_order_book("").await; match result { Ok(_) => { warn!(" Empty token ID returned data instead of error"); - } + }, Err(e) => { info!(" Empty token ID correctly returned error: {:?}", e); - } + }, } - + Ok(()) } @@ -254,56 +265,70 @@ async fn test_performance(client: &ClobClient, token_id: &str) -> Result<()> { let mut total_time = Duration::from_secs(0); let mut success_count = 0; let test_count = 5; - + info!(" Running {} performance tests...", test_count); - + for i in 1..=test_count { let start = std::time::Instant::now(); - + // Test a mix of endpoints let results = tokio::join!( client.get_server_time(), client.get_midpoint(token_id), client.get_spread(token_id), ); - + let duration = start.elapsed(); total_time += duration; - + match results { (Ok(_), Ok(_), Ok(_)) => { success_count += 1; - info!(" Test {}: PASSED {:.2}ms", i, duration.as_secs_f64() * 1000.0); - } + info!( + " Test {}: PASSED {:.2}ms", + i, + duration.as_secs_f64() * 1000.0 + ); + }, _ => { - warn!(" Test {}: FAILED in {:.2}ms", i, duration.as_secs_f64() * 1000.0); - } + warn!( + " Test {}: FAILED in {:.2}ms", + i, + duration.as_secs_f64() * 1000.0 + ); + }, } - + // Small delay between tests sleep(Duration::from_millis(100)).await; } - + let avg_time = total_time / test_count as u32; let success_rate = (success_count as f64 / test_count as f64) * 100.0; - + info!(" Performance Summary:"); info!(" Success rate: {:.1}%", success_rate); - info!(" Average response time: {:.2}ms", avg_time.as_secs_f64() * 1000.0); + info!( + " Average response time: {:.2}ms", + avg_time.as_secs_f64() * 1000.0 + ); info!(" Total time: {:.2}s", total_time.as_secs_f64()); - + // Performance thresholds if avg_time > Duration::from_secs(2) { - warn!(" Average response time is slow: {:.2}ms", avg_time.as_secs_f64() * 1000.0); + warn!( + " Average response time is slow: {:.2}ms", + avg_time.as_secs_f64() * 1000.0 + ); } else { info!(" Response times are acceptable"); } - + if success_rate < 80.0 { warn!(" Success rate is low: {:.1}%", success_rate); } else { info!(" Success rate is good"); } - + Ok(()) } diff --git a/examples/real_network_benchmark.rs b/examples/real_network_benchmark.rs index 06e7d86..149c590 100644 --- a/examples/real_network_benchmark.rs +++ b/examples/real_network_benchmark.rs @@ -7,13 +7,13 @@ use std::time::Instant; async fn main() -> Result<(), Box> { // Load environment variables from .env file dotenv::dotenv().ok(); - + println!("šŸš€ Real Network Benchmark - polyfill-rs vs polymarket-rs-client"); println!("================================================================"); - + // Set up client with credentials let client = ClobClient::new("https://clob.polymarket.com"); - + // API credentials from .env file let _api_key = std::env::var("POLYMARKET_API_KEY") .map_err(|_| "POLYMARKET_API_KEY not found in .env file")?; @@ -21,16 +21,16 @@ async fn main() -> Result<(), Box> { .map_err(|_| "POLYMARKET_SECRET not found in .env file")?; let _passphrase = std::env::var("POLYMARKET_PASSPHRASE") .map_err(|_| "POLYMARKET_PASSPHRASE not found in .env file")?; - + println!("āœ… Loaded API credentials from .env file"); - + println!("šŸ”‘ Using API credentials for authenticated requests"); - + // Test 1: Simplified Markets (matches original 404.5ms benchmark) println!("\nšŸ“Š Test 1: Fetch Simplified Markets"); println!("==================================="); println!("Original polymarket-rs-client: 404.5ms ± 22.9ms"); - + let mut times = Vec::new(); for i in 0..10 { let start = Instant::now(); @@ -39,42 +39,59 @@ async fn main() -> Result<(), Box> { let duration = start.elapsed(); times.push(duration); if i < 3 { - println!(" Run {}: āœ… {} markets in {:?}", i+1, markets.data.len(), duration); + println!( + " Run {}: āœ… {} markets in {:?}", + i + 1, + markets.data.len(), + duration + ); } - } + }, Err(e) => { let duration = start.elapsed(); times.push(duration); if i < 3 { - println!(" Run {}: āŒ Error in {:?}: {}", i+1, duration, e); + println!(" Run {}: āŒ Error in {:?}: {}", i + 1, duration, e); } - } + }, } } - + if !times.is_empty() { let avg = times.iter().sum::() / times.len() as u32; let min = times.iter().min().unwrap(); let max = times.iter().max().unwrap(); let std_dev = { let mean = avg.as_millis() as f64; - let variance = times.iter() + let variance = times + .iter() .map(|t| (t.as_millis() as f64 - mean).powi(2)) - .sum::() / times.len() as f64; + .sum::() + / times.len() as f64; variance.sqrt() }; - - println!(" šŸ“ˆ polyfill-rs: {:.1}ms ± {:.1}ms", avg.as_millis(), std_dev); + + println!( + " šŸ“ˆ polyfill-rs: {:.1}ms ± {:.1}ms", + avg.as_millis(), + std_dev + ); println!(" šŸ“Š Range: {:?} - {:?}", min, max); - println!(" šŸ†š vs original: {:.1}x {}", - 404.5 / avg.as_millis() as f64, - if avg.as_millis() < 405 { "faster" } else { "slower" }); + println!( + " šŸ†š vs original: {:.1}x {}", + 404.5 / avg.as_millis() as f64, + if avg.as_millis() < 405 { + "faster" + } else { + "slower" + } + ); } - + // Test 2: Full Markets (no direct comparison, but good to measure) println!("\nšŸ“Š Test 2: Fetch Full Markets"); println!("============================="); - + let mut times = Vec::new(); for i in 0..5 { let start = Instant::now(); @@ -83,38 +100,43 @@ async fn main() -> Result<(), Box> { let duration = start.elapsed(); times.push(duration); if i < 2 { - println!(" Run {}: āœ… {} markets in {:?}", i+1, markets.data.len(), duration); + println!( + " Run {}: āœ… {} markets in {:?}", + i + 1, + markets.data.len(), + duration + ); } - } + }, Err(e) => { let duration = start.elapsed(); times.push(duration); if i < 2 { - println!(" Run {}: āŒ Error in {:?}: {}", i+1, duration, e); + println!(" Run {}: āŒ Error in {:?}: {}", i + 1, duration, e); } - } + }, } } - + if !times.is_empty() { let avg = times.iter().sum::() / times.len() as u32; let min = times.iter().min().unwrap(); let max = times.iter().max().unwrap(); - + println!(" šŸ“ˆ polyfill-rs: {:?} average", avg); println!(" šŸ“Š Range: {:?} - {:?}", min, max); } - + // Test 3: Order Creation with EIP-712 (matches original 266.5ms benchmark) println!("\nšŸ“Š Test 3: Create Order with EIP-712 Signature"); println!("=============================================="); println!("Original polymarket-rs-client: 266.5ms ± 28.6ms"); - + // First, try to create or derive API key match client.create_or_derive_api_key(None).await { Ok(_creds) => { println!(" šŸ”‘ API credentials set up successfully"); - + // Now test order creation let mut times = Vec::new(); for i in 0..5 { @@ -124,59 +146,71 @@ async fn main() -> Result<(), Box> { Decimal::from_str("1.0").unwrap(), // Minimum order size Side::BUY, ); - + let start = Instant::now(); match client.create_order(&order_args, None, None, None).await { Ok(_order) => { let duration = start.elapsed(); times.push(duration); if i < 2 { - println!(" Run {}: āœ… Order created in {:?}", i+1, duration); + println!(" Run {}: āœ… Order created in {:?}", i + 1, duration); } - + // Cancel the order immediately to clean up // Note: Would need to extract order ID from response for cancellation - } + }, Err(e) => { let duration = start.elapsed(); times.push(duration); if i < 2 { - println!(" Run {}: āŒ Error in {:?}: {}", i+1, duration, e); + println!(" Run {}: āŒ Error in {:?}: {}", i + 1, duration, e); } - } + }, } } - + if !times.is_empty() { let avg = times.iter().sum::() / times.len() as u32; let min = times.iter().min().unwrap(); let max = times.iter().max().unwrap(); let std_dev = { let mean = avg.as_millis() as f64; - let variance = times.iter() + let variance = times + .iter() .map(|t| (t.as_millis() as f64 - mean).powi(2)) - .sum::() / times.len() as f64; + .sum::() + / times.len() as f64; variance.sqrt() }; - - println!(" šŸ“ˆ polyfill-rs: {:.1}ms ± {:.1}ms", avg.as_millis(), std_dev); + + println!( + " šŸ“ˆ polyfill-rs: {:.1}ms ± {:.1}ms", + avg.as_millis(), + std_dev + ); println!(" šŸ“Š Range: {:?} - {:?}", min, max); - println!(" šŸ†š vs original: {:.1}x {}", - 266.5 / avg.as_millis() as f64, - if avg.as_millis() < 267 { "faster" } else { "slower" }); + println!( + " šŸ†š vs original: {:.1}x {}", + 266.5 / avg.as_millis() as f64, + if avg.as_millis() < 267 { + "faster" + } else { + "slower" + } + ); } - } + }, Err(e) => { println!(" āŒ Could not set up API credentials: {}", e); println!(" āš ļø Skipping order creation benchmark"); - } + }, } - + // Test 4: Memory usage comparison println!("\nšŸ“Š Test 4: Memory Usage Analysis"); println!("==============================="); println!("Original: 88,053 allocs, 81,823 frees, 15,945,966 bytes allocated"); - + // This would require memory profiling tools for accurate measurement println!(" šŸ”§ polyfill-rs optimizations:"); println!(" • Fixed-point arithmetic reduces allocation overhead"); @@ -184,34 +218,38 @@ async fn main() -> Result<(), Box> { println!(" • Zero-allocation order book updates"); println!(" • Pre-allocated pools for high-frequency operations"); println!(" šŸ“ˆ Estimated: ~10x reduction in allocations"); - + // Test 5: Computational performance (our strength) println!("\nšŸ“Š Test 5: Computational Performance"); println!("==================================="); - + use polyfill_rs::OrderBookImpl; - + let mut book = OrderBookImpl::new("test_token".to_string(), 100); - + // Order book updates let start = Instant::now(); for i in 0..10000 { let price = Decimal::from_str(&format!("0.{:04}", 5000 + (i % 1000))).unwrap(); let size = Decimal::from_str("100.0").unwrap(); - + let delta = polyfill_rs::OrderDelta { token_id: "test_token".to_string(), timestamp: chrono::Utc::now(), - side: if i % 2 == 0 { polyfill_rs::Side::BUY } else { polyfill_rs::Side::SELL }, + side: if i % 2 == 0 { + polyfill_rs::Side::BUY + } else { + polyfill_rs::Side::SELL + }, price, size, sequence: i as u64, }; - + let _ = book.apply_delta(delta); } let book_duration = start.elapsed(); - + // Fast calculations let start = Instant::now(); for _ in 0..1000000 { @@ -219,12 +257,18 @@ async fn main() -> Result<(), Box> { let _ = book.mid_price_fast(); } let calc_duration = start.elapsed(); - - println!(" ⚔ Order book updates: 10,000 in {:?} ({:.0} ops/sec)", - book_duration, 10000.0 / book_duration.as_secs_f64()); - println!(" ⚔ Fast calculations: 2M in {:?} ({:.0}M ops/sec)", - calc_duration, 2.0 / calc_duration.as_secs_f64()); - + + println!( + " ⚔ Order book updates: 10,000 in {:?} ({:.0} ops/sec)", + book_duration, + 10000.0 / book_duration.as_secs_f64() + ); + println!( + " ⚔ Fast calculations: 2M in {:?} ({:.0}M ops/sec)", + calc_duration, + 2.0 / calc_duration.as_secs_f64() + ); + println!("\nšŸŽÆ Final Comparison Summary"); println!("=========================="); println!("| Metric | polymarket-rs-client | polyfill-rs | Improvement |"); @@ -234,13 +278,13 @@ async fn main() -> Result<(), Box> { println!("| Order book ops | N/A | ~1µs per update | New capability |"); println!("| Fast calculations | N/A | ~500ns per op | New capability |"); println!("| Memory usage | 15.9MB allocated | ~10x less | Significant |"); - + println!("\n✨ Key Advantages of polyfill-rs:"); println!(" • Competitive network performance"); println!(" • Superior computational performance"); println!(" • Memory-efficient data structures"); println!(" • Zero-allocation hot paths"); println!(" • Fixed-point arithmetic optimizations"); - + Ok(()) } diff --git a/examples/simple_network_benchmark.rs b/examples/simple_network_benchmark.rs index ae58396..eaba565 100644 --- a/examples/simple_network_benchmark.rs +++ b/examples/simple_network_benchmark.rs @@ -5,13 +5,13 @@ use std::time::Instant; async fn main() -> Result<(), Box> { println!("šŸš€ Simple Network Benchmark - polyfill-rs"); println!("=========================================="); - + let client = ClobClient::new("https://clob.polymarket.com"); - + // Test 1: Server Time (baseline network latency) println!("\nšŸ“Š Test 1: Server Time (Network Baseline)"); println!("========================================="); - + let mut times = Vec::new(); for i in 0..10 { let start = Instant::now(); @@ -20,52 +20,54 @@ async fn main() -> Result<(), Box> { let duration = start.elapsed(); times.push(duration); if i < 3 { - println!(" Run {}: āœ… {} in {:?}", i+1, timestamp, duration); + println!(" Run {}: āœ… {} in {:?}", i + 1, timestamp, duration); } - } + }, Err(e) => { let duration = start.elapsed(); times.push(duration); - println!(" Run {}: āŒ Error in {:?}: {}", i+1, duration, e); - } + println!(" Run {}: āŒ Error in {:?}: {}", i + 1, duration, e); + }, } } - + if !times.is_empty() { let avg = times.iter().sum::() / times.len() as u32; let min = times.iter().min().unwrap(); let max = times.iter().max().unwrap(); - + println!(" šŸ“ˆ Average: {:?}", avg); println!(" šŸ“Š Range: {:?} - {:?}", min, max); println!(" 🌐 Network baseline: ~{:?}", min); } - + // Test 2: Market Data (comparable to original benchmarks) println!("\nšŸ“Š Test 2: Market Data Fetching"); println!("==============================="); println!("Target: polymarket-rs-client 404.5ms ± 22.9ms"); - + // Try different endpoints to see which ones work let endpoints = vec![ ("Simplified Markets", "get_sampling_simplified_markets"), ("Full Markets", "get_sampling_markets"), ("Market Prices", "get_prices_batch"), ]; - + for (name, _method) in endpoints { println!("\n šŸ” Testing {}:", name); - + let mut times = Vec::new(); for i in 0..5 { let start = Instant::now(); let result = match name { - "Simplified Markets" => { - client.get_sampling_simplified_markets(None).await.map(|r| r.data.len()) - } - "Full Markets" => { - client.get_sampling_markets(None).await.map(|r| r.data.len()) - } + "Simplified Markets" => client + .get_sampling_simplified_markets(None) + .await + .map(|r| r.data.len()), + "Full Markets" => client + .get_sampling_markets(None) + .await + .map(|r| r.data.len()), "Market Prices" => { // Try with some example BookParams let book_params = vec![ @@ -75,96 +77,116 @@ async fn main() -> Result<(), Box> { } ]; client.get_prices(&book_params).await.map(|r| r.len()) - } + }, _ => continue, }; - + let duration = start.elapsed(); times.push(duration); - + match result { Ok(count) => { if i < 2 { - println!(" Run {}: āœ… {} items in {:?}", i+1, count, duration); + println!(" Run {}: āœ… {} items in {:?}", i + 1, count, duration); } - } + }, Err(e) => { if i < 2 { - println!(" Run {}: āŒ Error in {:?}: {}", i+1, duration, e); + println!(" Run {}: āŒ Error in {:?}: {}", i + 1, duration, e); } - } + }, } } - + if !times.is_empty() { let avg = times.iter().sum::() / times.len() as u32; let min = times.iter().min().unwrap(); let max = times.iter().max().unwrap(); let std_dev = { let mean = avg.as_millis() as f64; - let variance = times.iter() + let variance = times + .iter() .map(|t| (t.as_millis() as f64 - mean).powi(2)) - .sum::() / times.len() as f64; + .sum::() + / times.len() as f64; variance.sqrt() }; - - println!(" šŸ“ˆ polyfill-rs: {:.1}ms ± {:.1}ms", avg.as_millis(), std_dev); + + println!( + " šŸ“ˆ polyfill-rs: {:.1}ms ± {:.1}ms", + avg.as_millis(), + std_dev + ); println!(" šŸ“Š Range: {:?} - {:?}", min, max); - + if name == "Simplified Markets" { - println!(" šŸ†š vs original (404.5ms): {:.1}x {}", - 404.5 / avg.as_millis() as f64, - if avg.as_millis() < 405 { "faster" } else { "slower" }); + println!( + " šŸ†š vs original (404.5ms): {:.1}x {}", + 404.5 / avg.as_millis() as f64, + if avg.as_millis() < 405 { + "faster" + } else { + "slower" + } + ); } } } - + // Test 3: Computational Performance (our strength) println!("\nšŸ“Š Test 3: Computational Performance"); println!("==================================="); - + use polyfill_rs::OrderBookImpl; use rust_decimal::Decimal; use std::str::FromStr; - + let mut book = OrderBookImpl::new("test_token".to_string(), 100); - + // Populate the book first for i in 0..100 { let price = Decimal::from_str(&format!("0.{:04}", 5000 + i)).unwrap(); let size = Decimal::from_str("100.0").unwrap(); - + let delta = polyfill_rs::OrderDelta { token_id: "test_token".to_string(), timestamp: chrono::Utc::now(), - side: if i % 2 == 0 { polyfill_rs::Side::BUY } else { polyfill_rs::Side::SELL }, + side: if i % 2 == 0 { + polyfill_rs::Side::BUY + } else { + polyfill_rs::Side::SELL + }, price, size, sequence: i as u64, }; - + let _ = book.apply_delta(delta); } - + // Benchmark order book updates let start = Instant::now(); for i in 0..10000 { let price = Decimal::from_str(&format!("0.{:04}", 5000 + (i % 1000))).unwrap(); let size = Decimal::from_str("100.0").unwrap(); - + let delta = polyfill_rs::OrderDelta { token_id: "test_token".to_string(), timestamp: chrono::Utc::now(), - side: if i % 2 == 0 { polyfill_rs::Side::BUY } else { polyfill_rs::Side::SELL }, + side: if i % 2 == 0 { + polyfill_rs::Side::BUY + } else { + polyfill_rs::Side::SELL + }, price, size, sequence: (i + 1000) as u64, }; - + let _ = book.apply_delta(delta); } let book_duration = start.elapsed(); - + // Benchmark fast calculations let start = Instant::now(); for _ in 0..1000000 { @@ -172,17 +194,23 @@ async fn main() -> Result<(), Box> { let _ = book.mid_price_fast(); } let calc_duration = start.elapsed(); - + println!(" ⚔ Order book: 10,000 updates in {:?}", book_duration); - println!(" šŸ“Š Rate: {:.0} updates/second", 10000.0 / book_duration.as_secs_f64()); - + println!( + " šŸ“Š Rate: {:.0} updates/second", + 10000.0 / book_duration.as_secs_f64() + ); + println!(" ⚔ Fast calcs: 2M operations in {:?}", calc_duration); - println!(" šŸ“Š Rate: {:.0}M operations/second", 2.0 / calc_duration.as_secs_f64()); - + println!( + " šŸ“Š Rate: {:.0}M operations/second", + 2.0 / calc_duration.as_secs_f64() + ); + // Test 4: JSON Parsing Performance println!("\nšŸ“Š Test 4: JSON Parsing Performance"); println!("=================================="); - + let sample_market_json = r#"{ "condition_id": "21742633143463906290569050155826241533067272736897614950488156847949938836455", "question": "Will Donald Trump win the 2024 US Presidential Election?", @@ -213,35 +241,41 @@ async fn main() -> Result<(), Box> { } ] }"#; - + let start = Instant::now(); for _ in 0..10000 { let _: Result = serde_json::from_str(sample_market_json); } let json_duration = start.elapsed(); - + println!(" ⚔ JSON parsing: 10,000 parses in {:?}", json_duration); - println!(" šŸ“Š Rate: {:.0} parses/second", 10000.0 / json_duration.as_secs_f64()); - println!(" šŸ“Š Per parse: {:.1}µs", json_duration.as_micros() as f64 / 10000.0); - + println!( + " šŸ“Š Rate: {:.0} parses/second", + 10000.0 / json_duration.as_secs_f64() + ); + println!( + " šŸ“Š Per parse: {:.1}µs", + json_duration.as_micros() as f64 / 10000.0 + ); + println!("\nšŸŽÆ Summary"); println!("========="); println!("Network Performance:"); println!(" • Competitive with polymarket-rs-client baseline"); println!(" • Network latency dominates end-to-end performance"); println!(" • Geographic location affects results significantly"); - + println!("\nComputational Performance:"); println!(" • Order book operations: Sub-millisecond"); println!(" • Fast calculations: Sub-microsecond"); println!(" • JSON parsing: Microsecond-scale"); println!(" • Memory efficient: Zero-allocation hot paths"); - + println!("\n✨ polyfill-rs provides:"); println!(" • Same network performance as alternatives"); println!(" • Superior computational performance"); println!(" • Memory-optimized data structures"); println!(" • Fixed-point arithmetic advantages"); - + Ok(()) } diff --git a/examples/snipe.rs b/examples/snipe.rs index 3e27a6d..4c8abb7 100644 --- a/examples/snipe.rs +++ b/examples/snipe.rs @@ -13,9 +13,9 @@ use polyfill_rs::{ types::*, utils::time, }; +use rust_decimal::prelude::ToPrimitive; use rust_decimal::Decimal; use rust_decimal_macros::dec; -use rust_decimal::prelude::ToPrimitive; use std::time::Duration; use tokio::time::sleep; use tracing::{error, info, warn}; @@ -92,7 +92,7 @@ impl SnipeStrategy { fill_engine: FillEngine::new( min_order_size, dec!(2.0), // 2% max slippage - 5, // 5 bps fee rate + 5, // 5 bps fee rate ), stats: SnipeStats::default(), } @@ -105,16 +105,16 @@ impl SnipeStrategy { if data.token_id == self.token_id { self.process_book_update(data)?; } - } + }, StreamMessage::Trade { data } => { if data.token_id == self.token_id { self.process_trade(data)?; } - } + }, StreamMessage::Heartbeat { timestamp: _ } => { self.check_stale_quotes()?; - } - _ => {} + }, + _ => {}, } Ok(()) } @@ -123,13 +123,13 @@ impl SnipeStrategy { fn process_book_update(&mut self, delta: OrderDelta) -> Result<()> { // Ensure book exists self.book_manager.get_or_create_book(&self.token_id)?; - + // Update local order book self.book_manager.apply_delta(delta.clone())?; // Get current book state let book = self.book_manager.get_book(&self.token_id)?; - + // Update best prices if let Some(best_bid) = book.bids.first() { self.last_best_bid = Some(best_bid.price); @@ -137,7 +137,7 @@ impl SnipeStrategy { if let Some(best_ask) = book.asks.first() { self.last_best_ask = Some(best_ask.price); } - + self.last_update = time::now_secs(); // Check for trading opportunities @@ -158,10 +158,10 @@ impl SnipeStrategy { // Update statistics self.stats.total_volume += fill.size; - + // Calculate P&L if this was our trade // (In a real implementation, you'd track your own orders) - + Ok(()) } @@ -174,16 +174,14 @@ impl SnipeStrategy { // Calculate spread let spread_pct = match (bid, ask) { - (bid, ask) if bid > dec!(0) && ask > bid => { - (ask - bid) / bid * dec!(100) - } + (bid, ask) if bid > dec!(0) && ask > bid => (ask - bid) / bid * dec!(100), _ => return Ok(()), }; // Check if spread is within our target if spread_pct <= self.max_spread_pct { self.stats.opportunities_detected += 1; - + info!( "Opportunity detected: spread {}% (target: {}%)", spread_pct, self.max_spread_pct @@ -200,8 +198,8 @@ impl SnipeStrategy { fn execute_snipe_order(&mut self, bid: Decimal, ask: Decimal) -> Result<()> { // Calculate order size (random between min and max) let random_factor = Decimal::from(rand::random::() % 100) / Decimal::from(100); - let size = self.min_order_size + - (self.max_order_size - self.min_order_size) * random_factor; + let size = + self.min_order_size + (self.max_order_size - self.min_order_size) * random_factor; // Determine side based on market conditions let side = if bid > ask { @@ -222,7 +220,7 @@ impl SnipeStrategy { // Get current book for execution simulation let book = self.book_manager.get_book(&self.token_id)?; let mut book_impl = polyfill_rs::book::OrderBook::new(self.token_id.clone(), 100); - + // Convert to internal book format for level in &book.bids { book_impl.apply_delta(OrderDelta { @@ -234,7 +232,7 @@ impl SnipeStrategy { sequence: 1, })?; } - + for level in &book.asks { book_impl.apply_delta(OrderDelta { token_id: self.token_id.clone(), @@ -248,7 +246,9 @@ impl SnipeStrategy { // Execute order let start_time = std::time::Instant::now(); - let result = self.fill_engine.execute_market_order(&request, &book_impl)?; + let result = self + .fill_engine + .execute_market_order(&request, &book_impl)?; let fill_time = start_time.elapsed().as_millis() as f64; // Update statistics @@ -258,7 +258,8 @@ impl SnipeStrategy { } // Update average fill time - let total_time = self.stats.avg_fill_time_ms * (self.stats.orders_filled - 1) as f64 + fill_time; + let total_time = + self.stats.avg_fill_time_ms * (self.stats.orders_filled - 1) as f64 + fill_time; self.stats.avg_fill_time_ms = total_time / self.stats.orders_filled as f64; info!( @@ -327,7 +328,11 @@ impl MockMarketData { let new_price = self.base_price * (Decimal::from(1) + price_change); // Generate order book update - let side = if rand::random::() { Side::BUY } else { Side::SELL }; + let side = if rand::random::() { + Side::BUY + } else { + Side::SELL + }; let size = Decimal::from(rand::random::() % 1000 + 100); StreamMessage::BookUpdate { @@ -338,7 +343,7 @@ impl MockMarketData { price: new_price, size, sequence: self.sequence, - } + }, } } } @@ -372,7 +377,7 @@ async fn main() -> Result<()> { while message_count < max_messages { // Generate market update let update = market_data.generate_update(); - + // Process update if let Err(e) = strategy.process_update(update) { error!("Error processing update: {}", e); @@ -397,7 +402,10 @@ async fn main() -> Result<()> { // Print final statistics let final_stats = strategy.get_stats(); info!("Final statistics:"); - info!(" Opportunities detected: {}", final_stats.opportunities_detected); + info!( + " Opportunities detected: {}", + final_stats.opportunities_detected + ); info!(" Orders placed: {}", final_stats.orders_placed); info!(" Orders filled: {}", final_stats.orders_filled); info!(" Total volume: {}", final_stats.total_volume); @@ -405,4 +413,4 @@ async fn main() -> Result<()> { info!("Snipe trading example completed!"); Ok(()) -} \ No newline at end of file +} diff --git a/rustfmt.toml b/rustfmt.toml index 261906c..006a158 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -33,7 +33,6 @@ match_block_trailing_comma = true # Control flow control_brace_style = "AlwaysSameLine" -control_brace_style = "ClosingNextLine" # Functions fn_call_width = 60 diff --git a/src/auth.rs b/src/auth.rs index 7af12c3..a655e3d 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -54,7 +54,6 @@ sol! { } } - /// Get current Unix timestamp in seconds pub fn get_current_unix_time_secs() -> u64 { SystemTime::now() @@ -134,8 +133,10 @@ where method.to_uppercase(), request_path, match body { - Some(b) => serde_json::to_string(b) - .map_err(|e| PolyfillError::parse(format!("Failed to serialize body: {}", e), None))?, + Some(b) => serde_json::to_string(b).map_err(|e| PolyfillError::parse( + format!("Failed to serialize body: {}", e), + None + ))?, None => String::new(), } ); @@ -174,7 +175,8 @@ where let address = encode_prefixed(signer.address().as_slice()); let timestamp = get_current_unix_time_secs(); - let hmac_signature = build_hmac_signature(&api_creds.secret, timestamp, method, req_path, body)?; + let hmac_signature = + build_hmac_signature(&api_creds.secret, timestamp, method, req_path, body)?; Ok(HashMap::from([ (POLY_ADDR_HEADER, address), @@ -197,26 +199,15 @@ mod tests { #[test] fn test_hmac_signature() { - let result = build_hmac_signature::( - "test_secret", - 1234567890, - "GET", - "/test", - None, - ); + let result = + build_hmac_signature::("test_secret", 1234567890, "GET", "/test", None); assert!(result.is_ok()); } #[test] fn test_hmac_signature_with_body() { let body = r#"{"test": "data"}"#; - let result = build_hmac_signature( - "test_secret", - 1234567890, - "POST", - "/orders", - Some(body), - ); + let result = build_hmac_signature("test_secret", 1234567890, "POST", "/orders", Some(body)); assert!(result.is_ok()); let signature = result.unwrap(); assert!(!signature.is_empty()); @@ -228,10 +219,10 @@ mod tests { let timestamp = 1234567890; let method = "GET"; let path = "/test"; - + let sig1 = build_hmac_signature::(secret, timestamp, method, path, None).unwrap(); let sig2 = build_hmac_signature::(secret, timestamp, method, path, None).unwrap(); - + // Same inputs should produce same signature assert_eq!(sig1, sig2); } @@ -240,11 +231,13 @@ mod tests { fn test_hmac_signature_different_inputs() { let secret = "test_secret"; let timestamp = 1234567890; - + let sig1 = build_hmac_signature::(secret, timestamp, "GET", "/test", None).unwrap(); - let sig2 = build_hmac_signature::(secret, timestamp, "POST", "/test", None).unwrap(); - let sig3 = build_hmac_signature::(secret, timestamp, "GET", "/other", None).unwrap(); - + let sig2 = + build_hmac_signature::(secret, timestamp, "POST", "/test", None).unwrap(); + let sig3 = + build_hmac_signature::(secret, timestamp, "GET", "/other", None).unwrap(); + // Different inputs should produce different signatures assert_ne!(sig1, sig2); assert_ne!(sig1, sig3); @@ -253,15 +246,15 @@ mod tests { #[test] fn test_create_l1_headers() { - use alloy_signer_local::PrivateKeySigner; use alloy_primitives::U256; - + use alloy_signer_local::PrivateKeySigner; + let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234"; let signer: PrivateKeySigner = private_key.parse().expect("Valid private key"); - + let result = create_l1_headers(&signer, Some(U256::from(12345))); assert!(result.is_ok()); - + let headers = result.unwrap(); assert!(headers.contains_key("poly_address")); assert!(headers.contains_key("poly_signature")); @@ -271,69 +264,66 @@ mod tests { #[test] fn test_create_l1_headers_different_nonces() { - use alloy_signer_local::PrivateKeySigner; use alloy_primitives::U256; - + use alloy_signer_local::PrivateKeySigner; + let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234"; let signer: PrivateKeySigner = private_key.parse().expect("Valid private key"); - + let headers_1 = create_l1_headers(&signer, Some(U256::from(12345))).unwrap(); let headers_2 = create_l1_headers(&signer, Some(U256::from(54321))).unwrap(); - + // Different nonces should produce different signatures assert_ne!( headers_1.get("poly_signature"), headers_2.get("poly_signature") ); - + // But same address - assert_eq!( - headers_1.get("poly_address"), - headers_2.get("poly_address") - ); + assert_eq!(headers_1.get("poly_address"), headers_2.get("poly_address")); } #[test] fn test_create_l2_headers() { use alloy_signer_local::PrivateKeySigner; - + let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234"; let signer: PrivateKeySigner = private_key.parse().expect("Valid private key"); - + let api_creds = ApiCredentials { api_key: "test_key".to_string(), secret: "test_secret".to_string(), passphrase: "test_passphrase".to_string(), }; - + let result = create_l2_headers::(&signer, &api_creds, "/test", "GET", None); assert!(result.is_ok()); - + let headers = result.unwrap(); assert!(headers.contains_key("poly_api_key")); assert!(headers.contains_key("poly_signature")); assert!(headers.contains_key("poly_timestamp")); assert!(headers.contains_key("poly_passphrase")); - + assert_eq!(headers.get("poly_api_key").unwrap(), "test_key"); assert_eq!(headers.get("poly_passphrase").unwrap(), "test_passphrase"); } #[test] fn test_eip712_signature_format() { - use alloy_signer_local::PrivateKeySigner; use alloy_primitives::U256; - + use alloy_signer_local::PrivateKeySigner; + let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234"; let signer: PrivateKeySigner = private_key.parse().expect("Valid private key"); - + // Test that we can create and sign EIP-712 messages let result = create_l1_headers(&signer, Some(U256::from(12345))); assert!(result.is_ok()); - + let headers = result.unwrap(); let signature = headers.get("poly_signature").unwrap(); - + // EIP-712 signatures should be hex strings of specific length assert!(signature.starts_with("0x")); assert_eq!(signature.len(), 132); // 0x + 130 hex chars = 132 total @@ -344,10 +334,10 @@ mod tests { let ts1 = get_current_unix_time_secs(); std::thread::sleep(std::time::Duration::from_millis(1)); let ts2 = get_current_unix_time_secs(); - + // Timestamps should be increasing assert!(ts2 >= ts1); - + // Should be reasonable current time (after 2020, before 2030) assert!(ts1 > 1_600_000_000); assert!(ts1 < 1_900_000_000); diff --git a/src/book.rs b/src/book.rs index 7550f2c..d94362b 100644 --- a/src/book.rs +++ b/src/book.rs @@ -3,20 +3,20 @@ use crate::errors::{PolyfillError, Result}; use crate::types::*; use crate::utils::math; +use chrono::Utc; use rust_decimal::Decimal; use std::collections::BTreeMap; // BTreeMap keeps prices sorted automatically - crucial for order books use std::sync::{Arc, RwLock}; // For thread-safe access across multiple tasks use tracing::{debug, trace, warn}; // Logging for debugging and monitoring -use chrono::Utc; /// High-performance order book implementation -/// +/// /// This is the core data structure that holds all the live buy/sell orders for a token. /// The efficiency of this code is critical as the order book is constantly being updated as orders are added and removed. -/// +/// /// PERFORMANCE OPTIMIZATION: This struct now uses fixed-point integers internally /// instead of Decimal for maximum speed. The performance difference is dramatic: -/// +/// /// Before (Decimal): ~100ns per operation + memory allocation /// After (fixed-point): ~5ns per operation, zero allocations @@ -24,51 +24,51 @@ use chrono::Utc; pub struct OrderBook { /// Token ID this book represents (like "123456" for a specific prediction market outcome) pub token_id: String, - + /// Hash of token_id for fast lookups (avoids string comparisons in hot path) pub token_id_hash: u64, - + /// Current sequence number for ordering updates /// This helps us ignore old/duplicate updates that arrive out of order pub sequence: u64, - + /// Last update timestamp - when we last got new data for this book pub timestamp: chrono::DateTime, - + /// Bid side (price -> size, sorted descending) - NOW USING FIXED-POINT! /// BTreeMap automatically keeps highest bids first, which is what we want /// Key = price in ticks (like 6500 for $0.65), Value = size in fixed-point units - /// + /// /// BEFORE (slow): bids: BTreeMap, /// AFTER (fast): bids: BTreeMap, - /// + /// /// Why this is faster: /// - Integer comparisons are ~10x faster than Decimal comparisons /// - No memory allocation for each price level /// - Better CPU cache utilization (smaller data structures) bids: BTreeMap, - + /// Ask side (price -> size, sorted ascending) - NOW USING FIXED-POINT! /// BTreeMap keeps lowest asks first - people selling at cheapest prices - /// + /// /// BEFORE (slow): asks: BTreeMap, /// AFTER (fast): asks: BTreeMap, asks: BTreeMap, - + /// Minimum tick size for this market in ticks (like 10 for $0.001 increments) /// Some markets only allow certain price increments /// We store this in ticks for fast validation without conversion tick_size_ticks: Option, - + /// Maximum depth to maintain (how many price levels to keep) - /// + /// /// We don't need to track every single price level, just the best ones because: /// - Trading reality 90% of volume happens in the top 5-10 price levels /// - Execution priority: Orders get filled from best price first, so deep levels often don't matter /// - Market efficiency: If you're buying and best ask is $0.67, you'll never pay $0.95 /// - Risk management: Large orders that would hit deep levels are usually broken up /// - Data freshness: Deep levels often have stale orders from hours/days ago - /// + /// /// Typical values: 10-50 for retail, 100-500 for institutional HFT systems max_depth: usize, } @@ -85,7 +85,7 @@ impl OrderBook { token_id.hash(&mut hasher); hasher.finish() }; - + Self { token_id, token_id_hash, @@ -107,7 +107,7 @@ impl OrderBook { self.tick_size_ticks = Some(tick_size_ticks); Ok(()) } - + /// Set the tick size directly in ticks (even faster) /// Use this when you already have the tick size in our internal format pub fn set_tick_size_ticks(&mut self, tick_size_ticks: Price) { @@ -116,31 +116,34 @@ impl OrderBook { /// Get the current best bid (highest price someone is willing to pay) /// Uses next_back() because BTreeMap sorts ascending, but we want the highest bid - /// + /// /// PERFORMANCE: Now returns data in external format but internally uses fast lookups pub fn best_bid(&self) -> Option { // BEFORE (slow, ~50ns + allocation): // self.bids.iter().next_back().map(|(&price, &size)| BookLevel { price, size }) - + // AFTER (fast, ~5ns, no allocation for the lookup): - self.bids.iter().next_back().map(|(&price_ticks, &size_units)| { - // Convert from internal fixed-point to external Decimal format - // This conversion only happens at the API boundary - BookLevel { - price: price_to_decimal(price_ticks), - size: qty_to_decimal(size_units), - } - }) + self.bids + .iter() + .next_back() + .map(|(&price_ticks, &size_units)| { + // Convert from internal fixed-point to external Decimal format + // This conversion only happens at the API boundary + BookLevel { + price: price_to_decimal(price_ticks), + size: qty_to_decimal(size_units), + } + }) } /// Get the current best ask (lowest price someone is willing to sell at) /// Uses next() because BTreeMap sorts ascending, so first item is lowest ask - /// + /// /// PERFORMANCE: Now returns data in external format but internally uses fast lookups pub fn best_ask(&self) -> Option { // BEFORE (slow, ~50ns + allocation): // self.asks.iter().next().map(|(&price, &size)| BookLevel { price, size }) - + // AFTER (fast, ~5ns, no allocation for the lookup): self.asks.iter().next().map(|(&price_ticks, &size_units)| { // Convert from internal fixed-point to external Decimal format @@ -152,25 +155,27 @@ impl OrderBook { }) } - /// Get the current best bid in fast internal format + /// Get the current best bid in fast internal format /// Use this for internal calculations to avoid conversion overhead pub fn best_bid_fast(&self) -> Option { - self.bids.iter().next_back().map(|(&price, &size)| { - FastBookLevel::new(price, size) - }) + self.bids + .iter() + .next_back() + .map(|(&price, &size)| FastBookLevel::new(price, size)) } - /// Get the current best ask in fast internal format + /// Get the current best ask in fast internal format /// Use this for internal calculations to avoid conversion overhead pub fn best_ask_fast(&self) -> Option { - self.asks.iter().next().map(|(&price, &size)| { - FastBookLevel::new(price, size) - }) + self.asks + .iter() + .next() + .map(|(&price, &size)| FastBookLevel::new(price, size)) } /// Get the current spread (difference between best ask and best bid) /// This tells us how "tight" the market is - smaller spread = more liquid market - /// + /// /// PERFORMANCE: Now uses fast internal calculations, only converts to Decimal at the end pub fn spread(&self) -> Option { // BEFORE (slow, ~100ns + multiple allocations): @@ -178,7 +183,7 @@ impl OrderBook { // (Some(bid), Some(ask)) => Some(ask.price - bid.price), // _ => None, // } - + // AFTER (fast, ~5ns, no allocations): let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?; let spread_ticks = math::spread_fast(best_bid_ticks, best_ask_ticks)?; @@ -187,7 +192,7 @@ impl OrderBook { /// Get the current mid price (halfway between best bid and ask) /// This is often used as the "fair value" of the market - /// + /// /// PERFORMANCE: Now uses fast internal calculations, only converts to Decimal at the end pub fn mid_price(&self) -> Option { // BEFORE (slow, ~80ns + allocations): @@ -195,7 +200,7 @@ impl OrderBook { // self.best_bid()?.price, // self.best_ask()?.price, // ) - + // AFTER (fast, ~3ns, no allocations): let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?; let mid_ticks = math::mid_price_fast(best_bid_ticks, best_ask_ticks)?; @@ -204,7 +209,7 @@ impl OrderBook { /// Get the spread as a percentage (relative to the bid price) /// Useful for comparing spreads across different price levels - /// + /// /// PERFORMANCE: Now uses fast internal calculations and returns basis points pub fn spread_pct(&self) -> Option { let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?; @@ -212,7 +217,7 @@ impl OrderBook { // Convert basis points back to percentage decimal Some(Decimal::from(spread_bps) / Decimal::from(100)) } - + /// Get best bid and ask prices in fast internal format /// Helper method to avoid code duplication and minimize conversions fn best_prices_fast(&self) -> Option<(Price, Price)> { @@ -220,14 +225,14 @@ impl OrderBook { let best_ask_ticks = self.asks.iter().next()?.0; Some((*best_bid_ticks, *best_ask_ticks)) } - + /// Get the current spread in fast internal format (PERFORMANCE OPTIMIZED) /// Returns spread in ticks - use this for internal calculations pub fn spread_fast(&self) -> Option { let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?; math::spread_fast(best_bid_ticks, best_ask_ticks) } - + /// Get the current mid price in fast internal format (PERFORMANCE OPTIMIZED) /// Returns mid price in ticks - use this for internal calculations pub fn mid_price_fast(&self) -> Option { @@ -237,7 +242,7 @@ impl OrderBook { /// Get all bids up to a certain depth (top N price levels) /// Returns them in descending price order (best bids first) - /// + /// /// PERFORMANCE: Converts from internal fixed-point to external Decimal format /// Only call this when you need to return data to external APIs pub fn bids(&self, depth: Option) -> Vec { @@ -255,7 +260,7 @@ impl OrderBook { /// Get all asks up to a certain depth (top N price levels) /// Returns them in ascending price order (best asks first) - /// + /// /// PERFORMANCE: Converts from internal fixed-point to external Decimal format /// Only call this when you need to return data to external APIs pub fn asks(&self, depth: Option) -> Vec { @@ -269,8 +274,8 @@ impl OrderBook { }) .collect() } - - /// Get all bids in fast internal format + + /// Get all bids in fast internal format /// Use this for internal calculations to avoid conversion overhead pub fn bids_fast(&self, depth: Option) -> Vec { let depth = depth.unwrap_or(self.max_depth); @@ -308,7 +313,7 @@ impl OrderBook { /// Apply a delta update to the book (LEGACY VERSION - for external API compatibility) /// A "delta" is an incremental change - like "add 100 tokens at $0.65" or "remove all at $0.70" - /// + /// /// This method converts the external Decimal delta to our internal fixed-point format /// and then calls the fast version. Use apply_delta_fast() directly when possible. pub fn apply_delta(&mut self, delta: OrderDelta) -> Result<()> { @@ -316,16 +321,16 @@ impl OrderBook { let tick_size_decimal = self.tick_size_ticks.map(price_to_decimal); let fast_delta = FastOrderDelta::from_order_delta(&delta, tick_size_decimal) .map_err(|e| PolyfillError::validation(format!("Invalid delta: {}", e)))?; - + // Use the fast internal version self.apply_delta_fast(fast_delta) } - + /// Apply a delta update to the book - /// + /// /// This is the high-performance version that works directly with fixed-point data. /// It includes tick alignment validation and is much faster than the Decimal version. - /// + /// /// Performance improvement: ~50x faster than the old Decimal version! /// - No Decimal conversions in the hot path /// - Integer comparisons instead of Decimal comparisons @@ -334,7 +339,11 @@ impl OrderBook { // Validate sequence ordering - ignore old updates that arrive late // This is crucial for maintaining data integrity in real-time systems if delta.sequence <= self.sequence { - trace!("Ignoring stale delta: {} <= {}", delta.sequence, self.sequence); + trace!( + "Ignoring stale delta: {} <= {}", + delta.sequence, + self.sequence + ); return Ok(()); } @@ -351,7 +360,7 @@ impl OrderBook { // if !is_price_tick_aligned(price_to_decimal(delta.price), tick_size_decimal) { // return Err(...); // } - + // AFTER (fast, ~2ns, pure integer): if tick_size_ticks > 0 && delta.price % tick_size_ticks != 0 { // Price is not aligned to tick size - reject the update @@ -390,7 +399,7 @@ impl OrderBook { /// Apply a bid-side delta (someone wants to buy) - LEGACY VERSION /// If size is 0, it means "remove this price level entirely" /// Otherwise, set the total size at this price level - /// + /// /// This converts to fixed-point and calls the fast version #[allow(dead_code)] fn apply_bid_delta(&mut self, price: Decimal, size: Decimal) { @@ -402,7 +411,7 @@ impl OrderBook { /// Apply an ask-side delta (someone wants to sell) - LEGACY VERSION /// Same logic as bids - size of 0 means remove the price level - /// + /// /// This converts to fixed-point and calls the fast version #[allow(dead_code)] fn apply_ask_delta(&mut self, price: Decimal, size: Decimal) { @@ -411,9 +420,9 @@ impl OrderBook { let size_units = decimal_to_qty(size).unwrap_or(0); self.apply_ask_delta_fast(price_ticks, size_units); } - + /// Apply a bid-side delta (someone wants to buy) - FAST VERSION - /// + /// /// This is the high-performance version that works directly with fixed-point. /// Much faster than the Decimal version - pure integer operations. fn apply_bid_delta_fast(&mut self, price_ticks: Price, size_units: Qty) { @@ -423,7 +432,7 @@ impl OrderBook { // } else { // self.bids.insert(price, size); // } - + // AFTER (fast, ~5ns, no allocation): if size_units == 0 { self.bids.remove(&price_ticks); // No more buyers at this price @@ -433,7 +442,7 @@ impl OrderBook { } /// Apply an ask-side delta (someone wants to sell) - FAST VERSION - /// + /// /// This is the high-performance version that works directly with fixed-point. /// Much faster than the Decimal version - pure integer operations. fn apply_ask_delta_fast(&mut self, price_ticks: Price, size_units: Qty) { @@ -443,7 +452,7 @@ impl OrderBook { // } else { // self.asks.insert(price, size); // } - + // AFTER (fast, ~5ns, no allocation): if size_units == 0 { self.asks.remove(&price_ticks); // No more sellers at this price @@ -454,12 +463,12 @@ impl OrderBook { /// Trim the book to maintain depth limits /// We don't want to track every single price level - just the best ones - /// + /// /// Why limit depth? Several reasons: /// 1. Memory efficiency: A popular market might have thousands of price levels, /// but only the top 10-50 levels are actually tradeable with reasonable size /// 2. Performance: Fewer levels = faster iteration when calculating market impact - /// 3. Relevance: Deep levels (like bids at $0.01 when best bid is $0.65) are + /// 3. Relevance: Deep levels (like bids at $0.01 when best bid is $0.65) are /// mostly noise and will never get hit in normal trading /// 4. Stale data: Deep levels often contain old orders that haven't been cancelled /// 5. Network bandwidth: Less data to send when streaming updates @@ -473,7 +482,7 @@ impl OrderBook { } } - // For asks, remove the HIGHEST prices (worst asks) if we have too many + // For asks, remove the HIGHEST prices (worst asks) if we have too many // Example: If best ask is $0.67, we don't care about asks at $0.95 if self.asks.len() > self.max_depth { let to_remove = self.asks.len() - self.max_depth; @@ -493,16 +502,16 @@ impl OrderBook { pub fn calculate_market_impact(&self, side: Side, size: Decimal) -> Option { // PERFORMANCE NOTE: This method still uses Decimal for external compatibility, // but the internal order book lookups now use our fast fixed-point data structures. - // + // // BEFORE: Each level lookup involved Decimal operations (~50ns each) // AFTER: Level lookups use integer operations (~5ns each) - // + // // For a 10-level impact calculation: 500ns → 50ns (10x speedup) - + // Get the levels we'd be trading against let levels = match side { - Side::BUY => self.asks(None), // If buying, we hit the ask side - Side::SELL => self.bids(None), // If selling, we hit the bid side + Side::BUY => self.asks(None), // If buying, we hit the ask side + Side::SELL => self.bids(None), // If selling, we hit the bid side }; if levels.is_empty() { @@ -517,7 +526,7 @@ impl OrderBook { for level in levels { let fill_size = std::cmp::min(remaining_size, level.size); let level_cost = fill_size * level.price; - + total_cost += level_cost; weighted_price += level_cost; // This accumulates the weighted average remaining_size -= fill_size; @@ -532,21 +541,21 @@ impl OrderBook { // This is a perfect example of why we don't need infinite depth: // If we can't fill your order with the top N levels, you probably // shouldn't be placing that order anyway - it would move the market too much - return None; + return None; } let avg_price = weighted_price / size; - + // Calculate how much we moved the market compared to the best price let impact = match side { Side::BUY => { let best_ask = self.best_ask()?.price; (avg_price - best_ask) / best_ask // How much worse than best ask - } + }, Side::SELL => { let best_bid = self.best_bid()?.price; (best_bid - avg_price) / best_bid // How much worse than best bid - } + }, }; Some(MarketImpact { @@ -572,7 +581,7 @@ impl OrderBook { Ok(ticks) => ticks, Err(_) => return Decimal::ZERO, // Invalid price }; - + match side { Side::BUY => { // How much we can buy at this price (look at asks) @@ -583,13 +592,18 @@ impl OrderBook { // How much we can sell at this price (look at bids) let size_units = self.bids.get(&price_ticks).copied().unwrap_or_default(); qty_to_decimal(size_units) - } + }, } } /// Get the total liquidity within a price range /// Useful for understanding how much depth exists in a certain price band - pub fn liquidity_in_range(&self, min_price: Decimal, max_price: Decimal, side: Side) -> Decimal { + pub fn liquidity_in_range( + &self, + min_price: Decimal, + max_price: Decimal, + side: Side, + ) -> Decimal { // Convert decimal prices to our internal fixed-point representation let min_price_ticks = match decimal_to_price(min_price) { Ok(ticks) => ticks, @@ -599,10 +613,14 @@ impl OrderBook { Ok(ticks) => ticks, Err(_) => return Decimal::ZERO, // Invalid price }; - + let levels: Vec<_> = match side { Side::BUY => self.asks.range(min_price_ticks..=max_price_ticks).collect(), - Side::SELL => self.bids.range(min_price_ticks..=max_price_ticks).rev().collect(), + Side::SELL => self + .bids + .range(min_price_ticks..=max_price_ticks) + .rev() + .collect(), }; // Sum up the sizes, converting from fixed-point back to Decimal @@ -615,7 +633,7 @@ impl OrderBook { pub fn is_valid(&self) -> bool { match (self.best_bid(), self.best_ask()) { (Some(bid), Some(ask)) => bid.price < ask.price, // Normal market condition - _ => true, // Empty book is technically valid + _ => true, // Empty book is technically valid } } } @@ -624,20 +642,20 @@ impl OrderBook { /// This tells you what would happen if you executed a large order #[derive(Debug, Clone)] pub struct MarketImpact { - pub average_price: Decimal, // The average price you'd get across all fills - pub impact_pct: Decimal, // How much worse than the best price (as percentage) - pub total_cost: Decimal, // Total amount you'd pay/receive - pub size_filled: Decimal, // How much of your order got filled + pub average_price: Decimal, // The average price you'd get across all fills + pub impact_pct: Decimal, // How much worse than the best price (as percentage) + pub total_cost: Decimal, // Total amount you'd pay/receive + pub size_filled: Decimal, // How much of your order got filled } /// Thread-safe order book manager /// This manages multiple order books (one per token) and handles concurrent access /// Multiple threads can read/write different books simultaneously -/// +/// /// The depth limiting becomes even more critical here because we might be tracking /// hundreds or thousands of different tokens simultaneously. If each book had /// unlimited depth, we could easily use gigabytes of RAM for mostly useless data. -/// +/// /// Example: 1000 tokens Ɨ 1000 price levels Ɨ 32 bytes per level = 32MB just for prices /// With depth limiting: 1000 tokens Ɨ 50 levels Ɨ 32 bytes = 1.6MB (20x less memory) #[derive(Debug)] @@ -659,9 +677,10 @@ impl OrderBookManager { /// Get or create an order book for a token /// If we don't have a book for this token yet, create a new empty one pub fn get_or_create_book(&self, token_id: &str) -> Result { - let mut books = self.books.write().map_err(|_| { - PolyfillError::internal_simple("Failed to acquire book lock") - })?; + let mut books = self + .books + .write() + .map_err(|_| PolyfillError::internal_simple("Failed to acquire book lock"))?; if let Some(book) = books.get(token_id) { Ok(book.clone()) // Return a copy of the existing book @@ -676,19 +695,18 @@ impl OrderBookManager { /// Update a book with a delta /// This is called when we receive real-time updates from the exchange pub fn apply_delta(&self, delta: OrderDelta) -> Result<()> { - let mut books = self.books.write().map_err(|_| { - PolyfillError::internal_simple("Failed to acquire book lock") - })?; + let mut books = self + .books + .write() + .map_err(|_| PolyfillError::internal_simple("Failed to acquire book lock"))?; // Find the book for this token (must already exist) - let book = books - .get_mut(&delta.token_id) - .ok_or_else(|| { - PolyfillError::market_data( - format!("No book found for token: {}", delta.token_id), - crate::errors::MarketDataErrorKind::TokenNotFound, - ) - })?; + let book = books.get_mut(&delta.token_id).ok_or_else(|| { + PolyfillError::market_data( + format!("No book found for token: {}", delta.token_id), + crate::errors::MarketDataErrorKind::TokenNotFound, + ) + })?; // Apply the update to the specific book book.apply_delta(delta) @@ -697,9 +715,10 @@ impl OrderBookManager { /// Get a book snapshot /// Returns a copy of the current book state that won't change pub fn get_book(&self, token_id: &str) -> Result { - let books = self.books.read().map_err(|_| { - PolyfillError::internal_simple("Failed to acquire book lock") - })?; + let books = self + .books + .read() + .map_err(|_| PolyfillError::internal_simple("Failed to acquire book lock"))?; books .get(token_id) @@ -715,9 +734,10 @@ impl OrderBookManager { /// Get all available books /// Returns snapshots of every book we're currently tracking pub fn get_all_books(&self) -> Result> { - let books = self.books.read().map_err(|_| { - PolyfillError::internal_simple("Failed to acquire book lock") - })?; + let books = self + .books + .read() + .map_err(|_| PolyfillError::internal_simple("Failed to acquire book lock"))?; Ok(books.values().map(|book| book.snapshot()).collect()) } @@ -726,9 +746,10 @@ impl OrderBookManager { /// Cleans up books that haven't been updated recently (probably disconnected) /// This prevents memory leaks from accumulating dead books pub fn cleanup_stale_books(&self, max_age: std::time::Duration) -> Result { - let mut books = self.books.write().map_err(|_| { - PolyfillError::internal_simple("Failed to acquire book lock") - })?; + let mut books = self + .books + .write() + .map_err(|_| PolyfillError::internal_simple("Failed to acquire book lock"))?; let initial_count = books.len(); books.retain(|_, book| !book.is_stale(max_age)); // Keep only non-stale books @@ -748,13 +769,13 @@ impl OrderBookManager { pub struct BookAnalytics { pub token_id: String, pub timestamp: chrono::DateTime, - pub bid_count: usize, // How many different bid price levels - pub ask_count: usize, // How many different ask price levels - pub total_bid_size: Decimal, // Total size of all bids combined - pub total_ask_size: Decimal, // Total size of all asks combined - pub spread: Option, // Current spread (ask - bid) + pub bid_count: usize, // How many different bid price levels + pub ask_count: usize, // How many different ask price levels + pub total_bid_size: Decimal, // Total size of all bids combined + pub total_ask_size: Decimal, // Total size of all asks combined + pub spread: Option, // Current spread (ask - bid) pub spread_pct: Option, // Spread as percentage - pub mid_price: Option, // Current mid price + pub mid_price: Option, // Current mid price pub volatility: Option, // Price volatility (if calculated) } @@ -814,7 +835,7 @@ mod tests { fn test_apply_delta() { // Test that we can apply order book updates let mut book = OrderBook::new("test_token".to_string(), 10); - + // Create a buy order at $0.50 for 100 tokens let delta = OrderDelta { token_id: "test_token".to_string(), @@ -835,7 +856,7 @@ mod tests { fn test_spread_calculation() { // Test that we can calculate the spread between bid and ask let mut book = OrderBook::new("test_token".to_string(), 10); - + // Add a bid at $0.50 book.apply_delta(OrderDelta { token_id: "test_token".to_string(), @@ -844,7 +865,8 @@ mod tests { price: dec!(0.5), size: dec!(100), sequence: 1, - }).unwrap(); + }) + .unwrap(); // Add an ask at $0.52 book.apply_delta(OrderDelta { @@ -854,7 +876,8 @@ mod tests { price: dec!(0.52), size: dec!(100), sequence: 2, - }).unwrap(); + }) + .unwrap(); let spread = book.spread().unwrap(); assert_eq!(spread, dec!(0.02)); // $0.52 - $0.50 = $0.02 @@ -864,7 +887,7 @@ mod tests { fn test_market_impact() { // Test market impact calculation for a large order let mut book = OrderBook::new("test_token".to_string(), 10); - + // Add multiple ask levels (people selling at different prices) // $0.50 for 100 tokens, $0.51 for 100 tokens, $0.52 for 100 tokens for (i, price) in [dec!(0.50), dec!(0.51), dec!(0.52)].iter().enumerate() { @@ -875,7 +898,8 @@ mod tests { price: *price, size: dec!(100), sequence: i as u64 + 1, - }).unwrap(); + }) + .unwrap(); } // Try to buy 150 tokens (will need to hit multiple price levels) @@ -887,21 +911,27 @@ mod tests { #[test] fn test_apply_bid_delta_legacy() { let mut book = OrderBook::new("test_token".to_string(), 10); - + // Test adding a bid - book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::from_str("100.0").unwrap()); - + book.apply_bid_delta( + Decimal::from_str("0.75").unwrap(), + Decimal::from_str("100.0").unwrap(), + ); + let best_bid = book.best_bid(); assert!(best_bid.is_some()); let bid = best_bid.unwrap(); assert_eq!(bid.price, Decimal::from_str("0.75").unwrap()); assert_eq!(bid.size, Decimal::from_str("100.0").unwrap()); - + // Test updating the bid - book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::from_str("150.0").unwrap()); + book.apply_bid_delta( + Decimal::from_str("0.75").unwrap(), + Decimal::from_str("150.0").unwrap(), + ); let updated_bid = book.best_bid().unwrap(); assert_eq!(updated_bid.size, Decimal::from_str("150.0").unwrap()); - + // Test removing the bid book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::ZERO); assert!(book.best_bid().is_none()); @@ -910,21 +940,27 @@ mod tests { #[test] fn test_apply_ask_delta_legacy() { let mut book = OrderBook::new("test_token".to_string(), 10); - + // Test adding an ask - book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::from_str("50.0").unwrap()); - + book.apply_ask_delta( + Decimal::from_str("0.76").unwrap(), + Decimal::from_str("50.0").unwrap(), + ); + let best_ask = book.best_ask(); assert!(best_ask.is_some()); let ask = best_ask.unwrap(); assert_eq!(ask.price, Decimal::from_str("0.76").unwrap()); assert_eq!(ask.size, Decimal::from_str("50.0").unwrap()); - + // Test updating the ask - book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::from_str("75.0").unwrap()); + book.apply_ask_delta( + Decimal::from_str("0.76").unwrap(), + Decimal::from_str("75.0").unwrap(), + ); let updated_ask = book.best_ask().unwrap(); assert_eq!(updated_ask.size, Decimal::from_str("75.0").unwrap()); - + // Test removing the ask book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::ZERO); assert!(book.best_ask().is_none()); @@ -933,35 +969,48 @@ mod tests { #[test] fn test_liquidity_analysis() { let mut book = OrderBook::new("test_token".to_string(), 10); - + // Build order book using legacy methods - book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::from_str("100.0").unwrap()); - book.apply_bid_delta(Decimal::from_str("0.74").unwrap(), Decimal::from_str("50.0").unwrap()); - book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::from_str("80.0").unwrap()); - book.apply_ask_delta(Decimal::from_str("0.77").unwrap(), Decimal::from_str("120.0").unwrap()); - + book.apply_bid_delta( + Decimal::from_str("0.75").unwrap(), + Decimal::from_str("100.0").unwrap(), + ); + book.apply_bid_delta( + Decimal::from_str("0.74").unwrap(), + Decimal::from_str("50.0").unwrap(), + ); + book.apply_ask_delta( + Decimal::from_str("0.76").unwrap(), + Decimal::from_str("80.0").unwrap(), + ); + book.apply_ask_delta( + Decimal::from_str("0.77").unwrap(), + Decimal::from_str("120.0").unwrap(), + ); + // Test liquidity at specific price - when buying, we look at ask liquidity let buy_liquidity = book.liquidity_at_price(Decimal::from_str("0.76").unwrap(), Side::BUY); assert_eq!(buy_liquidity, Decimal::from_str("80.0").unwrap()); - - // Test liquidity at specific price - when selling, we look at bid liquidity - let sell_liquidity = book.liquidity_at_price(Decimal::from_str("0.75").unwrap(), Side::SELL); + + // Test liquidity at specific price - when selling, we look at bid liquidity + let sell_liquidity = + book.liquidity_at_price(Decimal::from_str("0.75").unwrap(), Side::SELL); assert_eq!(sell_liquidity, Decimal::from_str("100.0").unwrap()); - + // Test liquidity in range - when buying, we look at ask liquidity in range let buy_range_liquidity = book.liquidity_in_range( Decimal::from_str("0.74").unwrap(), Decimal::from_str("0.77").unwrap(), - Side::BUY + Side::BUY, ); // Should include ask liquidity: 80 (0.76 ask) + 120 (0.77 ask) = 200 assert_eq!(buy_range_liquidity, Decimal::from_str("200.0").unwrap()); - + // Test liquidity in range - when selling, we look at bid liquidity in range let sell_range_liquidity = book.liquidity_in_range( Decimal::from_str("0.74").unwrap(), Decimal::from_str("0.77").unwrap(), - Side::SELL + Side::SELL, ); // Should include bid liquidity: 50 (0.74 bid) + 100 (0.75 bid) = 150 assert_eq!(sell_range_liquidity, Decimal::from_str("150.0").unwrap()); @@ -970,31 +1019,43 @@ mod tests { #[test] fn test_book_validation() { let mut book = OrderBook::new("test_token".to_string(), 10); - + // Empty book should be valid assert!(book.is_valid()); - + // Add normal levels - book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::from_str("100.0").unwrap()); - book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::from_str("80.0").unwrap()); + book.apply_bid_delta( + Decimal::from_str("0.75").unwrap(), + Decimal::from_str("100.0").unwrap(), + ); + book.apply_ask_delta( + Decimal::from_str("0.76").unwrap(), + Decimal::from_str("80.0").unwrap(), + ); assert!(book.is_valid()); - + // Create crossed book (invalid) - bid higher than ask - book.apply_bid_delta(Decimal::from_str("0.77").unwrap(), Decimal::from_str("50.0").unwrap()); + book.apply_bid_delta( + Decimal::from_str("0.77").unwrap(), + Decimal::from_str("50.0").unwrap(), + ); assert!(!book.is_valid()); } #[test] fn test_book_staleness() { let mut book = OrderBook::new("test_token".to_string(), 10); - + // Fresh book should not be stale assert!(!book.is_stale(Duration::from_secs(60))); // 60 second threshold - + // Add some data - book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::from_str("100.0").unwrap()); + book.apply_bid_delta( + Decimal::from_str("0.75").unwrap(), + Decimal::from_str("100.0").unwrap(), + ); assert!(!book.is_stale(Duration::from_secs(60))); - + // Note: We can't easily test actual staleness without manipulating time, // but we can test the method exists and works with fresh data } @@ -1002,47 +1063,77 @@ mod tests { #[test] fn test_depth_management() { let mut book = OrderBook::new("test_token".to_string(), 3); // Only 3 levels - + // Add multiple levels - book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::from_str("100.0").unwrap()); - book.apply_bid_delta(Decimal::from_str("0.74").unwrap(), Decimal::from_str("50.0").unwrap()); - book.apply_bid_delta(Decimal::from_str("0.73").unwrap(), Decimal::from_str("20.0").unwrap()); - - book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::from_str("80.0").unwrap()); - book.apply_ask_delta(Decimal::from_str("0.77").unwrap(), Decimal::from_str("40.0").unwrap()); - book.apply_ask_delta(Decimal::from_str("0.78").unwrap(), Decimal::from_str("30.0").unwrap()); - + book.apply_bid_delta( + Decimal::from_str("0.75").unwrap(), + Decimal::from_str("100.0").unwrap(), + ); + book.apply_bid_delta( + Decimal::from_str("0.74").unwrap(), + Decimal::from_str("50.0").unwrap(), + ); + book.apply_bid_delta( + Decimal::from_str("0.73").unwrap(), + Decimal::from_str("20.0").unwrap(), + ); + + book.apply_ask_delta( + Decimal::from_str("0.76").unwrap(), + Decimal::from_str("80.0").unwrap(), + ); + book.apply_ask_delta( + Decimal::from_str("0.77").unwrap(), + Decimal::from_str("40.0").unwrap(), + ); + book.apply_ask_delta( + Decimal::from_str("0.78").unwrap(), + Decimal::from_str("30.0").unwrap(), + ); + // Should have levels on each side let bids = book.bids(Some(3)); let asks = book.asks(Some(3)); - + assert!(bids.len() <= 3); assert!(asks.len() <= 3); - + // Best levels should be there - assert_eq!(book.best_bid().unwrap().price, Decimal::from_str("0.75").unwrap()); - assert_eq!(book.best_ask().unwrap().price, Decimal::from_str("0.76").unwrap()); + assert_eq!( + book.best_bid().unwrap().price, + Decimal::from_str("0.75").unwrap() + ); + assert_eq!( + book.best_ask().unwrap().price, + Decimal::from_str("0.76").unwrap() + ); } #[test] fn test_fast_operations() { let mut book = OrderBook::new("test_token".to_string(), 10); - + // Test using legacy methods which call fast operations internally - book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::from_str("100.0").unwrap()); - book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::from_str("80.0").unwrap()); - + book.apply_bid_delta( + Decimal::from_str("0.75").unwrap(), + Decimal::from_str("100.0").unwrap(), + ); + book.apply_ask_delta( + Decimal::from_str("0.76").unwrap(), + Decimal::from_str("80.0").unwrap(), + ); + let best_bid_fast = book.best_bid_fast(); let best_ask_fast = book.best_ask_fast(); - + assert!(best_bid_fast.is_some()); assert!(best_ask_fast.is_some()); - + // Test fast spread and mid price let spread_fast = book.spread_fast(); let mid_fast = book.mid_price_fast(); - + assert!(spread_fast.is_some()); // Should have a spread - assert!(mid_fast.is_some()); // Should have a mid price + assert!(mid_fast.is_some()); // Should have a mid price } -} \ No newline at end of file +} diff --git a/src/client.rs b/src/client.rs index d97e3fa..8e2e650 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,26 +1,26 @@ //! High-performance Rust client for Polymarket -//! +//! //! This module provides a production-ready client for interacting with //! Polymarket, optimized for high-frequency trading environments. use crate::auth::{create_l1_headers, create_l2_headers}; use crate::errors::{PolyfillError, Result}; -use crate::http_config::{create_optimized_client, create_colocated_client, create_internet_client, prewarm_connections}; +use crate::http_config::{ + create_colocated_client, create_internet_client, create_optimized_client, prewarm_connections, +}; use crate::types::{OrderOptions, PostOrder, SignedOrderRequest}; -use reqwest::Client; -use serde_json::Value; -use std::str::FromStr; -use rust_decimal::Decimal; -use rust_decimal::prelude::FromPrimitive; use alloy_primitives::U256; use alloy_signer_local::PrivateKeySigner; -use reqwest::{Method, RequestBuilder}; use reqwest::header::HeaderName; +use reqwest::Client; +use reqwest::{Method, RequestBuilder}; +use rust_decimal::prelude::FromPrimitive; +use rust_decimal::Decimal; +use serde_json::Value; +use std::str::FromStr; // Re-export types for compatibility -pub use crate::types::{ - ApiCredentials as ApiCreds, Side, OrderType, -}; +pub use crate::types::{ApiCredentials as ApiCreds, OrderType, Side}; // Compatibility types #[derive(Debug)] @@ -102,11 +102,12 @@ impl ClobClient { /// Create a client with L1 headers (for authentication) pub fn with_l1_headers(host: &str, private_key: &str, chain_id: u64) -> Self { - let signer = private_key.parse::() + let signer = private_key + .parse::() .expect("Invalid private key"); - + let order_builder = crate::orders::OrderBuilder::new(signer.clone(), None, None); - + Self { http_client: create_optimized_client().unwrap_or_else(|_| Client::new()), base_url: host.to_string(), @@ -118,12 +119,18 @@ impl ClobClient { } /// Create a client with L2 headers (for API key authentication) - pub fn with_l2_headers(host: &str, private_key: &str, chain_id: u64, api_creds: ApiCreds) -> Self { - let signer = private_key.parse::() + pub fn with_l2_headers( + host: &str, + private_key: &str, + chain_id: u64, + api_creds: ApiCreds, + ) -> Self { + let signer = private_key + .parse::() .expect("Invalid private key"); - + let order_builder = crate::orders::OrderBuilder::new(signer.clone(), None, None); - + Self { http_client: create_optimized_client().unwrap_or_else(|_| Client::new()), base_url: host.to_string(), @@ -143,14 +150,18 @@ impl ClobClient { pub async fn prewarm_connections(&self) -> Result<()> { prewarm_connections(&self.http_client, &self.base_url) .await - .map_err(|e| PolyfillError::network(format!("Failed to prewarm connections: {}", e), e))?; + .map_err(|e| { + PolyfillError::network(format!("Failed to prewarm connections: {}", e), e) + })?; Ok(()) } /// Get the wallet address pub fn get_address(&self) -> Option { use alloy_primitives::hex; - self.signer.as_ref().map(|s| hex::encode_prefixed(s.address().as_slice())) + self.signer + .as_ref() + .map(|s| hex::encode_prefixed(s.address().as_slice())) } /// Get the collateral token address for the current chain @@ -173,7 +184,12 @@ impl ClobClient { /// Test basic connectivity pub async fn get_ok(&self) -> bool { - match self.http_client.get(format!("{}/ok", self.base_url)).send().await { + match self + .http_client + .get(format!("{}/ok", self.base_url)) + .send() + .await + { Ok(response) => response.status().is_success(), Err(_) => false, } @@ -181,34 +197,42 @@ impl ClobClient { /// Get server time pub async fn get_server_time(&self) -> Result { - let response = self.http_client + let response = self + .http_client .get(format!("{}/time", self.base_url)) .send() .await?; if !response.status().is_success() { - return Err(PolyfillError::api(response.status().as_u16(), "Failed to get server time")); + return Err(PolyfillError::api( + response.status().as_u16(), + "Failed to get server time", + )); } let time_text = response.text().await?; - let timestamp = time_text.trim() + let timestamp = time_text + .trim() .parse::() .map_err(|e| PolyfillError::parse(format!("Invalid timestamp format: {}", e), None))?; Ok(timestamp) } - /// Get order book for a token pub async fn get_order_book(&self, token_id: &str) -> Result { - let response = self.http_client + let response = self + .http_client .get(format!("{}/book", self.base_url)) .query(&[("token_id", token_id)]) .send() .await?; if !response.status().is_success() { - return Err(PolyfillError::api(response.status().as_u16(), "Failed to get order book")); + return Err(PolyfillError::api( + response.status().as_u16(), + "Failed to get order book", + )); } let order_book: OrderBookSummary = response.json().await?; @@ -217,14 +241,18 @@ impl ClobClient { /// Get midpoint for a token pub async fn get_midpoint(&self, token_id: &str) -> Result { - let response = self.http_client + let response = self + .http_client .get(format!("{}/midpoint", self.base_url)) .query(&[("token_id", token_id)]) .send() .await?; if !response.status().is_success() { - return Err(PolyfillError::api(response.status().as_u16(), "Failed to get midpoint")); + return Err(PolyfillError::api( + response.status().as_u16(), + "Failed to get midpoint", + )); } let midpoint: MidpointResponse = response.json().await?; @@ -233,14 +261,18 @@ impl ClobClient { /// Get spread for a token pub async fn get_spread(&self, token_id: &str) -> Result { - let response = self.http_client + let response = self + .http_client .get(format!("{}/spread", self.base_url)) .query(&[("token_id", token_id)]) .send() .await?; if !response.status().is_success() { - return Err(PolyfillError::api(response.status().as_u16(), "Failed to get spread")); + return Err(PolyfillError::api( + response.status().as_u16(), + "Failed to get spread", + )); } let spread: SpreadResponse = response.json().await?; @@ -248,7 +280,10 @@ impl ClobClient { } /// Get spreads for multiple tokens (batch) - pub async fn get_spreads(&self, token_ids: &[String]) -> Result> { + pub async fn get_spreads( + &self, + token_ids: &[String], + ) -> Result> { let request_data: Vec> = token_ids .iter() .map(|id| { @@ -258,33 +293,40 @@ impl ClobClient { }) .collect(); - let response = self.http_client + let response = self + .http_client .post(format!("{}/spreads", self.base_url)) .json(&request_data) .send() .await?; if !response.status().is_success() { - return Err(PolyfillError::api(response.status().as_u16(), "Failed to get batch spreads")); + return Err(PolyfillError::api( + response.status().as_u16(), + "Failed to get batch spreads", + )); } - response.json::>().await + response + .json::>() + .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None)) } /// Get price for a token and side pub async fn get_price(&self, token_id: &str, side: Side) -> Result { - let response = self.http_client + let response = self + .http_client .get(format!("{}/price", self.base_url)) - .query(&[ - ("token_id", token_id), - ("side", side.as_str()), - ]) + .query(&[("token_id", token_id), ("side", side.as_str())]) .send() .await?; if !response.status().is_success() { - return Err(PolyfillError::api(response.status().as_u16(), "Failed to get price")); + return Err(PolyfillError::api( + response.status().as_u16(), + "Failed to get price", + )); } let price: PriceResponse = response.json().await?; @@ -293,21 +335,29 @@ impl ClobClient { /// Get tick size for a token pub async fn get_tick_size(&self, token_id: &str) -> Result { - let response = self.http_client + let response = self + .http_client .get(format!("{}/tick-size", self.base_url)) .query(&[("token_id", token_id)]) .send() .await?; if !response.status().is_success() { - return Err(PolyfillError::api(response.status().as_u16(), "Failed to get tick size")); + return Err(PolyfillError::api( + response.status().as_u16(), + "Failed to get tick size", + )); } let tick_size_response: Value = response.json().await?; let tick_size = tick_size_response["minimum_tick_size"] .as_str() .and_then(|s| Decimal::from_str(s).ok()) - .or_else(|| tick_size_response["minimum_tick_size"].as_f64().map(|f| Decimal::from_f64(f).unwrap_or(Decimal::ZERO))) + .or_else(|| { + tick_size_response["minimum_tick_size"] + .as_f64() + .map(|f| Decimal::from_f64(f).unwrap_or(Decimal::ZERO)) + }) .ok_or_else(|| PolyfillError::parse("Invalid tick size format", None))?; Ok(tick_size) @@ -315,33 +365,48 @@ impl ClobClient { /// Create a new API key pub async fn create_api_key(&self, nonce: Option) -> Result { - let signer = self.signer.as_ref() + let signer = self + .signer + .as_ref() .ok_or_else(|| PolyfillError::auth("Signer not set"))?; - + let headers = create_l1_headers(signer, nonce)?; - let req = self.create_request_with_headers(Method::POST, "/auth/api-key", headers.into_iter()); - + let req = + self.create_request_with_headers(Method::POST, "/auth/api-key", headers.into_iter()); + let response = req.send().await?; if !response.status().is_success() { - return Err(PolyfillError::api(response.status().as_u16(), "Failed to create API key")); + return Err(PolyfillError::api( + response.status().as_u16(), + "Failed to create API key", + )); } - + Ok(response.json::().await?) } /// Derive an existing API key pub async fn derive_api_key(&self, nonce: Option) -> Result { - let signer = self.signer.as_ref() + let signer = self + .signer + .as_ref() .ok_or_else(|| PolyfillError::auth("Signer not set"))?; - + let headers = create_l1_headers(signer, nonce)?; - let req = self.create_request_with_headers(Method::GET, "/auth/derive-api-key", headers.into_iter()); - + let req = self.create_request_with_headers( + Method::GET, + "/auth/derive-api-key", + headers.into_iter(), + ); + let response = req.send().await?; if !response.status().is_success() { - return Err(PolyfillError::api(response.status().as_u16(), "Failed to derive API key")); + return Err(PolyfillError::api( + response.status().as_u16(), + "Failed to derive API key", + )); } - + Ok(response.json::().await?) } @@ -355,23 +420,36 @@ impl ClobClient { /// Get all API keys for the authenticated user pub async fn get_api_keys(&self) -> Result> { - let signer = self.signer.as_ref() + let signer = self + .signer + .as_ref() .ok_or_else(|| PolyfillError::config("Signer not configured"))?; - let api_creds = self.api_creds.as_ref() + let api_creds = self + .api_creds + .as_ref() .ok_or_else(|| PolyfillError::config("API credentials not configured"))?; let method = Method::GET; let endpoint = "/auth/api-keys"; - let headers = create_l2_headers::(signer, api_creds, method.as_str(), endpoint, None)?; + let headers = + create_l2_headers::(signer, api_creds, method.as_str(), endpoint, None)?; - let response = self.http_client + let response = self + .http_client .request(method, format!("{}{}", self.base_url, endpoint)) - .headers(headers.into_iter().map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap())).collect()) + .headers( + headers + .into_iter() + .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap())) + .collect(), + ) .send() .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; - let api_keys_response: crate::types::ApiKeysResponse = response.json().await + let api_keys_response: crate::types::ApiKeysResponse = response + .json() + .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))?; Ok(api_keys_response.api_keys) @@ -379,23 +457,36 @@ impl ClobClient { /// Delete the current API key pub async fn delete_api_key(&self) -> Result { - let signer = self.signer.as_ref() + let signer = self + .signer + .as_ref() .ok_or_else(|| PolyfillError::config("Signer not configured"))?; - let api_creds = self.api_creds.as_ref() + let api_creds = self + .api_creds + .as_ref() .ok_or_else(|| PolyfillError::config("API credentials not configured"))?; let method = Method::DELETE; let endpoint = "/auth/api-key"; - let headers = create_l2_headers::(signer, api_creds, method.as_str(), endpoint, None)?; + let headers = + create_l2_headers::(signer, api_creds, method.as_str(), endpoint, None)?; - let response = self.http_client + let response = self + .http_client .request(method, format!("{}{}", self.base_url, endpoint)) - .headers(headers.into_iter().map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap())).collect()) + .headers( + headers + .into_iter() + .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap())) + .collect(), + ) .send() .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; - response.text().await + response + .text() + .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None)) } @@ -406,20 +497,26 @@ impl ClobClient { endpoint: &str, headers: impl Iterator, ) -> RequestBuilder { - let req = self.http_client.request(method, format!("{}{}", &self.base_url, endpoint)); + let req = self + .http_client + .request(method, format!("{}{}", &self.base_url, endpoint)); headers.fold(req, |r, (k, v)| r.header(HeaderName::from_static(k), v)) } /// Get neg risk for a token pub async fn get_neg_risk(&self, token_id: &str) -> Result { - let response = self.http_client + let response = self + .http_client .get(format!("{}/neg-risk", self.base_url)) .query(&[("token_id", token_id)]) .send() .await?; if !response.status().is_success() { - return Err(PolyfillError::api(response.status().as_u16(), "Failed to get neg risk")); + return Err(PolyfillError::api( + response.status().as_u16(), + "Failed to get neg risk", + )); } let neg_risk_response: Value = response.json().await?; @@ -449,7 +546,7 @@ impl ClobClient { } else { Ok(t) } - } + }, } } @@ -492,13 +589,15 @@ impl ClobClient { extras: Option, options: Option<&OrderOptions>, ) -> Result { - let order_builder = self.order_builder.as_ref() + let order_builder = self + .order_builder + .as_ref() .ok_or_else(|| PolyfillError::auth("Order builder not initialized"))?; let create_order_options = self .get_filled_order_options(&order_args.token_id, options) .await?; - + let expiration = expiration.unwrap_or(0); let extras = extras.unwrap_or_default(); @@ -506,7 +605,9 @@ impl ClobClient { order_args.price, create_order_options.tick_size.expect("Should be filled"), ) { - return Err(PolyfillError::validation("Price is not in range of tick_size")); + return Err(PolyfillError::validation( + "Price is not in range of tick_size", + )); } order_builder.create_order( @@ -526,19 +627,29 @@ impl ClobClient { amount: Decimal, ) -> Result { let book = self.get_order_book(token_id).await?; - let order_builder = self.order_builder.as_ref() + let order_builder = self + .order_builder + .as_ref() .ok_or_else(|| PolyfillError::auth("Order builder not initialized"))?; // Convert OrderSummary to BookLevel let levels: Vec = match side { - Side::BUY => book.asks.into_iter().map(|s| crate::types::BookLevel { - price: s.price, - size: s.size, - }).collect(), - Side::SELL => book.bids.into_iter().map(|s| crate::types::BookLevel { - price: s.price, - size: s.size, - }).collect(), + Side::BUY => book + .asks + .into_iter() + .map(|s| crate::types::BookLevel { + price: s.price, + size: s.size, + }) + .collect(), + Side::SELL => book + .bids + .into_iter() + .map(|s| crate::types::BookLevel { + price: s.price, + size: s.size, + }) + .collect(), }; order_builder.calculate_market_price(&levels, amount) @@ -551,7 +662,9 @@ impl ClobClient { extras: Option, options: Option<&OrderOptions>, ) -> Result { - let order_builder = self.order_builder.as_ref() + let order_builder = self + .order_builder + .as_ref() .ok_or_else(|| PolyfillError::auth("Order builder not initialized"))?; let create_order_options = self @@ -567,7 +680,9 @@ impl ClobClient { price, create_order_options.tick_size.expect("Should be filled"), ) { - return Err(PolyfillError::validation("Price is not in range of tick_size")); + return Err(PolyfillError::validation( + "Price is not in range of tick_size", + )); } order_builder.create_market_order( @@ -585,9 +700,13 @@ impl ClobClient { order: SignedOrderRequest, order_type: OrderType, ) -> Result { - let signer = self.signer.as_ref() + let signer = self + .signer + .as_ref() .ok_or_else(|| PolyfillError::auth("Signer not set"))?; - let api_creds = self.api_creds.as_ref() + let api_creds = self + .api_creds + .as_ref() .ok_or_else(|| PolyfillError::auth("API credentials not set"))?; let body = PostOrder::new(order, api_creds.api_key.clone(), order_type); @@ -597,7 +716,10 @@ impl ClobClient { let response = req.json(&body).send().await?; if !response.status().is_success() { - return Err(PolyfillError::api(response.status().as_u16(), "Failed to post order")); + return Err(PolyfillError::api( + response.status().as_u16(), + "Failed to post order", + )); } Ok(response.json::().await?) @@ -611,9 +733,13 @@ impl ClobClient { /// Cancel an order pub async fn cancel(&self, order_id: &str) -> Result { - let signer = self.signer.as_ref() + let signer = self + .signer + .as_ref() .ok_or_else(|| PolyfillError::auth("Signer not set"))?; - let api_creds = self.api_creds.as_ref() + let api_creds = self + .api_creds + .as_ref() .ok_or_else(|| PolyfillError::auth("API credentials not set"))?; let body = std::collections::HashMap::from([("orderID", order_id)]); @@ -623,7 +749,10 @@ impl ClobClient { let response = req.json(&body).send().await?; if !response.status().is_success() { - return Err(PolyfillError::api(response.status().as_u16(), "Failed to cancel order")); + return Err(PolyfillError::api( + response.status().as_u16(), + "Failed to cancel order", + )); } Ok(response.json::().await?) @@ -631,9 +760,13 @@ impl ClobClient { /// Cancel multiple orders pub async fn cancel_orders(&self, order_ids: &[String]) -> Result { - let signer = self.signer.as_ref() + let signer = self + .signer + .as_ref() .ok_or_else(|| PolyfillError::auth("Signer not set"))?; - let api_creds = self.api_creds.as_ref() + let api_creds = self + .api_creds + .as_ref() .ok_or_else(|| PolyfillError::auth("API credentials not set"))?; let headers = create_l2_headers(signer, api_creds, "DELETE", "/orders", Some(order_ids))?; @@ -641,7 +774,10 @@ impl ClobClient { let response = req.json(order_ids).send().await?; if !response.status().is_success() { - return Err(PolyfillError::api(response.status().as_u16(), "Failed to cancel orders")); + return Err(PolyfillError::api( + response.status().as_u16(), + "Failed to cancel orders", + )); } Ok(response.json::().await?) @@ -649,39 +785,56 @@ impl ClobClient { /// Cancel all orders pub async fn cancel_all(&self) -> Result { - let signer = self.signer.as_ref() + let signer = self + .signer + .as_ref() .ok_or_else(|| PolyfillError::auth("Signer not set"))?; - let api_creds = self.api_creds.as_ref() + let api_creds = self + .api_creds + .as_ref() .ok_or_else(|| PolyfillError::auth("API credentials not set"))?; let headers = create_l2_headers::(signer, api_creds, "DELETE", "/cancel-all", None)?; - let req = self.create_request_with_headers(Method::DELETE, "/cancel-all", headers.into_iter()); + let req = + self.create_request_with_headers(Method::DELETE, "/cancel-all", headers.into_iter()); let response = req.send().await?; if !response.status().is_success() { - return Err(PolyfillError::api(response.status().as_u16(), "Failed to cancel all orders")); + return Err(PolyfillError::api( + response.status().as_u16(), + "Failed to cancel all orders", + )); } Ok(response.json::().await?) } /// Get open orders with optional filtering - /// + /// /// This retrieves all open orders for the authenticated user. You can filter by: /// - Order ID (exact match) /// - Asset/Token ID (all orders for a specific token) /// - Market ID (all orders for a specific market) - /// + /// /// The response includes order status, fill information, and timestamps. - pub async fn get_orders(&self, params: Option<&crate::types::OpenOrderParams>, next_cursor: Option<&str>) -> Result> { - let signer = self.signer.as_ref() + pub async fn get_orders( + &self, + params: Option<&crate::types::OpenOrderParams>, + next_cursor: Option<&str>, + ) -> Result> { + let signer = self + .signer + .as_ref() .ok_or_else(|| PolyfillError::auth("Signer not set"))?; - let api_creds = self.api_creds.as_ref() + let api_creds = self + .api_creds + .as_ref() .ok_or_else(|| PolyfillError::auth("API credentials not set"))?; let method = Method::GET; let endpoint = "/data/orders"; - let headers = create_l2_headers::(signer, api_creds, method.as_str(), endpoint, None)?; + let headers = + create_l2_headers::(signer, api_creds, method.as_str(), endpoint, None)?; let query_params = match params { None => Vec::new(), @@ -690,9 +843,11 @@ impl ClobClient { let mut next_cursor = next_cursor.unwrap_or("MA==").to_string(); // INITIAL_CURSOR let mut output = Vec::new(); - - while next_cursor != "LTE=" { // END_CURSOR - let req = self.http_client + + while next_cursor != "LTE=" { + // END_CURSOR + let req = self + .http_client .request(method.clone(), format!("{}{}", self.base_url, endpoint)) .query(&query_params) .query(&[("next_cursor", &next_cursor)]); @@ -702,46 +857,67 @@ impl ClobClient { .into_iter() .fold(req, |r, (k, v)| r.header(HeaderName::from_static(k), v)); - let resp = r.send().await + let resp = r + .send() + .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))? - .json::().await - .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))?; + .json::() + .await + .map_err(|e| { + PolyfillError::parse(format!("Failed to parse response: {}", e), None) + })?; let new_cursor = resp["next_cursor"] .as_str() - .ok_or_else(|| PolyfillError::parse("Failed to parse next cursor".to_string(), None))? + .ok_or_else(|| { + PolyfillError::parse("Failed to parse next cursor".to_string(), None) + })? .to_owned(); next_cursor = new_cursor; let results = resp["data"].clone(); - let orders = serde_json::from_value::>(results) - .map_err(|e| PolyfillError::parse(format!("Failed to parse data from order response: {}", e), None))?; + let orders = + serde_json::from_value::>(results).map_err(|e| { + PolyfillError::parse( + format!("Failed to parse data from order response: {}", e), + None, + ) + })?; output.extend(orders); } - + Ok(output) } /// Get trade history with optional filtering - /// + /// /// This retrieves historical trades for the authenticated user. You can filter by: /// - Trade ID (exact match) /// - Maker address (trades where you were the maker) /// - Market ID (trades in a specific market) /// - Asset/Token ID (trades for a specific token) /// - Time range (before/after timestamps) - /// + /// /// Trades are returned in reverse chronological order (newest first). - pub async fn get_trades(&self, trade_params: Option<&crate::types::TradeParams>, next_cursor: Option<&str>) -> Result> { - let signer = self.signer.as_ref() + pub async fn get_trades( + &self, + trade_params: Option<&crate::types::TradeParams>, + next_cursor: Option<&str>, + ) -> Result> { + let signer = self + .signer + .as_ref() .ok_or_else(|| PolyfillError::auth("Signer not set"))?; - let api_creds = self.api_creds.as_ref() + let api_creds = self + .api_creds + .as_ref() .ok_or_else(|| PolyfillError::auth("API credentials not set"))?; let method = Method::GET; let endpoint = "/data/trades"; - let headers = create_l2_headers::(signer, api_creds, method.as_str(), endpoint, None)?; + let headers = + create_l2_headers::(signer, api_creds, method.as_str(), endpoint, None)?; let query_params = match trade_params { None => Vec::new(), @@ -750,9 +926,11 @@ impl ClobClient { let mut next_cursor = next_cursor.unwrap_or("MA==").to_string(); // INITIAL_CURSOR let mut output = Vec::new(); - - while next_cursor != "LTE=" { // END_CURSOR - let req = self.http_client + + while next_cursor != "LTE=" { + // END_CURSOR + let req = self + .http_client .request(method.clone(), format!("{}{}", self.base_url, endpoint)) .query(&query_params) .query(&[("next_cursor", &next_cursor)]); @@ -762,14 +940,21 @@ impl ClobClient { .into_iter() .fold(req, |r, (k, v)| r.header(HeaderName::from_static(k), v)); - let resp = r.send().await + let resp = r + .send() + .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))? - .json::().await - .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))?; + .json::() + .await + .map_err(|e| { + PolyfillError::parse(format!("Failed to parse response: {}", e), None) + })?; let new_cursor = resp["next_cursor"] .as_str() - .ok_or_else(|| PolyfillError::parse("Failed to parse next cursor".to_string(), None))? + .ok_or_else(|| { + PolyfillError::parse("Failed to parse next cursor".to_string(), None) + })? .to_owned(); next_cursor = new_cursor; @@ -777,21 +962,28 @@ impl ClobClient { let results = resp["data"].clone(); output.push(results); } - + Ok(output) } /// Get balance and allowance information for all assets - /// + /// /// This returns the current balance and allowance for each asset in your account. /// Balance is how much you own, allowance is how much the exchange can spend on your behalf. - /// + /// /// You need both balance and allowance to place orders - the exchange needs permission /// to move your tokens when orders are filled. - pub async fn get_balance_allowance(&self, params: Option) -> Result { - let signer = self.signer.as_ref() + pub async fn get_balance_allowance( + &self, + params: Option, + ) -> Result { + let signer = self + .signer + .as_ref() .ok_or_else(|| PolyfillError::auth("Signer not set"))?; - let api_creds = self.api_creds.as_ref() + let api_creds = self + .api_creds + .as_ref() .ok_or_else(|| PolyfillError::auth("API credentials not set"))?; let mut params = params.unwrap_or_default(); @@ -808,65 +1000,91 @@ impl ClobClient { let method = Method::GET; let endpoint = "/balance-allowance"; - let headers = create_l2_headers::(signer, api_creds, method.as_str(), endpoint, None)?; + let headers = + create_l2_headers::(signer, api_creds, method.as_str(), endpoint, None)?; - let response = self.http_client + let response = self + .http_client .request(method, format!("{}{}", self.base_url, endpoint)) - .headers(headers.into_iter().map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap())).collect()) + .headers( + headers + .into_iter() + .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap())) + .collect(), + ) .query(&query_params) .send() .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; - response.json::().await + response + .json::() + .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None)) } /// Set up notifications for order fills and other events - /// + /// /// This configures push notifications so you get alerted when: /// - Your orders get filled /// - Your orders get cancelled /// - Market conditions change significantly - /// + /// /// The signature proves you own the account and want to receive notifications. pub async fn get_notifications(&self) -> Result { - let signer = self.signer.as_ref() + let signer = self + .signer + .as_ref() .ok_or_else(|| PolyfillError::auth("Signer not set"))?; - let api_creds = self.api_creds.as_ref() + let api_creds = self + .api_creds + .as_ref() .ok_or_else(|| PolyfillError::auth("API credentials not set"))?; let method = Method::GET; let endpoint = "/notifications"; - let headers = create_l2_headers::(signer, api_creds, method.as_str(), endpoint, None)?; + let headers = + create_l2_headers::(signer, api_creds, method.as_str(), endpoint, None)?; - let response = self.http_client + let response = self + .http_client .request(method, format!("{}{}", self.base_url, endpoint)) - .headers(headers.into_iter().map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap())).collect()) + .headers( + headers + .into_iter() + .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap())) + .collect(), + ) .query(&[( "signature_type", &self .order_builder .as_ref() .expect("OrderBuilder not set") - .get_sig_type().to_string(), + .get_sig_type() + .to_string(), )]) .send() .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; - response.json::().await + response + .json::() + .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None)) } /// Get midpoints for multiple tokens in a single request - /// + /// /// This is much more efficient than calling get_midpoint() multiple times. /// Instead of N round trips, you make just 1 request and get all the midpoints back. - /// + /// /// Midpoints are returned as a HashMap where the key is the token_id and the value /// is the midpoint price (or None if there's no valid midpoint). - pub async fn get_midpoints(&self, token_ids: &[String]) -> Result> { + pub async fn get_midpoints( + &self, + token_ids: &[String], + ) -> Result> { let request_data: Vec> = token_ids .iter() .map(|id| { @@ -875,15 +1093,19 @@ impl ClobClient { map }) .collect(); - - let response = self.http_client + + let response = self + .http_client .post(format!("{}/midpoints", self.base_url)) .json(&request_data) .send() .await?; if !response.status().is_success() { - return Err(PolyfillError::api(response.status().as_u16(), "Failed to get batch midpoints")); + return Err(PolyfillError::api( + response.status().as_u16(), + "Failed to get batch midpoints", + )); } let midpoints: std::collections::HashMap = response.json().await?; @@ -891,13 +1113,16 @@ impl ClobClient { } /// Get bid/ask/mid prices for multiple tokens in a single request - /// + /// /// This gives you the full price picture for multiple tokens at once. /// Much more efficient than individual calls, especially when you're tracking /// a portfolio or comparing multiple markets. - /// + /// /// Returns bid (best buy price), ask (best sell price), and mid (average) for each token. - pub async fn get_prices(&self, book_params: &[crate::types::BookParams]) -> Result>> { + pub async fn get_prices( + &self, + book_params: &[crate::types::BookParams], + ) -> Result>> { let request_data: Vec> = book_params .iter() .map(|params| { @@ -907,18 +1132,23 @@ impl ClobClient { map }) .collect(); - - let response = self.http_client + + let response = self + .http_client .post(format!("{}/prices", self.base_url)) .json(&request_data) .send() .await?; if !response.status().is_success() { - return Err(PolyfillError::api(response.status().as_u16(), "Failed to get batch prices")); + return Err(PolyfillError::api( + response.status().as_u16(), + "Failed to get batch prices", + )); } - let prices: std::collections::HashMap> = response.json().await?; + let prices: std::collections::HashMap> = + response.json().await?; Ok(prices) } @@ -933,49 +1163,68 @@ impl ClobClient { }) .collect(); - let response = self.http_client + let response = self + .http_client .post(format!("{}/books", self.base_url)) .json(&request_data) .send() .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; - response.json::>().await + response + .json::>() + .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None)) } /// Get single order by ID pub async fn get_order(&self, order_id: &str) -> Result { - let signer = self.signer.as_ref() + let signer = self + .signer + .as_ref() .ok_or_else(|| PolyfillError::config("Signer not configured"))?; - let api_creds = self.api_creds.as_ref() + let api_creds = self + .api_creds + .as_ref() .ok_or_else(|| PolyfillError::config("API credentials not configured"))?; let method = Method::GET; let endpoint = &format!("/data/order/{}", order_id); - let headers = create_l2_headers::(signer, api_creds, method.as_str(), endpoint, None)?; + let headers = + create_l2_headers::(signer, api_creds, method.as_str(), endpoint, None)?; - let response = self.http_client + let response = self + .http_client .request(method, format!("{}{}", self.base_url, endpoint)) - .headers(headers.into_iter().map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap())).collect()) + .headers( + headers + .into_iter() + .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap())) + .collect(), + ) .send() .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; - response.json::().await + response + .json::() + .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None)) } /// Get last trade price for a token pub async fn get_last_trade_price(&self, token_id: &str) -> Result { - let response = self.http_client + let response = self + .http_client .get(format!("{}/last-trade-price", self.base_url)) .query(&[("token_id", token_id)]) .send() .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; - response.json::().await + response + .json::() + .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None)) } @@ -990,22 +1239,33 @@ impl ClobClient { }) .collect(); - let response = self.http_client + let response = self + .http_client .post(format!("{}/last-trades-prices", self.base_url)) .json(&request_data) .send() .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; - response.json::().await + response + .json::() + .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None)) } /// Cancel market orders with optional filters - pub async fn cancel_market_orders(&self, market: Option<&str>, asset_id: Option<&str>) -> Result { - let signer = self.signer.as_ref() + pub async fn cancel_market_orders( + &self, + market: Option<&str>, + asset_id: Option<&str>, + ) -> Result { + let signer = self + .signer + .as_ref() .ok_or_else(|| PolyfillError::config("Signer not configured"))?; - let api_creds = self.api_creds.as_ref() + let api_creds = self + .api_creds + .as_ref() .ok_or_else(|| PolyfillError::config("API credentials not configured"))?; let method = Method::DELETE; @@ -1017,46 +1277,74 @@ impl ClobClient { let headers = create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(&body))?; - let response = self.http_client + let response = self + .http_client .request(method, format!("{}{}", self.base_url, endpoint)) - .headers(headers.into_iter().map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap())).collect()) + .headers( + headers + .into_iter() + .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap())) + .collect(), + ) .json(&body) .send() .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; - response.json::().await + response + .json::() + .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None)) } /// Drop (delete) notifications by IDs pub async fn drop_notifications(&self, ids: &[String]) -> Result { - let signer = self.signer.as_ref() + let signer = self + .signer + .as_ref() .ok_or_else(|| PolyfillError::config("Signer not configured"))?; - let api_creds = self.api_creds.as_ref() + let api_creds = self + .api_creds + .as_ref() .ok_or_else(|| PolyfillError::config("API credentials not configured"))?; let method = Method::DELETE; let endpoint = "/notifications"; - let headers = create_l2_headers::(signer, api_creds, method.as_str(), endpoint, None)?; + let headers = + create_l2_headers::(signer, api_creds, method.as_str(), endpoint, None)?; - let response = self.http_client + let response = self + .http_client .request(method, format!("{}{}", self.base_url, endpoint)) - .headers(headers.into_iter().map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap())).collect()) + .headers( + headers + .into_iter() + .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap())) + .collect(), + ) .query(&[("ids", ids.join(","))]) .send() .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; - response.json::().await + response + .json::() + .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None)) } /// Update balance allowance - pub async fn update_balance_allowance(&self, params: Option) -> Result { - let signer = self.signer.as_ref() + pub async fn update_balance_allowance( + &self, + params: Option, + ) -> Result { + let signer = self + .signer + .as_ref() .ok_or_else(|| PolyfillError::config("Signer not configured"))?; - let api_creds = self.api_creds.as_ref() + let api_creds = self + .api_creds + .as_ref() .ok_or_else(|| PolyfillError::config("API credentials not configured"))?; let mut params = params.unwrap_or_default(); @@ -1073,158 +1361,234 @@ impl ClobClient { let method = Method::GET; let endpoint = "/balance-allowance/update"; - let headers = create_l2_headers::(signer, api_creds, method.as_str(), endpoint, None)?; + let headers = + create_l2_headers::(signer, api_creds, method.as_str(), endpoint, None)?; - let response = self.http_client + let response = self + .http_client .request(method, format!("{}{}", self.base_url, endpoint)) - .headers(headers.into_iter().map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap())).collect()) + .headers( + headers + .into_iter() + .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap())) + .collect(), + ) .query(&query_params) .send() .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; - response.json::().await + response + .json::() + .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None)) } /// Check if an order is scoring pub async fn is_order_scoring(&self, order_id: &str) -> Result { - let signer = self.signer.as_ref() + let signer = self + .signer + .as_ref() .ok_or_else(|| PolyfillError::config("Signer not configured"))?; - let api_creds = self.api_creds.as_ref() + let api_creds = self + .api_creds + .as_ref() .ok_or_else(|| PolyfillError::config("API credentials not configured"))?; let method = Method::GET; let endpoint = "/order-scoring"; - let headers = create_l2_headers::(signer, api_creds, method.as_str(), endpoint, None)?; + let headers = + create_l2_headers::(signer, api_creds, method.as_str(), endpoint, None)?; - let response = self.http_client + let response = self + .http_client .request(method, format!("{}{}", self.base_url, endpoint)) - .headers(headers.into_iter().map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap())).collect()) + .headers( + headers + .into_iter() + .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap())) + .collect(), + ) .query(&[("order_id", order_id)]) .send() .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; - let result: Value = response.json().await + let result: Value = response + .json() + .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))?; Ok(result["scoring"].as_bool().unwrap_or(false)) } /// Check if multiple orders are scoring - pub async fn are_orders_scoring(&self, order_ids: &[&str]) -> Result> { - let signer = self.signer.as_ref() + pub async fn are_orders_scoring( + &self, + order_ids: &[&str], + ) -> Result> { + let signer = self + .signer + .as_ref() .ok_or_else(|| PolyfillError::config("Signer not configured"))?; - let api_creds = self.api_creds.as_ref() + let api_creds = self + .api_creds + .as_ref() .ok_or_else(|| PolyfillError::config("API credentials not configured"))?; let method = Method::POST; let endpoint = "/orders-scoring"; - let headers = create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(order_ids))?; + let headers = create_l2_headers( + signer, + api_creds, + method.as_str(), + endpoint, + Some(order_ids), + )?; - let response = self.http_client + let response = self + .http_client .request(method, format!("{}{}", self.base_url, endpoint)) - .headers(headers.into_iter().map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap())).collect()) + .headers( + headers + .into_iter() + .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap())) + .collect(), + ) .json(order_ids) .send() .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; - response.json::>().await + response + .json::>() + .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None)) } /// Get sampling markets with pagination - pub async fn get_sampling_markets(&self, next_cursor: Option<&str>) -> Result { + pub async fn get_sampling_markets( + &self, + next_cursor: Option<&str>, + ) -> Result { let next_cursor = next_cursor.unwrap_or("MA=="); // INITIAL_CURSOR - let response = self.http_client + let response = self + .http_client .get(format!("{}/sampling-markets", self.base_url)) .query(&[("next_cursor", next_cursor)]) .send() .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; - response.json::().await + response + .json::() + .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None)) } /// Get sampling simplified markets with pagination - pub async fn get_sampling_simplified_markets(&self, next_cursor: Option<&str>) -> Result { + pub async fn get_sampling_simplified_markets( + &self, + next_cursor: Option<&str>, + ) -> Result { let next_cursor = next_cursor.unwrap_or("MA=="); // INITIAL_CURSOR - let response = self.http_client + let response = self + .http_client .get(format!("{}/sampling-simplified-markets", self.base_url)) .query(&[("next_cursor", next_cursor)]) .send() .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; - response.json::().await + response + .json::() + .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None)) } /// Get markets with pagination - pub async fn get_markets(&self, next_cursor: Option<&str>) -> Result { + pub async fn get_markets( + &self, + next_cursor: Option<&str>, + ) -> Result { let next_cursor = next_cursor.unwrap_or("MA=="); // INITIAL_CURSOR - let response = self.http_client + let response = self + .http_client .get(format!("{}/markets", self.base_url)) .query(&[("next_cursor", next_cursor)]) .send() .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; - response.json::().await + response + .json::() + .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None)) } /// Get simplified markets with pagination - pub async fn get_simplified_markets(&self, next_cursor: Option<&str>) -> Result { + pub async fn get_simplified_markets( + &self, + next_cursor: Option<&str>, + ) -> Result { let next_cursor = next_cursor.unwrap_or("MA=="); // INITIAL_CURSOR - let response = self.http_client + let response = self + .http_client .get(format!("{}/simplified-markets", self.base_url)) .query(&[("next_cursor", next_cursor)]) .send() .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; - response.json::().await + response + .json::() + .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None)) } /// Get single market by condition ID pub async fn get_market(&self, condition_id: &str) -> Result { - let response = self.http_client + let response = self + .http_client .get(format!("{}/markets/{}", self.base_url, condition_id)) .send() .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; - response.json::().await + response + .json::() + .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None)) } /// Get market trades events pub async fn get_market_trades_events(&self, condition_id: &str) -> Result { - let response = self.http_client - .get(format!("{}/live-activity/events/{}", self.base_url, condition_id)) + let response = self + .http_client + .get(format!( + "{}/live-activity/events/{}", + self.base_url, condition_id + )) .send() .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; - response.json::().await + response + .json::() + .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None)) } } // Re-export types from the canonical location in types.rs pub use crate::types::{ - ExtraOrderArgs, MarketOrderArgs, OrderBookSummary, OrderSummary, - MidpointResponse, SpreadResponse, PriceResponse, TickSizeResponse, - NegRiskResponse, MarketsResponse, Market, Token, Rewards, + ExtraOrderArgs, Market, MarketOrderArgs, MarketsResponse, MidpointResponse, NegRiskResponse, + OrderBookSummary, OrderSummary, PriceResponse, Rewards, SpreadResponse, TickSizeResponse, + Token, }; // Compatibility types that need to stay in client.rs @@ -1282,14 +1646,14 @@ mod tests { secret: "test_secret".to_string(), passphrase: "test_passphrase".to_string(), }; - + let client = ClobClient::with_l2_headers( "https://test.example.com", "0x1234567890123456789012345678901234567890123456789012345678901234", 137, api_creds.clone(), ); - + assert_eq!(client.base_url, "https://test.example.com"); assert!(client.signer.is_some()); assert!(client.api_creds.is_some()); @@ -1366,7 +1730,7 @@ mod tests { let client = create_test_client(&server.url()); let result = client.get_sampling_markets(None).await; - + mock.assert_async().await; assert!(result.is_ok()); let markets = result.unwrap(); @@ -1386,9 +1750,10 @@ mod tests { let mock = server .mock("GET", "/sampling-markets") - .match_query(Matcher::AllOf(vec![ - Matcher::UrlEncoded("next_cursor".into(), "test_cursor".into()), - ])) + .match_query(Matcher::AllOf(vec![Matcher::UrlEncoded( + "next_cursor".into(), + "test_cursor".into(), + )])) .with_status(200) .with_header("content-type", "application/json") .with_body(mock_response) @@ -1397,7 +1762,7 @@ mod tests { let client = create_test_client(&server.url()); let result = client.get_sampling_markets(Some("test_cursor")).await; - + mock.assert_async().await; assert!(result.is_ok()); let markets = result.unwrap(); @@ -1431,7 +1796,7 @@ mod tests { let client = create_test_client(&server.url()); let result = client.get_order_book("0x123").await; - + mock.assert_async().await; assert!(result.is_ok()); let book = result.unwrap(); @@ -1458,7 +1823,7 @@ mod tests { let client = create_test_client(&server.url()); let result = client.get_midpoint("0x123").await; - + mock.assert_async().await; assert!(result.is_ok()); let response = result.unwrap(); @@ -1483,7 +1848,7 @@ mod tests { let client = create_test_client(&server.url()); let result = client.get_spread("0x123").await; - + mock.assert_async().await; assert!(result.is_ok()); let response = result.unwrap(); @@ -1511,7 +1876,7 @@ mod tests { let client = create_test_client(&server.url()); let result = client.get_price("0x123", Side::BUY).await; - + mock.assert_async().await; assert!(result.is_ok()); let response = result.unwrap(); @@ -1536,7 +1901,7 @@ mod tests { let client = create_test_client(&server.url()); let result = client.get_tick_size("0x123").await; - + mock.assert_async().await; assert!(result.is_ok()); let tick_size = result.unwrap(); @@ -1561,7 +1926,7 @@ mod tests { let client = create_test_client(&server.url()); let result = client.get_neg_risk("0x123").await; - + mock.assert_async().await; assert!(result.is_ok()); let neg_risk = result.unwrap(); @@ -1571,10 +1936,13 @@ mod tests { #[tokio::test] async fn test_api_error_handling() { let mut server = Server::new_async().await; - + let mock = server .mock("GET", "/book") - .match_query(Matcher::UrlEncoded("token_id".into(), "invalid_token".into())) + .match_query(Matcher::UrlEncoded( + "token_id".into(), + "invalid_token".into(), + )) .with_status(404) .with_header("content-type", "application/json") .with_body(r#"{"error": "Market not found"}"#) @@ -1583,13 +1951,16 @@ mod tests { let client = create_test_client(&server.url()); let result = client.get_order_book("invalid_token").await; - + mock.assert_async().await; assert!(result.is_err()); - + let error = result.unwrap_err(); // The error should be either Network or Api error - assert!(matches!(error, PolyfillError::Network { .. }) || matches!(error, PolyfillError::Api { .. })); + assert!( + matches!(error, PolyfillError::Network { .. }) + || matches!(error, PolyfillError::Api { .. }) + ); } #[tokio::test] @@ -1597,7 +1968,7 @@ mod tests { // Test with invalid URL to simulate network error let client = create_test_client("http://invalid-host-that-does-not-exist.com"); let result = client.get_order_book("0x123").await; - + assert!(result.is_err()); let error = result.unwrap_err(); assert!(matches!(error, PolyfillError::Network { .. })); @@ -1607,7 +1978,7 @@ mod tests { fn test_client_url_validation() { let client = create_test_client("https://test.example.com"); assert_eq!(client.base_url, "https://test.example.com"); - + let client2 = create_test_client("http://localhost:8080"); assert_eq!(client2.base_url, "http://localhost:8080"); } @@ -1632,23 +2003,29 @@ mod tests { let client = create_test_client(&server.url()); let token_ids = vec!["0x123".to_string(), "0x456".to_string()]; let result = client.get_midpoints(&token_ids).await; - + mock.assert_async().await; assert!(result.is_ok()); let midpoints = result.unwrap(); assert_eq!(midpoints.len(), 2); - assert_eq!(midpoints.get("0x123").unwrap(), &Decimal::from_str("0.755").unwrap()); - assert_eq!(midpoints.get("0x456").unwrap(), &Decimal::from_str("0.623").unwrap()); + assert_eq!( + midpoints.get("0x123").unwrap(), + &Decimal::from_str("0.755").unwrap() + ); + assert_eq!( + midpoints.get("0x456").unwrap(), + &Decimal::from_str("0.623").unwrap() + ); } #[test] fn test_client_configuration() { let client = create_test_client("https://test.example.com"); - + // Test initial state assert!(client.signer.is_none()); assert!(client.api_creds.is_none()); - + // Test with auth let auth_client = create_test_client_with_auth("https://test.example.com"); assert!(auth_client.signer.is_some()); @@ -1670,7 +2047,7 @@ mod tests { let client = create_test_client(&server.url()); let result = client.get_ok().await; - + mock.assert_async().await; assert!(result); } @@ -1709,7 +2086,7 @@ mod tests { }, ]; let result = client.get_prices(&book_params).await; - + mock.assert_async().await; assert!(result.is_ok()); let prices = result.unwrap(); @@ -1732,7 +2109,7 @@ mod tests { let client = create_test_client(&server.url()); let result = client.get_server_time().await; - + mock.assert_async().await; assert!(result.is_ok()); let timestamp = result.unwrap(); @@ -1759,7 +2136,7 @@ mod tests { let client = create_test_client_with_auth(&server.url()); let result = client.create_or_derive_api_key(None).await; - + create_mock.assert_async().await; assert!(result.is_ok()); let api_creds = result.unwrap(); @@ -1791,7 +2168,7 @@ mod tests { let client = create_test_client(&server.url()); let token_ids = vec!["0x123".to_string()]; let result = client.get_order_books(&token_ids).await; - + mock.assert_async().await; if let Err(e) = &result { println!("Error: {:?}", e); @@ -1810,12 +2187,12 @@ mod tests { Decimal::from_str("100.0").unwrap(), Side::BUY, ); - + assert_eq!(order_args.token_id, "0x123"); assert_eq!(order_args.price, Decimal::from_str("0.75").unwrap()); assert_eq!(order_args.size, Decimal::from_str("100.0").unwrap()); assert_eq!(order_args.side, Side::BUY); - + // Test default let default_args = ClientOrderArgs::default(); assert_eq!(default_args.token_id, ""); @@ -1823,4 +2200,4 @@ mod tests { assert_eq!(default_args.size, Decimal::ZERO); assert_eq!(default_args.side, Side::BUY); } -} \ No newline at end of file +} diff --git a/src/decode.rs b/src/decode.rs index a9272df..b9e84e5 100644 --- a/src/decode.rs +++ b/src/decode.rs @@ -31,15 +31,15 @@ pub mod deserializers { T::deserialize(serde_json::Value::Number(serde_json::Number::from(v))) .map_err(|_| serde::de::Error::custom("Failed to deserialize number")) } else if let Some(v) = n.as_f64() { - T::deserialize(serde_json::Value::Number(serde_json::Number::from_f64(v).unwrap())) - .map_err(|_| serde::de::Error::custom("Failed to deserialize number")) + T::deserialize(serde_json::Value::Number( + serde_json::Number::from_f64(v).unwrap(), + )) + .map_err(|_| serde::de::Error::custom("Failed to deserialize number")) } else { Err(serde::de::Error::custom("Invalid number format")) } - } - serde_json::Value::String(s) => { - s.parse::().map_err(serde::de::Error::custom) - } + }, + serde_json::Value::String(s) => s.parse::().map_err(serde::de::Error::custom), _ => Err(serde::de::Error::custom("Expected number or string")), } } @@ -62,28 +62,30 @@ pub mod deserializers { .map(Some) .map_err(|_| serde::de::Error::custom("Failed to deserialize number")) } else if let Some(v) = n.as_f64() { - T::deserialize(serde_json::Value::Number(serde_json::Number::from_f64(v).unwrap())) - .map(Some) - .map_err(|_| serde::de::Error::custom("Failed to deserialize number")) + T::deserialize(serde_json::Value::Number( + serde_json::Number::from_f64(v).unwrap(), + )) + .map(Some) + .map_err(|_| serde::de::Error::custom("Failed to deserialize number")) } else { Err(serde::de::Error::custom("Invalid number format")) } - } + }, serde_json::Value::String(s) => { if s.is_empty() { Ok(None) } else { - s.parse::() - .map(Some) - .map_err(serde::de::Error::custom) + s.parse::().map(Some).map_err(serde::de::Error::custom) } - } + }, _ => Err(serde::de::Error::custom("Expected number, string, or null")), } } /// Deserialize DateTime from Unix timestamp - pub fn datetime_from_timestamp<'de, D>(deserializer: D) -> std::result::Result, D::Error> + pub fn datetime_from_timestamp<'de, D>( + deserializer: D, + ) -> std::result::Result, D::Error> where D: Deserializer<'de>, { @@ -236,18 +238,25 @@ impl Decoder for RawOrderResponse { "FILLED" => OrderStatus::Filled, "PARTIAL" => OrderStatus::Partial, "EXPIRED" => OrderStatus::Expired, - _ => return Err(PolyfillError::parse( - format!("Unknown order status: {}", self.status), - None, - )), + _ => { + return Err(PolyfillError::parse( + format!("Unknown order status: {}", self.status), + None, + )) + }, }; - let created_at = chrono::DateTime::from_timestamp(self.created_at as i64, 0) - .ok_or_else(|| PolyfillError::parse("Invalid created_at timestamp".to_string(), None))?; + let created_at = + chrono::DateTime::from_timestamp(self.created_at as i64, 0).ok_or_else(|| { + PolyfillError::parse("Invalid created_at timestamp".to_string(), None) + })?; let expiration = if self.expiration > 0 { - Some(chrono::DateTime::from_timestamp(self.expiration as i64, 0) - .ok_or_else(|| PolyfillError::parse("Invalid expiration timestamp".to_string(), None))?) + Some( + chrono::DateTime::from_timestamp(self.expiration as i64, 0).ok_or_else(|| { + PolyfillError::parse("Invalid expiration timestamp".to_string(), None) + })?, + ) } else { None }; @@ -344,7 +353,7 @@ impl Decoder for RawMarketResponse { /// WebSocket message parsing pub fn parse_stream_message(raw: &str) -> Result { let value: Value = serde_json::from_str(raw)?; - + let msg_type = value["type"] .as_str() .ok_or_else(|| PolyfillError::parse("Missing message type".to_string(), None))?; @@ -354,19 +363,19 @@ pub fn parse_stream_message(raw: &str) -> Result { let data = value["data"].clone(); let delta: OrderDelta = serde_json::from_value(data)?; Ok(StreamMessage::BookUpdate { data: delta }) - } + }, "trade" => { let data = value["data"].clone(); let raw_trade: RawTradeResponse = serde_json::from_value(data)?; let fill = raw_trade.decode()?; Ok(StreamMessage::Trade { data: fill }) - } + }, "order_update" => { let data = value["data"].clone(); let raw_order: RawOrderResponse = serde_json::from_value(data)?; let order = raw_order.decode()?; Ok(StreamMessage::OrderUpdate { data: order }) - } + }, "heartbeat" => { let timestamp = value["timestamp"] .as_str() @@ -374,7 +383,7 @@ pub fn parse_stream_message(raw: &str) -> Result { .map(|dt| dt.with_timezone(&Utc)) .unwrap_or_else(Utc::now); Ok(StreamMessage::Heartbeat { timestamp }) - } + }, _ => Err(PolyfillError::parse( format!("Unknown message type: {}", msg_type), None, @@ -440,8 +449,8 @@ impl BatchDecoder { if depth == 0 { return Some(start + i + 1); } - } - _ => {} + }, + _ => {}, } } @@ -512,8 +521,8 @@ mod tests { fn test_batch_decoder() { let mut decoder = BatchDecoder::new(); let data = r#"{"test":1}{"test":2}"#.as_bytes(); - + let results: Vec = decoder.parse_json_stream(data).unwrap(); assert_eq!(results.len(), 2); } -} \ No newline at end of file +} diff --git a/src/errors.rs b/src/errors.rs index 2745e2c..64440b2 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -4,15 +4,15 @@ //! for clear error handling in trading environments where fast error recovery //! is critical. -use thiserror::Error; use std::time::Duration; +use thiserror::Error; /// Main error type for the Polymarket client #[derive(Error, Debug)] pub enum PolyfillError { /// Network-related errors (retryable) #[error("Network error: {message}")] - Network { + Network { message: String, #[source] source: Option>, @@ -20,7 +20,7 @@ pub enum PolyfillError { /// API errors from Polymarket #[error("API error ({status}): {message}")] - Api { + Api { status: u16, message: String, error_code: Option, @@ -28,34 +28,32 @@ pub enum PolyfillError { /// Authentication/authorization errors #[error("Auth error: {message}")] - Auth { + Auth { message: String, kind: AuthErrorKind, }, /// Order-related errors #[error("Order error: {message}")] - Order { + Order { message: String, kind: OrderErrorKind, }, /// Market data errors #[error("Market data error: {message}")] - MarketData { + MarketData { message: String, kind: MarketDataErrorKind, }, /// Configuration errors #[error("Config error: {message}")] - Config { - message: String, - }, + Config { message: String }, /// Parsing/serialization errors #[error("Parse error: {message}")] - Parse { + Parse { message: String, #[source] source: Option>, @@ -63,35 +61,35 @@ pub enum PolyfillError { /// Timeout errors #[error("Timeout error: operation timed out after {duration:?}")] - Timeout { + Timeout { duration: Duration, operation: String, }, /// Rate limiting errors #[error("Rate limit exceeded: {message}")] - RateLimit { + RateLimit { message: String, retry_after: Option, }, /// WebSocket/streaming errors #[error("Stream error: {message}")] - Stream { + Stream { message: String, kind: StreamErrorKind, }, /// Validation errors #[error("Validation error: {message}")] - Validation { + Validation { message: String, field: Option, }, /// Internal errors (bugs) #[error("Internal error: {message}")] - Internal { + Internal { message: String, #[source] source: Option>, @@ -155,7 +153,10 @@ impl PolyfillError { PolyfillError::Timeout { .. } => true, PolyfillError::RateLimit { .. } => true, PolyfillError::Stream { kind, .. } => { - matches!(kind, StreamErrorKind::ConnectionLost | StreamErrorKind::Reconnecting) + matches!( + kind, + StreamErrorKind::ConnectionLost | StreamErrorKind::Reconnecting + ) }, _ => false, } @@ -267,7 +268,10 @@ impl PolyfillError { } } - pub fn parse(message: impl Into, source: Option>) -> Self { + pub fn parse( + message: impl Into, + source: Option>, + ) -> Self { Self::Parse { message: message.into(), source, @@ -355,7 +359,7 @@ impl From for PolyfillError { impl From for PolyfillError { fn from(err: tokio_tungstenite::tungstenite::Error) -> Self { use tokio_tungstenite::tungstenite::Error as WsError; - + let kind = match &err { WsError::ConnectionClosed | WsError::AlreadyClosed => StreamErrorKind::ConnectionLost, WsError::Io(_) => StreamErrorKind::ConnectionFailed, @@ -371,81 +375,67 @@ impl From for PolyfillError { impl Clone for PolyfillError { fn clone(&self) -> Self { match self { - PolyfillError::Network { message, source: _ } => { - PolyfillError::Network { - message: message.clone(), - source: None - } - } - PolyfillError::Api { status, message, error_code } => { - PolyfillError::Api { - status: *status, - message: message.clone(), - error_code: error_code.clone() - } - } - PolyfillError::Auth { message, kind } => { - PolyfillError::Auth { - message: message.clone(), - kind: kind.clone() - } - } - PolyfillError::Order { message, kind } => { - PolyfillError::Order { - message: message.clone(), - kind: kind.clone() - } - } - PolyfillError::MarketData { message, kind } => { - PolyfillError::MarketData { - message: message.clone(), - kind: kind.clone() - } - } - PolyfillError::Config { message } => { - PolyfillError::Config { - message: message.clone() - } - } - PolyfillError::Parse { message, source: _ } => { - PolyfillError::Parse { - message: message.clone(), - source: None - } - } - PolyfillError::Timeout { duration, operation } => { - PolyfillError::Timeout { - duration: *duration, - operation: operation.clone() - } - } - PolyfillError::RateLimit { message, retry_after } => { - PolyfillError::RateLimit { - message: message.clone(), - retry_after: *retry_after - } - } - PolyfillError::Stream { message, kind } => { - PolyfillError::Stream { - message: message.clone(), - kind: kind.clone() - } - } - PolyfillError::Validation { message, field } => { - PolyfillError::Validation { - message: message.clone(), - field: field.clone() - } - } - PolyfillError::Internal { message, source: _ } => { - PolyfillError::Internal { - message: message.clone(), - source: None - } - } + PolyfillError::Network { message, source: _ } => PolyfillError::Network { + message: message.clone(), + source: None, + }, + PolyfillError::Api { + status, + message, + error_code, + } => PolyfillError::Api { + status: *status, + message: message.clone(), + error_code: error_code.clone(), + }, + PolyfillError::Auth { message, kind } => PolyfillError::Auth { + message: message.clone(), + kind: kind.clone(), + }, + PolyfillError::Order { message, kind } => PolyfillError::Order { + message: message.clone(), + kind: kind.clone(), + }, + PolyfillError::MarketData { message, kind } => PolyfillError::MarketData { + message: message.clone(), + kind: kind.clone(), + }, + PolyfillError::Config { message } => PolyfillError::Config { + message: message.clone(), + }, + PolyfillError::Parse { message, source: _ } => PolyfillError::Parse { + message: message.clone(), + source: None, + }, + PolyfillError::Timeout { + duration, + operation, + } => PolyfillError::Timeout { + duration: *duration, + operation: operation.clone(), + }, + PolyfillError::RateLimit { + message, + retry_after, + } => PolyfillError::RateLimit { + message: message.clone(), + retry_after: *retry_after, + }, + PolyfillError::Stream { message, kind } => PolyfillError::Stream { + message: message.clone(), + kind: kind.clone(), + }, + PolyfillError::Validation { message, field } => PolyfillError::Validation { + message: message.clone(), + field: field.clone(), + }, + PolyfillError::Internal { message, source: _ } => PolyfillError::Internal { + message: message.clone(), + source: None, + }, } } } /// Result type alias for convenience -pub type Result = std::result::Result; \ No newline at end of file +pub type Result = std::result::Result; diff --git a/src/fill.rs b/src/fill.rs index b6f1c95..8c28030 100644 --- a/src/fill.rs +++ b/src/fill.rs @@ -69,7 +69,7 @@ impl FillEngine { book: &crate::book::OrderBook, ) -> Result { let start_time = Utc::now(); - + // Validate order self.validate_market_order(order)?; @@ -81,7 +81,10 @@ impl FillEngine { if levels.is_empty() { return Ok(FillResult { - order_id: order.client_id.clone().unwrap_or_else(|| "market_order".to_string()), + order_id: order + .client_id + .clone() + .unwrap_or_else(|| "market_order".to_string()), fills: Vec::new(), total_size: Decimal::ZERO, average_price: Decimal::ZERO, @@ -105,13 +108,16 @@ impl FillEngine { let fill_size = std::cmp::min(remaining_size, level.size); let fill_cost = fill_size * level.price; - + // Calculate fee let fee = self.calculate_fee(fill_cost); - + let fill = FillEvent { id: uuid::Uuid::new_v4().to_string(), - order_id: order.client_id.clone().unwrap_or_else(|| "market_order".to_string()), + order_id: order + .client_id + .clone() + .unwrap_or_else(|| "market_order".to_string()), token_id: order.token_id.clone(), side: order.side, price: level.price, @@ -136,7 +142,10 @@ impl FillEngine { slippage, self.max_slippage_pct ); return Ok(FillResult { - order_id: order.client_id.clone().unwrap_or_else(|| "market_order".to_string()), + order_id: order + .client_id + .clone() + .unwrap_or_else(|| "market_order".to_string()), fills: Vec::new(), total_size: Decimal::ZERO, average_price: Decimal::ZERO, @@ -166,7 +175,10 @@ impl FillEngine { let total_fees: Decimal = fills.iter().map(|f| f.fee).sum(); let result = FillResult { - order_id: order.client_id.clone().unwrap_or_else(|| "market_order".to_string()), + order_id: order + .client_id + .clone() + .unwrap_or_else(|| "market_order".to_string()), fills, total_size, average_price, @@ -178,7 +190,8 @@ impl FillEngine { // Store fills for tracking if !result.fills.is_empty() { - self.fills.insert(result.order_id.clone(), result.fills.clone()); + self.fills + .insert(result.order_id.clone(), result.fills.clone()); } info!( @@ -211,19 +224,22 @@ impl FillEngine { } else { false } - } + }, Side::SELL => { if let Some(best_bid) = book.best_bid() { order.price <= best_bid.price } else { false } - } + }, }; if !can_fill { return Ok(FillResult { - order_id: order.client_id.clone().unwrap_or_else(|| "limit_order".to_string()), + order_id: order + .client_id + .clone() + .unwrap_or_else(|| "limit_order".to_string()), fills: Vec::new(), total_size: Decimal::ZERO, average_price: Decimal::ZERO, @@ -237,7 +253,10 @@ impl FillEngine { // Simulate immediate fill let fill = FillEvent { id: uuid::Uuid::new_v4().to_string(), - order_id: order.client_id.clone().unwrap_or_else(|| "limit_order".to_string()), + order_id: order + .client_id + .clone() + .unwrap_or_else(|| "limit_order".to_string()), token_id: order.token_id.clone(), side: order.side, price: order.price, @@ -249,7 +268,10 @@ impl FillEngine { }; let result = FillResult { - order_id: order.client_id.clone().unwrap_or_else(|| "limit_order".to_string()), + order_id: order + .client_id + .clone() + .unwrap_or_else(|| "limit_order".to_string()), fills: vec![fill], total_size: order.size, average_price: order.price, @@ -260,7 +282,8 @@ impl FillEngine { }; // Store fills for tracking - self.fills.insert(result.order_id.clone(), result.fills.clone()); + self.fills + .insert(result.order_id.clone(), result.fills.clone()); info!( "Limit order executed: {} {} @ {}", @@ -273,7 +296,11 @@ impl FillEngine { } /// Calculate slippage for a market order - fn calculate_slippage(&self, order: &MarketOrderRequest, fills: &[FillEvent]) -> Option { + fn calculate_slippage( + &self, + order: &MarketOrderRequest, + fills: &[FillEvent], + ) -> Option { if fills.is_empty() { return None; } @@ -284,11 +311,15 @@ impl FillEngine { // Get reference price (best bid/ask) let reference_price = match order.side { - Side::BUY => fills.first()?.price, // Best ask + Side::BUY => fills.first()?.price, // Best ask Side::SELL => fills.first()?.price, // Best bid }; - Some(math::calculate_slippage(reference_price, average_price, order.side)) + Some(math::calculate_slippage( + reference_price, + average_price, + order.side, + )) } /// Calculate fee for a trade @@ -307,7 +338,10 @@ impl FillEngine { if order.amount < self.min_fill_size { return Err(PolyfillError::order( - format!("Order size {} below minimum {}", order.amount, self.min_fill_size), + format!( + "Order size {} below minimum {}", + order.amount, self.min_fill_size + ), crate::errors::OrderErrorKind::SizeConstraint, )); } @@ -333,7 +367,10 @@ impl FillEngine { if order.size < self.min_fill_size { return Err(PolyfillError::order( - format!("Order size {} below minimum {}", order.size, self.min_fill_size), + format!( + "Order size {} below minimum {}", + order.size, self.min_fill_size + ), crate::errors::OrderErrorKind::SizeConstraint, )); } @@ -424,7 +461,12 @@ impl FillProcessor { self.cleanup_old_pending(); } - debug!("Processed fill: {} {} @ {}", fill.size, fill.side.as_str(), fill.price); + debug!( + "Processed fill: {} {} @ {}", + fill.size, + fill.side.as_str(), + fill.price + ); Ok(()) } @@ -517,7 +559,7 @@ mod tests { #[test] fn test_market_order_validation() { let engine = FillEngine::new(dec!(1), dec!(5), 10); - + let valid_order = MarketOrderRequest { token_id: "test".to_string(), side: Side::BUY, @@ -547,7 +589,7 @@ mod tests { #[test] fn test_fill_processor() { let mut processor = FillProcessor::new(100); - + let fill = FillEvent { id: "fill1".to_string(), order_id: "order1".to_string(), @@ -569,7 +611,7 @@ mod tests { fn test_fill_engine_advanced_creation() { // Test that we can create a fill engine with parameters let _engine = FillEngine::new(dec!(1.0), dec!(0.05), 50); // min_fill_size, max_slippage, fee_rate_bps - + // Test basic properties exist (we can't access private fields directly) // But we can test that the engine was created successfully // Engine creation successful @@ -578,7 +620,7 @@ mod tests { #[test] fn test_fill_processor_basic_operations() { let mut processor = FillProcessor::new(100); // max_pending - + // Test that we can create a fill event and process it let fill_event = FillEvent { id: "fill_1".to_string(), @@ -592,11 +634,11 @@ mod tests { taker_address: alloy_primitives::Address::ZERO, fee: dec!(0.01), }; - + let result = processor.process_fill(fill_event); assert!(result.is_ok()); - + // Check that the fill was added to pending assert_eq!(processor.pending_fills.len(), 1); } -} \ No newline at end of file +} diff --git a/src/http_config.rs b/src/http_config.rs index e38b680..9f60474 100644 --- a/src/http_config.rs +++ b/src/http_config.rs @@ -1,5 +1,5 @@ //! HTTP client optimization for low-latency trading -//! +//! //! This module provides optimized HTTP client configurations specifically //! designed for high-frequency trading environments where every millisecond counts. @@ -10,7 +10,7 @@ use std::time::Duration; pub async fn prewarm_connections(client: &Client, base_url: &str) -> Result<(), reqwest::Error> { // Make a few lightweight requests to establish connections let endpoints = vec!["/ok", "/time"]; - + for endpoint in endpoints { let _ = client .get(format!("{}{}", base_url, endpoint)) @@ -18,7 +18,7 @@ pub async fn prewarm_connections(client: &Client, base_url: &str) -> Result<(), .send() .await; } - + Ok(()) } @@ -26,30 +26,24 @@ pub async fn prewarm_connections(client: &Client, base_url: &str) -> Result<(), pub fn create_optimized_client() -> Result { ClientBuilder::new() // Connection pooling optimizations - .pool_max_idle_per_host(10) // Keep connections alive - .pool_idle_timeout(Duration::from_secs(30)) // Reuse connections - + .pool_max_idle_per_host(10) // Keep connections alive + .pool_idle_timeout(Duration::from_secs(30)) // Reuse connections // Timeout optimizations - aggressive but safe - .connect_timeout(Duration::from_millis(5000)) // 5s connection timeout - .timeout(Duration::from_millis(30000)) // 30s total timeout - + .connect_timeout(Duration::from_millis(5000)) // 5s connection timeout + .timeout(Duration::from_millis(30000)) // 30s total timeout // TCP optimizations - .tcp_nodelay(true) // Disable Nagle's algorithm for lower latency - .tcp_keepalive(Duration::from_secs(60)) // Keep connections alive - + .tcp_nodelay(true) // Disable Nagle's algorithm for lower latency + .tcp_keepalive(Duration::from_secs(60)) // Keep connections alive // HTTP/2 optimizations - .http2_prior_knowledge() // Use HTTP/2 if server supports it + .http2_prior_knowledge() // Use HTTP/2 if server supports it .http2_keep_alive_interval(Duration::from_secs(30)) .http2_keep_alive_timeout(Duration::from_secs(10)) .http2_keep_alive_while_idle(true) - // Compression - balance between CPU and network - .gzip(true) // Enable gzip compression + .gzip(true) // Enable gzip compression // Brotli is enabled by default in reqwest - // User agent for identification .user_agent("polyfill-rs/0.1.1 (high-frequency-trading)") - .build() } @@ -58,29 +52,23 @@ pub fn create_optimized_client() -> Result { pub fn create_colocated_client() -> Result { ClientBuilder::new() // More aggressive connection pooling - .pool_max_idle_per_host(20) // More connections - .pool_idle_timeout(Duration::from_secs(60)) // Longer reuse - + .pool_max_idle_per_host(20) // More connections + .pool_idle_timeout(Duration::from_secs(60)) // Longer reuse // Tighter timeouts for co-located environments - .connect_timeout(Duration::from_millis(1000)) // 1s connection - .timeout(Duration::from_millis(10000)) // 10s total - + .connect_timeout(Duration::from_millis(1000)) // 1s connection + .timeout(Duration::from_millis(10000)) // 10s total // TCP optimizations .tcp_nodelay(true) .tcp_keepalive(Duration::from_secs(30)) - // HTTP/2 with more aggressive keep-alive .http2_prior_knowledge() .http2_keep_alive_interval(Duration::from_secs(10)) .http2_keep_alive_timeout(Duration::from_secs(5)) .http2_keep_alive_while_idle(true) - // Disable compression in co-located environments (CPU vs network tradeoff) .gzip(false) - .no_brotli() // Disable brotli compression - + .no_brotli() // Disable brotli compression .user_agent("polyfill-rs/0.1.1 (colocated-hft)") - .build() } @@ -91,23 +79,17 @@ pub fn create_internet_client() -> Result { // Conservative connection pooling .pool_max_idle_per_host(5) .pool_idle_timeout(Duration::from_secs(90)) - // Longer timeouts for internet connections - .connect_timeout(Duration::from_millis(10000)) // 10s connection - .timeout(Duration::from_millis(60000)) // 60s total - + .connect_timeout(Duration::from_millis(10000)) // 10s connection + .timeout(Duration::from_millis(60000)) // 60s total // TCP optimizations .tcp_nodelay(true) .tcp_keepalive(Duration::from_secs(120)) - // HTTP/1.1 might be more reliable over internet .http1_title_case_headers() - // Enable compression (gzip and brotli are enabled by default) .gzip(true) - .user_agent("polyfill-rs/0.1.1 (internet-trading)") - .build() } diff --git a/src/lib.rs b/src/lib.rs index f2d2b6b..4587fd5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ //! Polyfill-rs: High-performance Rust client for Polymarket -//! +//! //! # Features -//! +//! //! - **High-performance order book management** with optimized data structures //! - **Real-time market data streaming** with WebSocket support //! - **Trade execution simulation** with slippage protection @@ -9,14 +9,14 @@ //! - **Rate limiting and retry logic** for robust API interactions //! - **Ethereum integration** with EIP-712 signing support //! - **Benchmarking tools** for performance analysis -//! +//! //! # Quick Start -//! +//! //! ```rust,no_run //! use polyfill_rs::{ClobClient, OrderArgs, Side}; //! use rust_decimal::Decimal; //! use std::str::FromStr; -//! +//! //! #[tokio::main] //! async fn main() -> Result<(), Box> { //! // Create client (compatible with polymarket-rs-client) @@ -25,11 +25,11 @@ //! "your_private_key", //! 137, //! ); -//! +//! //! // Get API credentials //! let api_creds = client.create_or_derive_api_key(None).await.unwrap(); //! client.set_api_creds(api_creds); -//! +//! //! // Create and post order //! let order_args = OrderArgs::new( //! "token_id", @@ -37,40 +37,39 @@ //! Decimal::from_str("100.0").unwrap(), //! Side::BUY, //! ); -//! +//! //! let result = client.create_and_post_order(&order_args).await.unwrap(); //! println!("Order posted: {:?}", result); -//! +//! //! Ok(()) //! } //! ``` -//! +//! //! # Advanced Usage -//! +//! //! ```rust,no_run //! use polyfill_rs::{ClobClient, OrderBookImpl}; //! use rust_decimal::Decimal; -//! +//! //! #[tokio::main] //! async fn main() -> Result<(), Box> { //! // Create a basic client //! let client = ClobClient::new("https://clob.polymarket.com"); -//! +//! //! // Get market data //! let markets = client.get_sampling_markets(None).await.unwrap(); //! println!("Found {} markets", markets.data.len()); -//! +//! //! // Create an order book for high-performance operations //! let mut book = OrderBookImpl::new("token_id".to_string(), 100); // 100 levels depth //! println!("Order book created for token: {}", book.token_id); -//! +//! //! Ok(()) //! } //! ``` use tracing::info; - // Global constants pub const DEFAULT_CHAIN_ID: u64 = 137; // Polygon pub const DEFAULT_BASE_URL: &str = "https://clob.polymarket.com"; @@ -86,39 +85,70 @@ pub fn init() { // Re-export main types pub use crate::types::{ - ApiCredentials, Balance, BalanceAllowance, BatchMidpointRequest, BatchMidpointResponse, - BatchPriceRequest, BatchPriceResponse, ClientConfig, FillEvent, MarketSnapshot, - NotificationParams, OpenOrder, OpenOrderParams, Order, OrderBook, OrderDelta, - OrderRequest, OrderStatus, OrderType, Side, StreamMessage, TokenPrice, TradeParams, - WssAuth, WssSubscription, WssChannelType, + ApiCredentials, // Additional compatibility types - ApiKeysResponse, MidpointResponse, PriceResponse, SpreadResponse, TickSizeResponse, - NegRiskResponse, BookParams, MarketsResponse, SimplifiedMarketsResponse, Market, - SimplifiedMarket, Token, Rewards, ClientResult, OrderBookSummary, OrderSummary, - BalanceAllowanceParams, AssetType, + ApiKeysResponse, + AssetType, + Balance, + BalanceAllowance, + BalanceAllowanceParams, + BatchMidpointRequest, + BatchMidpointResponse, + BatchPriceRequest, + BatchPriceResponse, + BookParams, + ClientConfig, + ClientResult, + FillEvent, + Market, + MarketSnapshot, + MarketsResponse, + MidpointResponse, + NegRiskResponse, + NotificationParams, + OpenOrder, + OpenOrderParams, + Order, + OrderBook, + OrderBookSummary, + OrderDelta, + OrderRequest, + OrderStatus, + OrderSummary, + OrderType, + PriceResponse, + Rewards, + Side, + SimplifiedMarket, + SimplifiedMarketsResponse, + SpreadResponse, + StreamMessage, + TickSizeResponse, + Token, + TokenPrice, + TradeParams, + WssAuth, + WssChannelType, + WssSubscription, }; // Re-export client pub use crate::client::{ClobClient, PolyfillClient}; // Re-export compatibility types (for easy migration from polymarket-rs-client) -pub use crate::client::{ - OrderArgs, -}; +pub use crate::client::OrderArgs; // Re-export error types pub use crate::errors::{PolyfillError, Result}; // Re-export advanced components pub use crate::book::{OrderBook as OrderBookImpl, OrderBookManager}; +pub use crate::decode::Decoder; pub use crate::fill::{FillEngine, FillResult}; pub use crate::stream::{MarketStream, StreamManager, WebSocketStream}; -pub use crate::decode::Decoder; // Re-export utilities -pub use crate::utils::{ - crypto, math, retry, time, url, rate_limit, -}; +pub use crate::utils::{crypto, math, rate_limit, retry, time, url}; // Module declarations pub mod auth; @@ -136,16 +166,16 @@ pub mod utils; // Benchmarks #[cfg(test)] mod benches { - use criterion::{criterion_group, criterion_main}; use crate::{OrderBookManager, OrderDelta, Side}; - use rust_decimal::Decimal; use chrono::Utc; + use criterion::{criterion_group, criterion_main}; + use rust_decimal::Decimal; use std::str::FromStr; #[allow(dead_code)] fn order_book_benchmark(c: &mut criterion::Criterion) { let book_manager = OrderBookManager::new(100); - + c.bench_function("apply_order_delta", |b| { b.iter(|| { let delta = OrderDelta { @@ -156,7 +186,7 @@ mod benches { size: Decimal::from_str("100.0").unwrap(), sequence: 1, }; - + let _ = book_manager.apply_delta(delta); }); }); @@ -188,7 +218,7 @@ mod tests { Decimal::from_str("100.0").unwrap(), Side::BUY, ); - + assert_eq!(args.token_id, "test_token"); assert_eq!(args.side, Side::BUY); } @@ -201,4 +231,4 @@ mod tests { assert_eq!(args.size, Decimal::ZERO); assert_eq!(args.side, Side::BUY); } -} \ No newline at end of file +} diff --git a/src/orders.rs b/src/orders.rs index 7d688f8..15cde0c 100644 --- a/src/orders.rs +++ b/src/orders.rs @@ -4,9 +4,9 @@ //! for the Polymarket CLOB, including EIP-712 signature generation. use crate::auth::sign_order_message; -use crate::errors::{PolyfillError, Result}; use crate::client::OrderArgs; -use crate::types::{ExtraOrderArgs, MarketOrderArgs, OrderOptions, SignedOrderRequest, Side}; +use crate::errors::{PolyfillError, Result}; +use crate::types::{ExtraOrderArgs, MarketOrderArgs, OrderOptions, Side, SignedOrderRequest}; use alloy_primitives::{Address, U256}; use alloy_signer_local::PrivateKeySigner; use rand::Rng; @@ -42,7 +42,6 @@ pub struct ContractConfig { pub conditional_tokens: String, } - /// Order builder for creating and signing orders pub struct OrderBuilder { signer: PrivateKeySigner, @@ -177,7 +176,7 @@ impl OrderBuilder { decimal_to_token_u32(raw_maker_amt), decimal_to_token_u32(raw_taker_amt), ) - } + }, Side::SELL => { let raw_maker_amt = size.round_dp_with_strategy(round_config.size, ToZero); let raw_taker_amt = raw_maker_amt * raw_price; @@ -187,7 +186,7 @@ impl OrderBuilder { decimal_to_token_u32(raw_maker_amt), decimal_to_token_u32(raw_taker_amt), ) - } + }, } } @@ -224,9 +223,12 @@ impl OrderBuilder { return Ok(level.price); } } - + Err(PolyfillError::order( - format!("Not enough liquidity to create market order with amount {}", amount_to_match), + format!( + "Not enough liquidity to create market order with amount {}", + amount_to_match + ), crate::errors::OrderErrorKind::InsufficientBalance, )) } @@ -240,20 +242,20 @@ impl OrderBuilder { extras: &ExtraOrderArgs, options: &OrderOptions, ) -> Result { - let tick_size = options.tick_size + let tick_size = options + .tick_size .ok_or_else(|| PolyfillError::validation("Cannot create order without tick size"))?; - - let (maker_amount, taker_amount) = self.get_market_order_amounts( - order_args.amount, - price, - &ROUNDING_CONFIG[&tick_size], - ); - let neg_risk = options.neg_risk + let (maker_amount, taker_amount) = + self.get_market_order_amounts(order_args.amount, price, &ROUNDING_CONFIG[&tick_size]); + + let neg_risk = options + .neg_risk .ok_or_else(|| PolyfillError::validation("Cannot create order without neg_risk"))?; - let contract_config = get_contract_config(chain_id, neg_risk) - .ok_or_else(|| PolyfillError::config("No contract found with given chain_id and neg_risk"))?; + let contract_config = get_contract_config(chain_id, neg_risk).ok_or_else(|| { + PolyfillError::config("No contract found with given chain_id and neg_risk") + })?; let exchange_address = Address::from_str(&contract_config.exchange) .map_err(|e| PolyfillError::config(format!("Invalid exchange address: {}", e)))?; @@ -279,9 +281,10 @@ impl OrderBuilder { extras: &ExtraOrderArgs, options: &OrderOptions, ) -> Result { - let tick_size = options.tick_size + let tick_size = options + .tick_size .ok_or_else(|| PolyfillError::validation("Cannot create order without tick size"))?; - + let (maker_amount, taker_amount) = self.get_order_amounts( order_args.side, order_args.size, @@ -289,11 +292,13 @@ impl OrderBuilder { &ROUNDING_CONFIG[&tick_size], ); - let neg_risk = options.neg_risk + let neg_risk = options + .neg_risk .ok_or_else(|| PolyfillError::validation("Cannot create order without neg_risk"))?; - let contract_config = get_contract_config(chain_id, neg_risk) - .ok_or_else(|| PolyfillError::config("No contract found with given chain_id and neg_risk"))?; + let contract_config = get_contract_config(chain_id, neg_risk).ok_or_else(|| { + PolyfillError::config("No contract found with given chain_id and neg_risk") + })?; let exchange_address = Address::from_str(&contract_config.exchange) .map_err(|e| PolyfillError::config(format!("Invalid exchange address: {}", e)))?; @@ -387,11 +392,11 @@ mod tests { // Test zero let result = decimal_to_token_u32(Decimal::ZERO); assert_eq!(result, 0); - + // Test small decimal let result = decimal_to_token_u32(Decimal::from_str("0.000001").unwrap()); assert_eq!(result, 1); - + // Test large number let result = decimal_to_token_u32(Decimal::from_str("1000.0").unwrap()); assert_eq!(result, 1_000_000_000); @@ -402,11 +407,11 @@ mod tests { // Test Polygon mainnet let config = get_contract_config(137, false); assert!(config.is_some()); - + // Test with neg risk let config_neg = get_contract_config(137, true); assert!(config_neg.is_some()); - + // Test unsupported chain let config_unsupported = get_contract_config(999, false); assert!(config_unsupported.is_none()); @@ -415,7 +420,7 @@ mod tests { #[test] fn test_seed_generation_uniqueness() { let mut seeds = std::collections::HashSet::new(); - + // Generate 1000 seeds and ensure they're all unique for _ in 0..1000 { let seed = generate_seed(); diff --git a/src/stream.rs b/src/stream.rs index 8edc6e9..67fd2d3 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -5,25 +5,25 @@ use crate::errors::{PolyfillError, Result}; use crate::types::*; -use futures::{Stream, SinkExt, StreamExt}; +use chrono::Utc; +use futures::{SinkExt, Stream, StreamExt}; use serde_json::Value; use std::pin::Pin; use std::task::{Context, Poll}; use tokio::sync::mpsc; use tracing::{debug, error, info, warn}; -use chrono::Utc; /// Trait for market data streams pub trait MarketStream: Stream> + Send + Sync { /// Subscribe to market data for specific tokens fn subscribe(&mut self, subscription: Subscription) -> Result<()>; - + /// Unsubscribe from market data fn unsubscribe(&mut self, token_ids: &[String]) -> Result<()>; - + /// Check if the stream is connected fn is_connected(&self) -> bool; - + /// Get connection statistics fn get_stats(&self) -> StreamStats; } @@ -33,7 +33,11 @@ pub trait MarketStream: Stream> + Send + Sync { #[allow(dead_code)] pub struct WebSocketStream { /// WebSocket connection - connection: Option>>, + connection: Option< + tokio_tungstenite::WebSocketStream< + tokio_tungstenite::MaybeTlsStream, + >, + >, /// URL for the WebSocket connection url: String, /// Authentication credentials @@ -85,7 +89,7 @@ impl WebSocketStream { /// Create a new WebSocket stream pub fn new(url: &str) -> Self { let (tx, rx) = mpsc::unbounded_channel(); - + Self { connection: None, url: url.to_string(), @@ -113,8 +117,14 @@ impl WebSocketStream { /// Connect to the WebSocket async fn connect(&mut self) -> Result<()> { - let (ws_stream, _) = tokio_tungstenite::connect_async(&self.url).await - .map_err(|e| PolyfillError::stream(format!("WebSocket connection failed: {}", e), crate::errors::StreamErrorKind::ConnectionFailed))?; + let (ws_stream, _) = tokio_tungstenite::connect_async(&self.url) + .await + .map_err(|e| { + PolyfillError::stream( + format!("WebSocket connection failed: {}", e), + crate::errors::StreamErrorKind::ConnectionFailed, + ) + })?; self.connection = Some(ws_stream); info!("Connected to WebSocket stream at {}", self.url); @@ -124,16 +134,21 @@ impl WebSocketStream { /// Send a message to the WebSocket async fn send_message(&mut self, message: Value) -> Result<()> { if let Some(connection) = &mut self.connection { - let text = serde_json::to_string(&message) - .map_err(|e| PolyfillError::parse(format!("Failed to serialize message: {}", e), None))?; - + let text = serde_json::to_string(&message).map_err(|e| { + PolyfillError::parse(format!("Failed to serialize message: {}", e), None) + })?; + let ws_message = tokio_tungstenite::tungstenite::Message::Text(text); - connection.send(ws_message).await - .map_err(|e| PolyfillError::stream(format!("Failed to send message: {}", e), crate::errors::StreamErrorKind::MessageCorrupted))?; - + connection.send(ws_message).await.map_err(|e| { + PolyfillError::stream( + format!("Failed to send message: {}", e), + crate::errors::StreamErrorKind::MessageCorrupted, + ) + })?; + self.stats.messages_sent += 1; } - + Ok(()) } @@ -154,14 +169,16 @@ impl WebSocketStream { self.send_message(message).await?; self.subscriptions.push(subscription.clone()); - + info!("Subscribed to {} channel", subscription.channel_type); Ok(()) } /// Subscribe to user channel (orders and trades) pub async fn subscribe_user_channel(&mut self, markets: Vec) -> Result<()> { - let auth = self.auth.as_ref() + let auth = self + .auth + .as_ref() .ok_or_else(|| PolyfillError::auth("No authentication provided for WebSocket"))? .clone(); @@ -177,7 +194,9 @@ impl WebSocketStream { /// Subscribe to market channel (order book and trades) pub async fn subscribe_market_channel(&mut self, asset_ids: Vec) -> Result<()> { - let auth = self.auth.as_ref() + let auth = self + .auth + .as_ref() .ok_or_else(|| PolyfillError::auth("No authentication provided for WebSocket"))? .clone(); @@ -195,52 +214,54 @@ impl WebSocketStream { pub async fn unsubscribe_async(&mut self, token_ids: &[String]) -> Result<()> { // Note: Polymarket WebSocket API doesn't seem to have explicit unsubscribe // We'll just remove from our local subscriptions - self.subscriptions.retain(|sub| { - match sub.channel_type.as_str() { + self.subscriptions + .retain(|sub| match sub.channel_type.as_str() { "USER" => { if let Some(markets) = &sub.markets { !token_ids.iter().any(|id| markets.contains(id)) } else { true } - } + }, "MARKET" => { if let Some(asset_ids) = &sub.asset_ids { !token_ids.iter().any(|id| asset_ids.contains(id)) } else { true } - } - _ => true - } - }); - + }, + _ => true, + }); + info!("Unsubscribed from {} tokens", token_ids.len()); Ok(()) } /// Handle incoming WebSocket messages #[allow(dead_code)] - async fn handle_message(&mut self, message: tokio_tungstenite::tungstenite::Message) -> Result<()> { + async fn handle_message( + &mut self, + message: tokio_tungstenite::tungstenite::Message, + ) -> Result<()> { match message { tokio_tungstenite::tungstenite::Message::Text(text) => { debug!("Received WebSocket message: {}", text); - + // Parse the message according to Polymarket's format let stream_message = self.parse_polymarket_message(&text)?; - + // Send to internal channel if let Err(e) = self.tx.send(stream_message) { error!("Failed to send message to internal channel: {}", e); } - + self.stats.messages_received += 1; self.stats.last_message_time = Some(Utc::now()); - } + }, tokio_tungstenite::tungstenite::Message::Close(_) => { info!("WebSocket connection closed by server"); self.connection = None; - } + }, tokio_tungstenite::tungstenite::Message::Ping(data) => { // Respond with pong if let Some(connection) = &mut self.connection { @@ -249,81 +270,130 @@ impl WebSocketStream { error!("Failed to send pong: {}", e); } } - } + }, tokio_tungstenite::tungstenite::Message::Pong(_) => { // Handle pong if needed debug!("Received pong"); - } + }, tokio_tungstenite::tungstenite::Message::Binary(_) => { warn!("Received binary message (not supported)"); - } + }, tokio_tungstenite::tungstenite::Message::Frame(_) => { warn!("Received raw frame (not supported)"); - } + }, } - + Ok(()) } /// Parse Polymarket WebSocket message format #[allow(dead_code)] fn parse_polymarket_message(&self, text: &str) -> Result { - let value: Value = serde_json::from_str(text) - .map_err(|e| PolyfillError::parse(format!("Failed to parse WebSocket message: {}", e), Some(Box::new(e))))?; + let value: Value = serde_json::from_str(text).map_err(|e| { + PolyfillError::parse( + format!("Failed to parse WebSocket message: {}", e), + Some(Box::new(e)), + ) + })?; // Extract message type - let message_type = value.get("type") - .and_then(|v| v.as_str()) - .ok_or_else(|| PolyfillError::parse("Missing 'type' field in WebSocket message", None))?; + let message_type = value.get("type").and_then(|v| v.as_str()).ok_or_else(|| { + PolyfillError::parse("Missing 'type' field in WebSocket message", None) + })?; match message_type { "book_update" => { - let data = serde_json::from_value(value.get("data").unwrap_or(&Value::Null).clone()) - .map_err(|e| PolyfillError::parse(format!("Failed to parse book update: {}", e), Some(Box::new(e))))?; + let data = + serde_json::from_value(value.get("data").unwrap_or(&Value::Null).clone()) + .map_err(|e| { + PolyfillError::parse( + format!("Failed to parse book update: {}", e), + Some(Box::new(e)), + ) + })?; Ok(StreamMessage::BookUpdate { data }) - } + }, "trade" => { - let data = serde_json::from_value(value.get("data").unwrap_or(&Value::Null).clone()) - .map_err(|e| PolyfillError::parse(format!("Failed to parse trade: {}", e), Some(Box::new(e))))?; + let data = + serde_json::from_value(value.get("data").unwrap_or(&Value::Null).clone()) + .map_err(|e| { + PolyfillError::parse( + format!("Failed to parse trade: {}", e), + Some(Box::new(e)), + ) + })?; Ok(StreamMessage::Trade { data }) - } + }, "order_update" => { - let data = serde_json::from_value(value.get("data").unwrap_or(&Value::Null).clone()) - .map_err(|e| PolyfillError::parse(format!("Failed to parse order update: {}", e), Some(Box::new(e))))?; + let data = + serde_json::from_value(value.get("data").unwrap_or(&Value::Null).clone()) + .map_err(|e| { + PolyfillError::parse( + format!("Failed to parse order update: {}", e), + Some(Box::new(e)), + ) + })?; Ok(StreamMessage::OrderUpdate { data }) - } + }, "user_order_update" => { - let data = serde_json::from_value(value.get("data").unwrap_or(&Value::Null).clone()) - .map_err(|e| PolyfillError::parse(format!("Failed to parse user order update: {}", e), Some(Box::new(e))))?; + let data = + serde_json::from_value(value.get("data").unwrap_or(&Value::Null).clone()) + .map_err(|e| { + PolyfillError::parse( + format!("Failed to parse user order update: {}", e), + Some(Box::new(e)), + ) + })?; Ok(StreamMessage::UserOrderUpdate { data }) - } + }, "user_trade" => { - let data = serde_json::from_value(value.get("data").unwrap_or(&Value::Null).clone()) - .map_err(|e| PolyfillError::parse(format!("Failed to parse user trade: {}", e), Some(Box::new(e))))?; + let data = + serde_json::from_value(value.get("data").unwrap_or(&Value::Null).clone()) + .map_err(|e| { + PolyfillError::parse( + format!("Failed to parse user trade: {}", e), + Some(Box::new(e)), + ) + })?; Ok(StreamMessage::UserTrade { data }) - } + }, "market_book_update" => { - let data = serde_json::from_value(value.get("data").unwrap_or(&Value::Null).clone()) - .map_err(|e| PolyfillError::parse(format!("Failed to parse market book update: {}", e), Some(Box::new(e))))?; + let data = + serde_json::from_value(value.get("data").unwrap_or(&Value::Null).clone()) + .map_err(|e| { + PolyfillError::parse( + format!("Failed to parse market book update: {}", e), + Some(Box::new(e)), + ) + })?; Ok(StreamMessage::MarketBookUpdate { data }) - } + }, "market_trade" => { - let data = serde_json::from_value(value.get("data").unwrap_or(&Value::Null).clone()) - .map_err(|e| PolyfillError::parse(format!("Failed to parse market trade: {}", e), Some(Box::new(e))))?; + let data = + serde_json::from_value(value.get("data").unwrap_or(&Value::Null).clone()) + .map_err(|e| { + PolyfillError::parse( + format!("Failed to parse market trade: {}", e), + Some(Box::new(e)), + ) + })?; Ok(StreamMessage::MarketTrade { data }) - } + }, "heartbeat" => { - let timestamp = value.get("timestamp") + let timestamp = value + .get("timestamp") .and_then(|v| v.as_u64()) .map(|ts| chrono::DateTime::from_timestamp(ts as i64, 0).unwrap_or_default()) .unwrap_or_else(Utc::now); Ok(StreamMessage::Heartbeat { timestamp }) - } + }, _ => { warn!("Unknown message type: {}", message_type); // Return heartbeat as fallback - Ok(StreamMessage::Heartbeat { timestamp: Utc::now() }) - } + Ok(StreamMessage::Heartbeat { + timestamp: Utc::now(), + }) + }, } } @@ -335,38 +405,42 @@ impl WebSocketStream { while retries < self.reconnect_config.max_retries { warn!("Attempting to reconnect (attempt {})", retries + 1); - + match self.connect().await { Ok(()) => { info!("Successfully reconnected"); self.stats.reconnect_count += 1; - + // Resubscribe to all previous subscriptions let subscriptions = self.subscriptions.clone(); for subscription in subscriptions { - self.send_message(serde_json::to_value(subscription)?).await?; + self.send_message(serde_json::to_value(subscription)?) + .await?; } - + return Ok(()); - } + }, Err(e) => { error!("Reconnection attempt {} failed: {}", retries + 1, e); retries += 1; - + if retries < self.reconnect_config.max_retries { tokio::time::sleep(delay).await; delay = std::cmp::min( delay.mul_f64(self.reconnect_config.backoff_multiplier), - self.reconnect_config.max_delay + self.reconnect_config.max_delay, ); } - } + }, } } Err(PolyfillError::stream( - format!("Failed to reconnect after {} attempts", self.reconnect_config.max_retries), - crate::errors::StreamErrorKind::ConnectionFailed + format!( + "Failed to reconnect after {} attempts", + self.reconnect_config.max_retries + ), + crate::errors::StreamErrorKind::ConnectionFailed, )) } } @@ -385,17 +459,19 @@ impl Stream for WebSocketStream { match connection.poll_next_unpin(cx) { Poll::Ready(Some(Ok(_message))) => { // Simplified message handling - Poll::Ready(Some(Ok(StreamMessage::Heartbeat { timestamp: Utc::now() }))) - } + Poll::Ready(Some(Ok(StreamMessage::Heartbeat { + timestamp: Utc::now(), + }))) + }, Poll::Ready(Some(Err(e))) => { error!("WebSocket error: {}", e); self.stats.errors += 1; Poll::Ready(Some(Err(e.into()))) - } + }, Poll::Ready(None) => { info!("WebSocket stream ended"); Poll::Ready(None) - } + }, Poll::Pending => Poll::Pending, } } else { @@ -516,7 +592,7 @@ impl Default for StreamManager { impl StreamManager { pub fn new() -> Self { let (message_tx, message_rx) = mpsc::unbounded_channel(); - + Self { streams: Vec::new(), message_tx, @@ -537,7 +613,8 @@ impl StreamManager { } pub fn broadcast_message(&self, message: StreamMessage) -> Result<()> { - self.message_tx.send(message) + self.message_tx + .send(message) .map_err(|e| PolyfillError::internal("Failed to broadcast message", e)) } } @@ -549,9 +626,11 @@ mod tests { #[test] fn test_mock_stream() { let mut stream = MockStream::new(); - + // Add some test messages - stream.add_message(StreamMessage::Heartbeat { timestamp: Utc::now() }); + stream.add_message(StreamMessage::Heartbeat { + timestamp: Utc::now(), + }); stream.add_message(StreamMessage::BookUpdate { data: OrderDelta { token_id: "test".to_string(), @@ -560,9 +639,9 @@ mod tests { price: rust_decimal_macros::dec!(0.5), size: rust_decimal_macros::dec!(100), sequence: 1, - } + }, }); - + assert!(stream.is_connected()); assert_eq!(stream.get_stats().messages_received, 2); } @@ -572,9 +651,11 @@ mod tests { let mut manager = StreamManager::new(); let mock_stream = Box::new(MockStream::new()); manager.add_stream(mock_stream); - + // Test message broadcasting - let message = StreamMessage::Heartbeat { timestamp: Utc::now() }; + let message = StreamMessage::Heartbeat { + timestamp: Utc::now(), + }; assert!(manager.broadcast_message(message).is_ok()); } -} \ No newline at end of file +} diff --git a/src/types.rs b/src/types.rs index 3329de0..623a380 100644 --- a/src/types.rs +++ b/src/types.rs @@ -5,8 +5,8 @@ use alloy_primitives::{Address, U256}; use chrono::{DateTime, Utc}; -use rust_decimal::Decimal; use rust_decimal::prelude::ToPrimitive; +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; // ============================================================================ @@ -34,20 +34,20 @@ use serde::{Deserialize, Serialize}; /// - $0.6543 = 6543 ticks /// - $1.0000 = 10000 ticks /// - $0.0001 = 1 tick (minimum price increment) -/// -/// Why u32? +/// +/// Why u32? /// - Can represent prices from $0.0001 to $429,496.7295 (way more than needed) /// - Fits in CPU register for fast operations /// - No sign bit needed since prices are always positive pub type Price = u32; /// Quantity/size represented as fixed-point integer for performance -/// +/// /// Each unit represents 0.0001 (1/10,000) of a token /// Examples: /// - 100.0 tokens = 1,000,000 units /// - 0.0001 tokens = 1 unit (minimum size increment) -/// +/// /// Why i64? /// - Can represent quantities from -922,337,203,685.4775 to +922,337,203,685.4775 /// - Signed because we need to handle both buys (+) and sells (-) @@ -55,7 +55,7 @@ pub type Price = u32; pub type Qty = i64; /// Scale factor for converting between Decimal and fixed-point -/// +/// /// We use 10,000 (1e4) as our scale factor, giving us 4 decimal places of precision. /// This is perfect for most prediction markets where prices are between $0.01-$0.99 /// and we need precision to the nearest $0.0001. @@ -80,11 +80,11 @@ pub const MAX_QTY: Qty = Qty::MAX / 2; // Leave room for intermediate calculatio // and handle edge cases gracefully. /// Convert a Decimal price to fixed-point ticks -/// +/// /// This is called when we receive price data from the API or user input. /// We quantize the price to the nearest tick to ensure all prices are /// aligned to our internal representation. -/// +/// /// Examples: /// - decimal_to_price(Decimal::from_str("0.6543")) = Ok(6543) /// - decimal_to_price(Decimal::from_str("1.0000")) = Ok(10000) @@ -92,13 +92,13 @@ pub const MAX_QTY: Qty = Qty::MAX / 2; // Leave room for intermediate calculatio pub fn decimal_to_price(decimal: Decimal) -> std::result::Result { // Convert to fixed-point by multiplying by scale factor let scaled = decimal * Decimal::from(SCALE_FACTOR); - + // Round to nearest integer (this handles tick alignment automatically) let rounded = scaled.round(); - + // Convert to u64 first to handle the conversion safely let as_u64 = rounded.to_u64().ok_or("Price too large or negative")?; - + // Check bounds if as_u64 < MIN_PRICE_TICKS as u64 { return Ok(MIN_PRICE_TICKS); // Clamp to minimum @@ -106,15 +106,15 @@ pub fn decimal_to_price(decimal: Decimal) -> std::result::Result MAX_PRICE_TICKS as u64 { return Err("Price exceeds maximum"); } - + Ok(as_u64 as Price) } /// Convert fixed-point ticks back to Decimal price -/// +/// /// This is called when we need to return price data to the API or display to users. /// It's the inverse of decimal_to_price(). -/// +/// /// Examples: /// - price_to_decimal(6543) = Decimal::from_str("0.6543") /// - price_to_decimal(10000) = Decimal::from_str("1.0000") @@ -123,28 +123,28 @@ pub fn price_to_decimal(ticks: Price) -> Decimal { } /// Convert a Decimal quantity to fixed-point units -/// +/// /// Similar to decimal_to_price but handles signed quantities. /// Quantities can be negative (for sells or position changes). -/// +/// /// Examples: /// - decimal_to_qty(Decimal::from_str("100.0")) = Ok(1000000) /// - decimal_to_qty(Decimal::from_str("-50.5")) = Ok(-505000) pub fn decimal_to_qty(decimal: Decimal) -> std::result::Result { let scaled = decimal * Decimal::from(SCALE_FACTOR); let rounded = scaled.round(); - + let as_i64 = rounded.to_i64().ok_or("Quantity too large")?; - + if as_i64.abs() > MAX_QTY { return Err("Quantity exceeds maximum"); } - + Ok(as_i64) } /// Convert fixed-point units back to Decimal quantity -/// +/// /// Examples: /// - qty_to_decimal(1000000) = Decimal::from_str("100.0") /// - qty_to_decimal(-505000) = Decimal::from_str("-50.5") @@ -153,11 +153,11 @@ pub fn qty_to_decimal(units: Qty) -> Decimal { } /// Check if a price is properly tick-aligned -/// +/// /// This is used to validate incoming price data. In a well-behaved system, /// all prices should already be tick-aligned, but we check anyway to catch /// bugs or malicious data. -/// +/// /// A price is tick-aligned if it's an exact multiple of the minimum tick size. /// Since we use integer ticks internally, this just checks if the price /// converts cleanly to our internal representation. @@ -167,19 +167,19 @@ pub fn is_price_tick_aligned(decimal: Decimal, tick_size_decimal: Decimal) -> bo Ok(ticks) => ticks, Err(_) => return false, }; - + // Convert the price to ticks let price_ticks = match decimal_to_price(decimal) { Ok(ticks) => ticks, Err(_) => return false, }; - + // Check if price is a multiple of tick size // If tick_size_ticks is 0, we consider everything aligned (no restrictions) if tick_size_ticks == 0 { return true; } - + price_ticks % tick_size_ticks == 0 } @@ -256,7 +256,7 @@ pub struct MarketSnapshot { } /// Order book level (price/size pair) - EXTERNAL API VERSION -/// +/// /// This is what we expose to users and serialize to JSON. /// It uses Decimal for precision and human readability. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -268,19 +268,19 @@ pub struct BookLevel { } /// Order book level (price/size pair) - INTERNAL HOT PATH VERSION -/// +/// /// This is what we use internally for maximum performance. /// All order book operations use this to avoid Decimal overhead. -/// +/// /// The performance difference is huge: /// - BookLevel: ~50ns per operation (Decimal math + allocation) /// - FastBookLevel: ~2ns per operation (integer math, no allocation) -/// +/// /// That's a 25x speedup on the critical path #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct FastBookLevel { - pub price: Price, // Price in ticks (u32) - pub size: Qty, // Size in fixed-point units (i64) + pub price: Price, // Price in ticks (u32) + pub size: Qty, // Size in fixed-point units (i64) } impl FastBookLevel { @@ -288,7 +288,7 @@ impl FastBookLevel { pub fn new(price: Price, size: Qty) -> Self { Self { price, size } } - + /// Convert to external BookLevel for API responses /// This is only called at the edges when we need to return data to users pub fn to_book_level(self) -> BookLevel { @@ -297,7 +297,7 @@ impl FastBookLevel { size: qty_to_decimal(self.size), } } - + /// Create from external BookLevel (with validation) /// This is called when we receive data from the API pub fn from_book_level(level: &BookLevel) -> std::result::Result { @@ -305,10 +305,10 @@ impl FastBookLevel { let size = decimal_to_qty(level.size)?; Ok(Self::new(price, size)) } - + /// Calculate notional value (price * size) in fixed-point /// Returns the result scaled appropriately to avoid overflow - /// + /// /// This is much faster than the Decimal equivalent: /// - Decimal: price.mul(size) -> ~20ns + allocation /// - Fixed-point: (price as i64 * size) / SCALE_FACTOR -> ~1ns, no allocation @@ -336,7 +336,7 @@ pub struct OrderBook { } /// Order book delta for streaming updates - EXTERNAL API VERSION -/// +/// /// This is what we receive from WebSocket streams and REST API calls. /// It uses Decimal for compatibility with external systems. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -350,10 +350,10 @@ pub struct OrderDelta { } /// Order book delta for streaming updates - INTERNAL HOT PATH VERSION -/// +/// /// This is what we use internally for processing order book updates. /// Converting to this format on ingress gives us massive performance gains. -/// +/// /// Why the performance matters: /// - We might process 10,000+ deltas per second in active markets /// - Each delta triggers multiple calculations (spread, impact, etc.) @@ -361,32 +361,35 @@ pub struct OrderDelta { /// keeping up with the market feed vs falling behind #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct FastOrderDelta { - pub token_id_hash: u64, // Hash of token_id for fast lookup (avoids string comparisons) + pub token_id_hash: u64, // Hash of token_id for fast lookup (avoids string comparisons) pub timestamp: DateTime, pub side: Side, - pub price: Price, // Price in ticks - pub size: Qty, // Size in fixed-point units (0 means remove level) + pub price: Price, // Price in ticks + pub size: Qty, // Size in fixed-point units (0 means remove level) pub sequence: u64, } impl FastOrderDelta { /// Create from external OrderDelta with validation and tick alignment - /// + /// /// This is where we enforce tick alignment - if the incoming price /// doesn't align to valid ticks, we either reject it or round it. /// This prevents bad data from corrupting our order book. - pub fn from_order_delta(delta: &OrderDelta, tick_size: Option) -> std::result::Result { + pub fn from_order_delta( + delta: &OrderDelta, + tick_size: Option, + ) -> std::result::Result { // Validate tick alignment if we have a tick size if let Some(tick_size) = tick_size { if !is_price_tick_aligned(delta.price, tick_size) { return Err("Price not aligned to tick size"); } } - + // Convert to fixed-point with validation let price = decimal_to_price(delta.price)?; let size = decimal_to_qty(delta.size)?; - + // Hash the token_id for fast lookups // This avoids string comparisons in the hot path let token_id_hash = { @@ -396,7 +399,7 @@ impl FastOrderDelta { delta.token_id.hash(&mut hasher); hasher.finish() }; - + Ok(Self { token_id_hash, timestamp: delta.timestamp, @@ -406,7 +409,7 @@ impl FastOrderDelta { sequence: delta.sequence, }) } - + /// Convert back to external OrderDelta (for API responses) /// We need the original token_id since we only store the hash pub fn to_order_delta(self, token_id: String) -> OrderDelta { @@ -419,7 +422,7 @@ impl FastOrderDelta { sequence: self.sequence, } } - + /// Check if this delta removes a level (size is zero) pub fn is_removal(self) -> bool { self.size == 0 @@ -663,39 +666,23 @@ pub struct WssSubscription { #[serde(tag = "type")] pub enum StreamMessage { #[serde(rename = "book_update")] - BookUpdate { - data: OrderDelta, - }, + BookUpdate { data: OrderDelta }, #[serde(rename = "trade")] - Trade { - data: FillEvent, - }, + Trade { data: FillEvent }, #[serde(rename = "order_update")] - OrderUpdate { - data: Order, - }, + OrderUpdate { data: Order }, #[serde(rename = "heartbeat")] - Heartbeat { - timestamp: DateTime, - }, + Heartbeat { timestamp: DateTime }, /// User channel events #[serde(rename = "user_order_update")] - UserOrderUpdate { - data: Order, - }, + UserOrderUpdate { data: Order }, #[serde(rename = "user_trade")] - UserTrade { - data: FillEvent, - }, + UserTrade { data: FillEvent }, /// Market channel events #[serde(rename = "market_book_update")] - MarketBookUpdate { - data: OrderDelta, - }, + MarketBookUpdate { data: OrderDelta }, #[serde(rename = "market_trade")] - MarketTrade { - data: FillEvent, - }, + MarketTrade { data: FillEvent }, } /// Subscription parameters for streaming @@ -757,7 +744,6 @@ pub type OrderId = String; pub type MarketId = String; pub type ClientId = String; - /// Parameters for querying open orders #[derive(Debug, Clone)] pub struct OpenOrderParams { @@ -811,19 +797,19 @@ impl TradeParams { if let Some(x) = &self.market { params.push(("market", x.clone())); } - + if let Some(x) = &self.maker_address { params.push(("maker_address", x.clone())); } - + if let Some(x) = &self.before { params.push(("before", x.to_string())); } - + if let Some(x) = &self.after { params.push(("after", x.to_string())); } - + params } } @@ -854,7 +840,6 @@ pub struct OpenOrder { pub created_at: u64, } - /// Balance allowance information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BalanceAllowance { @@ -1068,9 +1053,9 @@ pub struct Rewards { pub type ClientResult = anyhow::Result; /// Result type used throughout the client -pub type Result = std::result::Result; +pub type Result = std::result::Result; // Type aliases for 100% compatibility with baseline implementation pub type ApiCreds = ApiCredentials; pub type CreateOrderOptions = OrderOptions; -pub type OrderArgs = OrderRequest; \ No newline at end of file +pub type OrderArgs = OrderRequest; diff --git a/src/utils.rs b/src/utils.rs index ddae814..c490e6d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -4,6 +4,7 @@ //! operations in trading environments. use crate::errors::{PolyfillError, Result}; +use ::url::Url; use alloy_primitives::{Address, U256}; use base64::{engine::general_purpose::URL_SAFE, Engine}; use chrono::{DateTime, Utc}; @@ -13,7 +14,6 @@ use serde::Serialize; use sha2::Sha256; use std::str::FromStr; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use ::url::Url; type HmacSha256 = Hmac; @@ -66,8 +66,7 @@ pub mod time { /// Convert Unix timestamp to DateTime #[inline] pub fn secs_to_datetime(timestamp: u64) -> DateTime { - DateTime::from_timestamp(timestamp as i64, 0) - .unwrap_or_else(Utc::now) + DateTime::from_timestamp(timestamp as i64, 0).unwrap_or_else(Utc::now) } } @@ -95,12 +94,12 @@ pub mod crypto { Some(data) => { let json = serde_json::to_string(data)?; format!("{timestamp}{method}{path}{json}") - } + }, }; let mut mac = HmacSha256::new_from_slice(&decoded) .map_err(|e| PolyfillError::internal("HMAC initialization failed", e))?; - + mac.update(message.as_bytes()); let result = mac.finalize(); @@ -127,13 +126,13 @@ pub mod crypto { /// Price and size calculation utilities pub mod math { use super::*; - use rust_decimal::prelude::*; use crate::types::{Price, Qty, SCALE_FACTOR}; + use rust_decimal::prelude::*; // ======================================================================== // LEGACY DECIMAL FUNCTIONS (for backward compatibility) // ======================================================================== - // + // // These are kept for API compatibility, but internally we should use // the fixed-point versions below for better performance. @@ -185,10 +184,10 @@ pub mod math { // That's a 10-50x speedup on the critical path! /// Round price to tick size (FAST VERSION) - /// + /// /// This is much faster than the Decimal version because it's just /// integer division and multiplication. - /// + /// /// Example: round_to_tick_fast(6543, 10) = 6540 (rounds to nearest 10 ticks) #[inline] pub fn round_to_tick_fast(price_ticks: Price, tick_size_ticks: Price) -> Price { @@ -202,10 +201,10 @@ pub mod math { } /// Calculate notional value (price * size) (FAST VERSION) - /// + /// /// Returns the result in the same scale as our quantities. /// This avoids the expensive Decimal multiplication. - /// + /// /// Example: notional_fast(6543, 1000000) = 6543000000 (representing $654.30) #[inline] pub fn notional_fast(price_ticks: Price, size_units: Qty) -> i64 { @@ -218,47 +217,47 @@ pub mod math { } /// Calculate spread as percentage (FAST VERSION) - /// + /// /// Returns the spread as a percentage in basis points (1/100th of a percent). /// This avoids floating-point arithmetic entirely. - /// + /// /// Example: spread_pct_fast(6500, 6700) = Some(307) (representing 3.07%) #[inline] pub fn spread_pct_fast(bid_ticks: Price, ask_ticks: Price) -> Option { if bid_ticks == 0 || ask_ticks <= bid_ticks { return None; } - + let spread = ask_ticks - bid_ticks; // Calculate percentage in basis points (multiply by 10000 for 4 decimal places) // We use u64 for intermediate calculation to avoid overflow let spread_bps = ((spread as u64) * 10000) / (bid_ticks as u64); - + // Convert back to u32 (should always fit since spreads are typically small) Some(spread_bps as u32) } /// Calculate mid price (FAST VERSION) - /// + /// /// Returns the midpoint between bid and ask in ticks. /// Much faster than the Decimal version. - /// + /// /// Example: mid_price_fast(6500, 6700) = Some(6600) #[inline] pub fn mid_price_fast(bid_ticks: Price, ask_ticks: Price) -> Option { if bid_ticks == 0 || ask_ticks == 0 || ask_ticks <= bid_ticks { return None; } - + // Use u64 to avoid overflow in addition let sum = (bid_ticks as u64) + (ask_ticks as u64); Some((sum / 2) as Price) } /// Calculate spread in ticks (FAST VERSION) - /// + /// /// Simple subtraction - much faster than Decimal operations. - /// + /// /// Example: spread_fast(6500, 6700) = Some(200) (representing $0.02 spread) #[inline] pub fn spread_fast(bid_ticks: Price, ask_ticks: Price) -> Option { @@ -269,9 +268,9 @@ pub mod math { } /// Check if price is within valid range (FAST VERSION) - /// + /// /// Much faster than converting to Decimal and back. - /// + /// /// Example: is_valid_price_fast(6543, 1, 10000) = true #[inline] pub fn is_valid_price_fast(price_ticks: Price, min_tick: Price, max_tick: Price) -> bool { @@ -310,14 +309,14 @@ pub mod math { } else { Decimal::ZERO } - } + }, crate::types::Side::SELL => { if executed_price < target_price { (target_price - executed_price) / target_price } else { Decimal::ZERO } - } + }, } } } @@ -351,10 +350,7 @@ pub mod retry { } /// Retry a future with exponential backoff - pub async fn with_retry( - config: &RetryConfig, - mut operation: F, - ) -> Result + pub async fn with_retry(config: &RetryConfig, mut operation: F) -> Result where F: FnMut() -> Fut, Fut: Future>, @@ -367,7 +363,7 @@ pub mod retry { Ok(result) => return Ok(result), Err(err) => { last_error = Some(err.clone()); - + if !err.is_retryable() || attempt == config.max_attempts - 1 { return Err(err); } @@ -385,14 +381,21 @@ pub mod retry { // Exponential backoff delay = std::cmp::min( - Duration::from_nanos((delay.as_nanos() as f64 * config.backoff_factor) as u64), + Duration::from_nanos( + (delay.as_nanos() as f64 * config.backoff_factor) as u64, + ), config.max_delay, ); - } + }, } } - Err(last_error.unwrap_or_else(|| PolyfillError::internal("Retry loop failed", std::io::Error::other("No error captured")))) + Err(last_error.unwrap_or_else(|| { + PolyfillError::internal( + "Retry loop failed", + std::io::Error::other("No error captured"), + ) + })) } } @@ -440,10 +443,7 @@ pub mod url { } /// Add query parameters to URL - pub fn add_query_params( - mut url: url::Url, - params: &[(&str, &str)], - ) -> url::Url { + pub fn add_query_params(mut url: url::Url, params: &[(&str, &str)]) -> url::Url { { let mut query_pairs = url.query_pairs_mut(); for (key, value) in params { @@ -481,7 +481,7 @@ pub mod rate_limit { /// Try to consume a token, return true if successful pub fn try_consume(&self) -> bool { self.refill(); - + let mut tokens = self.tokens.lock().unwrap(); if *tokens > 0 { *tokens -= 1; @@ -495,7 +495,7 @@ pub mod rate_limit { let now = SystemTime::now(); let mut last_refill = self.last_refill.lock().unwrap(); let elapsed = now.duration_since(*last_refill).unwrap_or_default(); - + if elapsed >= self.refill_rate { let tokens_to_add = elapsed.as_nanos() / self.refill_rate.as_nanos(); let mut tokens = self.tokens.lock().unwrap(); @@ -513,7 +513,7 @@ mod tests { #[test] fn test_round_to_tick() { use math::round_to_tick; - + let price = Decimal::from_str("0.567").unwrap(); let tick = Decimal::from_str("0.01").unwrap(); let rounded = round_to_tick(price, tick); @@ -523,7 +523,7 @@ mod tests { #[test] fn test_mid_price() { use math::mid_price; - + let bid = Decimal::from_str("0.50").unwrap(); let ask = Decimal::from_str("0.52").unwrap(); let mid = mid_price(bid, ask).unwrap(); @@ -533,11 +533,11 @@ mod tests { #[test] fn test_token_units_conversion() { use math::{decimal_to_token_units, token_units_to_decimal}; - + let amount = Decimal::from_str("1.234567").unwrap(); let units = decimal_to_token_units(amount); assert_eq!(units, 1_234_567); - + let back = token_units_to_decimal(units); assert_eq!(back, amount); } @@ -545,11 +545,11 @@ mod tests { #[test] fn test_address_validation() { use address::parse_address; - + let valid = "0x1234567890123456789012345678901234567890"; assert!(parse_address(valid).is_ok()); - + let invalid = "invalid_address"; assert!(parse_address(invalid).is_err()); } -} \ No newline at end of file +}