fix: resolve rustfmt configuration duplicate key error and apply consistent code formatting across all source files

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