docs(book.rs): Updated documentation for orderbook implementation

This commit is contained in:
floor-licker
2025-08-14 19:11:52 -04:00
parent 322b338f3c
commit 7fe94ea9c8
6 changed files with 943 additions and 214 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ name = "polyfill-rs"
version = "0.1.0"
edition = "2021"
authors = ["Julius Tranquilli <julius@example.com>"]
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"
+23 -122
View File
@@ -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",
]
# 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"]
+730
View File
@@ -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<Self> {
// 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::<bool>() {
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(())
}
+5 -5
View File
@@ -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::<bool>() { Side::Buy } else { Side::Sell };
let side = if rand::random::<bool>() { Side::BUY } else { Side::SELL };
let size = Decimal::from(rand::random::<u64>() % 1000 + 100);
StreamMessage::BookUpdate {
+184 -84
View File
@@ -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<Utc>,
/// 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<Decimal, Decimal>,
/// Ask side (price -> size, sorted ascending)
/// Ask side (price -> size, sorted ascending)
/// BTreeMap keeps lowest asks first - people selling at cheapest prices
asks: BTreeMap<Decimal, Decimal>,
/// 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<Decimal>,
/// 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<BookLevel> {
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<BookLevel> {
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<Decimal> {
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<Decimal> {
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<Decimal> {
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<usize>) -> Vec<BookLevel> {
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<usize>) -> Vec<BookLevel> {
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<MarketImpact> {
// 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<RwLock<std::collections::HashMap<String, OrderBook>>>,
books: Arc<RwLock<std::collections::HashMap<String, OrderBook>>>, // 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<OrderBook> {
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<crate::types::OrderBook> {
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<Vec<crate::types::OrderBook>> {
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<usize> {
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<Utc>,
pub bid_count: usize,
pub ask_count: usize,
pub total_bid_size: Decimal,
pub total_ask_size: Decimal,
pub spread: Option<Decimal>,
pub spread_pct: Option<Decimal>,
pub mid_price: Option<Decimal>,
pub volatility: Option<Decimal>,
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<Decimal>, // Current spread (ask - bid)
pub spread_pct: Option<Decimal>, // Spread as percentage
pub mid_price: Option<Decimal>, // Current mid price
pub volatility: Option<Decimal>, // 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<Decimal> {
// 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
}
}
-2
View File
@@ -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