mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-07-27 20:47:46 +00:00
fix: resolve rustfmt configuration duplicate key error and apply consistent code formatting across all source files
This commit is contained in:
+14
-17
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+41
-32
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user