From 7fe94ea9c84ee40ac6c9799b21aa69f7ec5040df Mon Sep 17 00:00:00 2001 From: floor-licker Date: Thu, 14 Aug 2025 19:11:52 -0400 Subject: [PATCH] docs(book.rs): Updated documentation for orderbook implementation --- Cargo.toml | 2 +- clippy.toml | 145 ++----- examples/comprehensive_demo.rs | 730 +++++++++++++++++++++++++++++++++ examples/snipe.rs | 10 +- src/book.rs | 268 ++++++++---- src/lib.rs | 2 - 6 files changed, 943 insertions(+), 214 deletions(-) create mode 100644 examples/comprehensive_demo.rs diff --git a/Cargo.toml b/Cargo.toml index b34e602..5a62383 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "polyfill-rs" version = "0.1.0" edition = "2021" authors = ["Julius Tranquilli "] -description = "Production-ready Rust client for Polymarket with HFT optimizations" +description = "Production-ready Rust client for Polymarket" license = "MIT OR Apache-2.0" repository = "https://github.com/juliustranquilli/polyfill-rs" readme = "README.md" diff --git a/clippy.toml b/clippy.toml index 3aac1a8..a99d0c4 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,129 +1,30 @@ # Clippy configuration for polyfill-rs -# Optimized for HFT and production code quality +# Modern configuration format for production -# Performance -unsafe_code = "forbid" -missing_safety_doc = "warn" -undocumented_unsafe_blocks = "warn" +# Complexity thresholds +cognitive-complexity-threshold = 30 +too-many-arguments-threshold = 7 +too-many-lines-threshold = 150 +type-complexity-threshold = 250 -# Correctness -unwrap_used = "warn" -expect_used = "warn" -panic = "warn" -unreachable = "warn" -unimplemented = "warn" -todo = "warn" +# Size limits +trivial-copy-size-limit = 128 +pass-by-value-size-limit = 256 +large-error-threshold = 128 -# Complexity -cognitive_complexity = "warn" -too_many_arguments = "warn" -too_many_lines = "warn" -type_complexity = "warn" +# Naming +single-char-binding-names-threshold = 4 +enum-variant-name-threshold = 3 -# Style -doc_markdown = "warn" -missing_docs = "warn" -missing_errors_doc = "warn" -missing_panics_doc = "warn" +# Performance and safety +avoid-breaking-exported-api = false +check-private-items = true -# Suspicious -assign_op_pattern = "warn" -erasing_op = "warn" -eval_order_dependence = "warn" -float_cmp = "warn" -format_push_string = "warn" -identity_op = "warn" -ineffective_bit_mask = "warn" -int_plus_one = "warn" -large_enum_variant = "warn" -len_without_is_empty = "warn" -let_underscore_lock = "warn" -linkedlist = "warn" -map_entry = "warn" -modulo_one = "warn" -mut_mut = "warn" -mutex_integer = "warn" -needless_bitwise_bool = "warn" -needless_continue = "warn" -needless_for_each = "warn" -needless_pass_by_ref_mut = "warn" -needless_range_loop = "warn" -needless_return = "warn" -needless_update = "warn" -nonminimal_bool = "warn" -ok_expect = "warn" -option_map_unit_fn = "warn" -or_fun_call = "warn" -path_buf_push_overwrite = "warn" -precedence = "warn" -ptr_as_ptr = "warn" -redundant_clone = "warn" -redundant_closure = "warn" -redundant_closure_call = "warn" -redundant_else = "warn" -redundant_field_names = "warn" -redundant_guards = "warn" -redundant_pattern = "warn" -redundant_slicing = "warn" -same_item_push = "warn" -search_is_some = "warn" -self_named_constructors = "warn" -semicolon_if_nothing_returned = "warn" -single_char_pattern = "warn" -string_lit_as_bytes = "warn" -suboptimal_flops = "warn" -temporary_cstring_as_ptr = "warn" -toplevel_ref_arg = "warn" -transmute_int_to_char = "warn" -transmute_ptr_to_ptr = "warn" -unnecessary_filter_map = "warn" -unnecessary_fold = "warn" -unnecessary_mut_passed = "warn" -unnecessary_operation = "warn" -unnecessary_self_imports = "warn" -unneeded_field_pattern = "warn" -unreachable = "warn" -unreachable_pub = "warn" -unsafe_removed_from_name = "warn" -unused_async = "warn" -unused_assignments = "warn" -unused_attributes = "warn" -unused_borrowed_ref = "warn" -unused_collect = "warn" -unused_comparisons = "warn" -unused_doc_comments = "warn" -unused_enumerate_index = "warn" -unused_features = "warn" -unused_imports = "warn" -unused_labels = "warn" -unused_macros = "warn" -unused_parens = "warn" -unused_qualifications = "warn" -unused_unsafe = "warn" -unused_variables = "warn" -useless_attribute = "warn" -useless_conversion = "warn" -useless_format = "warn" -useless_let_if_seq = "warn" -useless_transmute = "warn" -vec_init_then_push = "warn" -verbose_file_reads = "warn" -while_let_on_iterator = "warn" +# Documentation +missing-docs-in-crate-items = true -# HFT-specific -# Allow some performance optimizations that might be considered "unsafe" in general code -allow = [ - "cast_possible_truncation", - "cast_possible_wrap", - "cast_precision_loss", - "cast_sign_loss", - "clippy::inline_always", - "clippy::module_name_repetitions", - "clippy::must_use_candidate", - "clippy::new_without_default", - "clippy::redundant_pub_crate", - "clippy::too_many_arguments", - "clippy::type_complexity", - "clippy::upper_case_acronyms", - "clippy::vec_init_then_push", -] \ No newline at end of file +# Allow certain patterns common in HFT code +allowed-idents-below-min-chars = ["id", "tx", "rx", "ok", "io", "db", "ws", "ts", "ms", "us", "ns"] + +# Third-party crates where we allow more lenient rules +third-party = ["serde", "tokio", "alloy"] \ No newline at end of file diff --git a/examples/comprehensive_demo.rs b/examples/comprehensive_demo.rs new file mode 100644 index 0000000..ba2546a --- /dev/null +++ b/examples/comprehensive_demo.rs @@ -0,0 +1,730 @@ +//! Comprehensive Demo for polyfill-rs +//! +//! This example demonstrates all the major functions and capabilities of the polyfill-rs library: +//! - Basic client operations and API calls +//! - Order book management and analytics +//! - Real-time streaming capabilities +//! - Trade execution and fill processing +//! - Utility functions and mathematical operations +//! - Error handling and retry logic +//! - 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}, + + // Utility functions + utils::{crypto, math, retry, time, url, rate_limit, address}, + + // Configuration + ClientConfig, +}; + +use rust_decimal::Decimal; +use rust_decimal_macros::dec; +use std::time::Duration; +use tokio::time::sleep; +use tracing::{error, info, debug}; + +/// Comprehensive demo showcasing all polyfill-rs functionality +pub struct PolyfillDemo { + /// Basic HTTP client + client: ClobClient, + /// Advanced client with configuration + advanced_client: PolyfillClient, + /// Order book manager + book_manager: OrderBookManager, + /// Fill engine for trade execution + fill_engine: FillEngine, + /// Fill processor for handling fills + fill_processor: FillProcessor, + /// Stream manager for real-time data + stream_manager: StreamManager, + /// Rate limiter + rate_limiter: rate_limit::TokenBucket, + /// Statistics + stats: DemoStats, +} + +/// Demo statistics +#[derive(Debug, Clone)] +pub struct DemoStats { + pub api_calls: u64, + pub orders_processed: u64, + pub fills_processed: u64, + pub stream_messages: u64, + pub errors: u64, + pub total_volume: Decimal, +} + +impl Default for DemoStats { + fn default() -> Self { + Self { + api_calls: 0, + orders_processed: 0, + fills_processed: 0, + stream_messages: 0, + errors: 0, + total_volume: dec!(0), + } + } +} + +impl PolyfillDemo { + /// Create a new comprehensive demo + 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 + max_slippage: Some(dec!(0.01)), // 1% max slippage + 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 + ); + + // 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, + book_manager, + fill_engine, + fill_processor, + stream_manager, + rate_limiter, + stats: DemoStats::default(), + }) + } + + /// 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(Some(5)).await { + Ok(markets) => { + info!("Found {} markets", markets.data.len()); + for market in &markets.data[..std::cmp::min(3, markets.data.len())] { + 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()); + + // 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 { + token_id: token_id.to_string(), + timestamp: chrono::Utc::now(), + side: Side::BUY, + price: bid.price, + size: bid.size, + sequence: i as u64, + })?; + } + + for (i, ask) in order_book.asks.iter().enumerate() { + local_book.apply_delta(OrderDelta { + token_id: token_id.to_string(), + timestamp: chrono::Utc::now(), + side: Side::SELL, + price: ask.price, + size: ask.size, + 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); + if let Some(spread) = analytics.spread { + 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:"); + info!(" Average price: {}", impact.average_price); + 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, + ); + + info!("Created order args: {:?}", order_args); + + // Create market order request + let market_order = MarketOrderRequest { + token_id: "12345".to_string(), + side: Side::BUY, + amount: dec!(100.0), + 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(), + side: Side::BUY, + price: dec!(0.75), + size: dec!(100.0), + order_type: OrderType::GTC, + 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 { + token_id: "12345".to_string(), + timestamp: chrono::Utc::now(), + side: Side::BUY, + price: dec!(0.70) + Decimal::from(i) * dec!(0.01), + size: dec!(100.0), + sequence: i, + })?; + } + + for i in 1..=5 { + book.apply_delta(OrderDelta { + token_id: "12345".to_string(), + timestamp: chrono::Utc::now(), + side: Side::SELL, + price: dec!(0.80) + Decimal::from(i) * dec!(0.01), + size: dec!(100.0), + sequence: i + 10, + })?; + } + + info!("Created order book with liquidity"); + + // Execute market order + let market_order = MarketOrderRequest { + token_id: "12345".to_string(), + side: Side::BUY, + amount: dec!(50.0), + 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)?; + + info!("Market order execution result:"); + info!(" Status: {:?}", fill_result.status); + info!(" Total size: {}", fill_result.total_size); + info!(" Average price: {}", fill_result.average_price); + 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(), + side: Side::SELL, + price: dec!(0.85), + size: dec!(25.0), + order_type: OrderType::GTC, + 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); + + 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"; + match address::parse_address(address) { + 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" }); + } + + 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, + initial_delay: Duration::from_millis(100), + max_delay: Duration::from_secs(1), + 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::new(std::io::ErrorKind::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")); + 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 { + address: "0x1234567890123456789012345678901234567890".to_string(), + signature: "mock_signature".to_string(), + timestamp: time::now_secs(), + nonce: crypto::generate_nonce().to_string(), + }, + markets: Some(vec!["market1".to_string(), "market2".to_string()]), + 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::BookUpdate { + data: OrderDelta { + token_id: "12345".to_string(), + timestamp: chrono::Utc::now(), + side: Side::BUY, + price: dec!(0.75), + size: dec!(100.0), + sequence: 1, + } + }, + StreamMessage::Trade { + data: FillEvent { + id: "fill1".to_string(), + order_id: "order1".to_string(), + token_id: "12345".to_string(), + side: Side::BUY, + price: dec!(0.75), + size: dec!(50.0), + timestamp: chrono::Utc::now(), + 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 } => { + info!(" Processing book update for token: {}", data.token_id); + if let Err(e) = self.book_manager.apply_delta(data.clone()) { + 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); + 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:"); + info!(" Total orders: {}", fill_stats.total_orders); + 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:"); + info!(" Pending orders: {}", processor_stats.pending_orders); + info!(" Pending fills: {}", processor_stats.pending_fills); + 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); + info!(" Orders processed: {}", self.stats.orders_processed); + info!(" Fills processed: {}", self.stats.fills_processed); + 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 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(()) + } +} + +#[tokio::main] +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/snipe.rs b/examples/snipe.rs index cc47416..3102c71 100644 --- a/examples/snipe.rs +++ b/examples/snipe.rs @@ -205,9 +205,9 @@ impl SnipeStrategy { // Determine side based on market conditions let side = if bid > ask { - Side::Sell // Crossed market, sell + Side::SELL // Crossed market, sell } else { - Side::Buy // Normal market, buy + Side::BUY // Normal market, buy }; // Create market order request @@ -228,7 +228,7 @@ impl SnipeStrategy { book_impl.apply_delta(OrderDelta { token_id: self.token_id.clone(), timestamp: chrono::Utc::now(), - side: Side::Buy, + side: Side::BUY, price: level.price, size: level.size, sequence: 1, @@ -239,7 +239,7 @@ impl SnipeStrategy { book_impl.apply_delta(OrderDelta { token_id: self.token_id.clone(), timestamp: chrono::Utc::now(), - side: Side::Sell, + side: Side::SELL, price: level.price, size: level.size, sequence: 2, @@ -327,7 +327,7 @@ 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 { diff --git a/src/book.rs b/src/book.rs index 26cab4e..c9a212d 100644 --- a/src/book.rs +++ b/src/book.rs @@ -1,75 +1,101 @@ //! Order book management for Polymarket client -//! -//! This module provides high-performance order book operations optimized -//! for latency-sensitive trading environments. use crate::errors::{PolyfillError, Result}; use crate::types::*; use crate::utils::math; use rust_decimal::Decimal; -use std::collections::BTreeMap; -use std::sync::{Arc, RwLock}; -use tracing::{debug, trace, warn}; +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; use std::collections::HashMap; /// 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. #[derive(Debug, Clone)] pub struct OrderBook { - /// Token ID this book represents + /// Token ID this book represents (like "123456" for a specific prediction market outcome) pub token_id: String, + /// Current sequence number for ordering updates + /// This helps us ignore old/duplicate updates that arrive out of order pub sequence: u64, - /// Last update timestamp + + /// Last update timestamp - when we last got new data for this book pub timestamp: chrono::DateTime, + /// Bid side (price -> size, sorted descending) + /// BTreeMap automatically keeps highest bids first, which is what we want + /// Key = price (like 0.65), Value = total size at that price (like 1000 tokens) bids: BTreeMap, - /// Ask side (price -> size, sorted ascending) + + /// Ask side (price -> size, sorted ascending) + /// BTreeMap keeps lowest asks first - people selling at cheapest prices asks: BTreeMap, - /// Minimum tick size for this market + + /// Minimum tick size for this market (like 0.01 = prices must be in penny increments) + /// Some markets only allow certain price increments tick_size: Option, - /// Maximum depth to maintain + + /// 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, } impl OrderBook { /// Create a new order book + /// Just sets up empty bid/ask maps and basic metadata pub fn new(token_id: String, max_depth: usize) -> Self { Self { token_id, - sequence: 0, + sequence: 0, // Start at 0, will increment as we get updates timestamp: Utc::now(), - bids: BTreeMap::new(), - asks: BTreeMap::new(), - tick_size: None, + bids: BTreeMap::new(), // Empty to start + asks: BTreeMap::new(), // Empty to start + tick_size: None, // We'll set this later when we learn about the market max_depth, } } /// Set the tick size for this book + /// This tells us the minimum price increment allowed (like 0.01 for penny increments) pub fn set_tick_size(&mut self, tick_size: Decimal) { self.tick_size = Some(tick_size); } - /// Get the current best bid + /// 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 pub fn best_bid(&self) -> Option { self.bids.iter().next_back().map(|(&price, &size)| BookLevel { price, size }) } - /// Get the current best ask + /// 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 pub fn best_ask(&self) -> Option { self.asks.iter().next().map(|(&price, &size)| BookLevel { price, size }) } - /// Get the current spread + /// Get the current spread (difference between best ask and best bid) + /// This tells us how "tight" the market is - smaller spread = more liquid market pub fn spread(&self) -> Option { match (self.best_bid(), self.best_ask()) { (Some(bid), Some(ask)) => Some(ask.price - bid.price), - _ => None, + _ => None, // Can't calculate spread if we're missing bid or ask } } - /// Get the current mid price + /// Get the current mid price (halfway between best bid and ask) + /// This is often used as the "fair value" of the market pub fn mid_price(&self) -> Option { math::mid_price( self.best_bid()?.price, @@ -77,7 +103,8 @@ impl OrderBook { ) } - /// Get the spread as a percentage + /// Get the spread as a percentage (relative to the bid price) + /// Useful for comparing spreads across different price levels pub fn spread_pct(&self) -> Option { match (self.best_bid(), self.best_ask()) { (Some(bid), Some(ask)) => math::spread_pct(bid.price, ask.price), @@ -85,57 +112,63 @@ impl OrderBook { } } - /// Get all bids up to a certain depth + /// Get all bids up to a certain depth (top N price levels) + /// Returns them in descending price order (best bids first) pub fn bids(&self, depth: Option) -> Vec { let depth = depth.unwrap_or(self.max_depth); self.bids .iter() - .rev() - .take(depth) + .rev() // Reverse because we want highest prices first + .take(depth) // Only take the top N levels .map(|(&price, &size)| BookLevel { price, size }) .collect() } - /// Get all asks up to a certain depth + /// Get all asks up to a certain depth (top N price levels) + /// Returns them in ascending price order (best asks first) pub fn asks(&self, depth: Option) -> Vec { let depth = depth.unwrap_or(self.max_depth); self.asks - .iter() - .take(depth) + .iter() // Already in ascending order, so no need to reverse + .take(depth) // Only take the top N levels .map(|(&price, &size)| BookLevel { price, size }) .collect() } /// Get the full book snapshot + /// Creates a copy of the current state that can be safely passed around + /// without worrying about the original book changing pub fn snapshot(&self) -> crate::types::OrderBook { crate::types::OrderBook { token_id: self.token_id.clone(), timestamp: self.timestamp, - bids: self.bids(None), - asks: self.asks(None), + bids: self.bids(None), // Get all bids (up to max_depth) + asks: self.asks(None), // Get all asks (up to max_depth) sequence: self.sequence, } } /// Apply a delta update to the book + /// A "delta" is an incremental change - like "add 100 tokens at $0.65" or "remove all at $0.70" pub fn apply_delta(&mut self, delta: OrderDelta) -> Result<()> { - // Validate sequence ordering + // 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); return Ok(()); } - // Update sequence and timestamp + // Update our tracking info self.sequence = delta.sequence; self.timestamp = delta.timestamp; - // Apply the delta + // Apply the actual change to the appropriate side match delta.side { Side::BUY => self.apply_bid_delta(delta.price, delta.size), Side::SELL => self.apply_ask_delta(delta.price, delta.size), } - // Maintain depth limits + // Keep the book from getting too deep (memory management) self.trim_depth(); debug!( @@ -149,82 +182,114 @@ impl OrderBook { Ok(()) } - /// Apply a bid-side delta + /// Apply a bid-side delta (someone wants to buy) + /// If size is 0, it means "remove this price level entirely" + /// Otherwise, set the total size at this price level fn apply_bid_delta(&mut self, price: Decimal, size: Decimal) { if size.is_zero() { - self.bids.remove(&price); + self.bids.remove(&price); // No more buyers at this price } else { - self.bids.insert(price, size); + self.bids.insert(price, size); // Update total size at this price } } - /// Apply an ask-side delta + /// Apply an ask-side delta (someone wants to sell) + /// Same logic as bids - size of 0 means remove the price level fn apply_ask_delta(&mut self, price: Decimal, size: Decimal) { if size.is_zero() { - self.asks.remove(&price); + self.asks.remove(&price); // No more sellers at this price } else { - self.asks.insert(price, size); + self.asks.insert(price, size); // Update total size at this price } } /// 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 + /// 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 + fn trim_depth(&mut self) { + // For bids, remove the LOWEST prices (worst bids) if we have too many + // Example: If best bid is $0.65, we don't care about bids at $0.10 if self.bids.len() > self.max_depth { let to_remove = self.bids.len() - self.max_depth; for _ in 0..to_remove { - self.bids.pop_first(); + self.bids.pop_first(); // Remove lowest bid prices (furthest from market) } } + // 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; for _ in 0..to_remove { - self.asks.pop_last(); + self.asks.pop_last(); // Remove highest ask prices (furthest from market) } } } /// Calculate the market impact for a given order size + /// This is exactly why we don't need deep levels - if your order would require + /// hitting prices way off the current market (like $0.95 when best ask is $0.67), + /// you'd never actually place that order. You'd either: + /// 1. Break it into smaller pieces over time + /// 2. Use a different trading strategy + /// 3. Accept that there's not enough liquidity right now pub fn calculate_market_impact(&self, side: Side, size: Decimal) -> Option { + // Get the levels we'd be trading against let levels = match side { - Side::BUY => self.asks(None), - Side::SELL => self.bids(None), + 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() { - return None; + return None; // No liquidity available } let mut remaining_size = size; let mut total_cost = Decimal::ZERO; let mut weighted_price = Decimal::ZERO; + // Walk through each price level, filling as much as we can 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; + weighted_price += level_cost; // This accumulates the weighted average remaining_size -= fill_size; if remaining_size.is_zero() { - break; + break; // We've filled our entire order } } if remaining_size > Decimal::ZERO { - return None; // Not enough liquidity + // Not enough liquidity to fill the whole order + // 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; } 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 + (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 + (best_bid - avg_price) / best_bid // How much worse than best bid } }; @@ -237,20 +302,23 @@ impl OrderBook { } /// Check if the book is stale (no recent updates) + /// Useful for detecting when we've lost connection to live data pub fn is_stale(&self, max_age: std::time::Duration) -> bool { let age = Utc::now() - self.timestamp; age > chrono::Duration::from_std(max_age).unwrap_or_default() } /// Get the total liquidity at a given price level + /// Tells you how much you can buy/sell at exactly this price pub fn liquidity_at_price(&self, price: Decimal, side: Side) -> Decimal { match side { - Side::BUY => self.asks.get(&price).copied().unwrap_or_default(), - Side::SELL => self.bids.get(&price).copied().unwrap_or_default(), + Side::BUY => self.asks.get(&price).copied().unwrap_or_default(), // How much we can buy at this price + Side::SELL => self.bids.get(&price).copied().unwrap_or_default(), // How much we can sell at this price } } /// 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 { let levels: Vec<_> = match side { Side::BUY => self.asks.range(min_price..=max_price).collect(), @@ -261,32 +329,44 @@ impl OrderBook { } /// Validate that prices are properly ordered + /// A healthy book should have best bid < best ask (otherwise there's an arbitrage opportunity) pub fn is_valid(&self) -> bool { match (self.best_bid(), self.best_ask()) { - (Some(bid), Some(ask)) => bid.price < ask.price, - _ => true, // Empty book is valid + (Some(bid), Some(ask)) => bid.price < ask.price, // Normal market condition + _ => true, // Empty book is technically valid } } } /// Market impact calculation result +/// This tells you what would happen if you executed a large order #[derive(Debug, Clone)] pub struct MarketImpact { - pub average_price: Decimal, - pub impact_pct: Decimal, - pub total_cost: Decimal, - pub size_filled: Decimal, + 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)] pub struct OrderBookManager { - books: Arc>>, + books: Arc>>, // Token ID -> OrderBook max_depth: usize, } impl OrderBookManager { /// Create a new order book manager + /// Starts with an empty collection of books pub fn new(max_depth: usize) -> Self { Self { books: Arc::new(RwLock::new(std::collections::HashMap::new())), @@ -295,14 +375,16 @@ 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") })?; if let Some(book) = books.get(token_id) { - Ok(book.clone()) + Ok(book.clone()) // Return a copy of the existing book } else { + // Create a new book for this token let book = OrderBook::new(token_id.to_string(), self.max_depth); books.insert(token_id.to_string(), book.clone()); Ok(book) @@ -310,11 +392,13 @@ 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") })?; + // Find the book for this token (must already exist) let book = books .get_mut(&delta.token_id) .ok_or_else(|| { @@ -324,10 +408,12 @@ impl OrderBookManager { ) })?; + // Apply the update to the specific book book.apply_delta(delta) } /// 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") @@ -335,7 +421,7 @@ impl OrderBookManager { books .get(token_id) - .map(|book| book.snapshot()) + .map(|book| book.snapshot()) // Create a snapshot copy .ok_or_else(|| { PolyfillError::market_data( format!("No book found for token: {}", token_id), @@ -345,6 +431,7 @@ 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") @@ -354,13 +441,15 @@ impl OrderBookManager { } /// Remove stale books + /// 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 initial_count = books.len(); - books.retain(|_, book| !book.is_stale(max_age)); + books.retain(|_, book| !book.is_stale(max_age)); // Keep only non-stale books let removed = initial_count - books.len(); if removed > 0 { @@ -372,27 +461,29 @@ impl OrderBookManager { } /// Order book analytics and statistics +/// Provides a summary view of the book's health and characteristics #[derive(Debug, Clone)] pub struct BookAnalytics { pub token_id: String, pub timestamp: chrono::DateTime, - pub bid_count: usize, - pub ask_count: usize, - pub total_bid_size: Decimal, - pub total_ask_size: Decimal, - pub spread: Option, - pub spread_pct: Option, - pub mid_price: Option, - pub volatility: Option, + 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 volatility: Option, // Price volatility (if calculated) } impl OrderBook { /// Calculate analytics for this book + /// Gives you a quick health check of the market pub fn analytics(&self) -> BookAnalytics { let bid_count = self.bids.len(); let ask_count = self.asks.len(); - let total_bid_size: Decimal = self.bids.values().sum(); - let total_ask_size: Decimal = self.asks.values().sum(); + let total_bid_size: Decimal = self.bids.values().sum(); // Add up all bid sizes + let total_ask_size: Decimal = self.asks.values().sum(); // Add up all ask sizes BookAnalytics { token_id: self.token_id.clone(), @@ -409,9 +500,11 @@ impl OrderBook { } /// Calculate price volatility (simplified) + /// This is a placeholder - real volatility needs historical price data fn calculate_volatility(&self) -> Option { // This is a simplified volatility calculation - // In a real implementation, you'd want to track price history + // In a real implementation, you'd want to track price history over time + // and calculate standard deviation of price changes None } } @@ -419,20 +512,23 @@ impl OrderBook { #[cfg(test)] mod tests { use super::*; - use rust_decimal_macros::dec; + use rust_decimal_macros::dec; // Convenient macro for creating Decimal literals #[test] fn test_order_book_creation() { + // Test that we can create a new empty order book let book = OrderBook::new("test_token".to_string(), 10); assert_eq!(book.token_id, "test_token"); - assert_eq!(book.bids.len(), 0); - assert_eq!(book.asks.len(), 0); + assert_eq!(book.bids.len(), 0); // Should start empty + assert_eq!(book.asks.len(), 0); // Should start empty } #[test] 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(), timestamp: Utc::now(), @@ -443,16 +539,17 @@ mod tests { }; book.apply_delta(delta).unwrap(); - assert_eq!(book.sequence, 1); - assert_eq!(book.best_bid().unwrap().price, dec!(0.5)); - assert_eq!(book.best_bid().unwrap().size, dec!(100)); + assert_eq!(book.sequence, 1); // Sequence should update + assert_eq!(book.best_bid().unwrap().price, dec!(0.5)); // Should be our bid + assert_eq!(book.best_bid().unwrap().size, dec!(100)); // Should be our size } #[test] 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 bid + // Add a bid at $0.50 book.apply_delta(OrderDelta { token_id: "test_token".to_string(), timestamp: Utc::now(), @@ -462,7 +559,7 @@ mod tests { sequence: 1, }).unwrap(); - // Add ask + // Add an ask at $0.52 book.apply_delta(OrderDelta { token_id: "test_token".to_string(), timestamp: Utc::now(), @@ -473,14 +570,16 @@ mod tests { }).unwrap(); let spread = book.spread().unwrap(); - assert_eq!(spread, dec!(0.02)); + assert_eq!(spread, dec!(0.02)); // $0.52 - $0.50 = $0.02 } #[test] 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 + // 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() { book.apply_delta(OrderDelta { token_id: "test_token".to_string(), @@ -492,8 +591,9 @@ mod tests { }).unwrap(); } + // Try to buy 150 tokens (will need to hit multiple price levels) let impact = book.calculate_market_impact(Side::BUY, dec!(150)).unwrap(); - assert!(impact.average_price > dec!(0.50)); - assert!(impact.average_price < dec!(0.51)); + assert!(impact.average_price > dec!(0.50)); // Should be worse than best price + assert!(impact.average_price < dec!(0.51)); // But not as bad as second level } } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index e96a03c..36bdd9b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,5 @@ //! Polyfill-rs: High-performance Rust client for Polymarket //! -//! A production-ready Rust client for Polymarket optimized for high-frequency trading. -//! //! # Features //! //! - **High-performance order book management** with optimized data structures